From 6be3fda6fc746b1285e6dee72de1118177ba429e Mon Sep 17 00:00:00 2001
From: CEnnis91 <cennis91@gmail.com>
Date: Thu, 8 Feb 2024 01:45:44 -0500
Subject: [PATCH 001/679] Fix swift packages not resolving (#29095)

Fixes #29094
---
 routers/api/packages/swift/swift.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 427e262d06..6ad289e51e 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -157,7 +157,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
 }
 
 type Resource struct {
-	Name     string `json:"id"`
+	Name     string `json:"name"`
 	Type     string `json:"type"`
 	Checksum string `json:"checksum"`
 }

From eb5ddc0a78ecfe007a6e279a3c59711cdfb3f701 Mon Sep 17 00:00:00 2001
From: CEnnis91 <cennis91@gmail.com>
Date: Thu, 8 Feb 2024 03:53:44 -0500
Subject: [PATCH 002/679] Fix incorrect link to swift doc and swift
 package-registry login command (#29096)

Fixes a few mistakes in the Swift package registry documentation.

Syntax for the `package-registry login` command can be found
[here](https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageRegistry/PackageRegistryUsage.md#registry-authentication).
I was not sure the best way to compress all of that information, so I
just focused on making sure the incorrect `package-registry set` command
was fixed.
---
 docs/content/usage/packages/overview.en-us.md | 2 +-
 docs/content/usage/packages/swift.en-us.md    | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/content/usage/packages/overview.en-us.md b/docs/content/usage/packages/overview.en-us.md
index 44d18ff482..89fc6f286e 100644
--- a/docs/content/usage/packages/overview.en-us.md
+++ b/docs/content/usage/packages/overview.en-us.md
@@ -42,7 +42,7 @@ The following package managers are currently supported:
 | [PyPI](usage/packages/pypi.md) | Python | `pip`, `twine` |
 | [RPM](usage/packages/rpm.md) | - | `yum`, `dnf`, `zypper` |
 | [RubyGems](usage/packages/rubygems.md) | Ruby | `gem`, `Bundler` |
-| [Swift](usage/packages/rubygems.md) | Swift | `swift` |
+| [Swift](usage/packages/swift.md) | Swift | `swift` |
 | [Vagrant](usage/packages/vagrant.md) | - | `vagrant` |
 
 **The following paragraphs only apply if Packages are not globally disabled!**
diff --git a/docs/content/usage/packages/swift.en-us.md b/docs/content/usage/packages/swift.en-us.md
index 606fa20b36..38eb155641 100644
--- a/docs/content/usage/packages/swift.en-us.md
+++ b/docs/content/usage/packages/swift.en-us.md
@@ -26,7 +26,8 @@ To work with the Swift package registry, you need to use [swift](https://www.swi
 To register the package registry and provide credentials, execute:
 
 ```shell
-swift package-registry set https://gitea.example.com/api/packages/{owner}/swift -login {username} -password {password}
+swift package-registry set https://gitea.example.com/api/packages/{owner}/swift
+swift package-registry login https://gitea.example.com/api/packages/{owner}/swift --username {username} --password {password}
 ```
 
 | Placeholder  | Description |

From 98e7e3a5f07b8bc620e26bc1ab6f7a86bccbb7cb Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 8 Feb 2024 13:07:02 +0100
Subject: [PATCH 003/679] Move vitest setup file to root (#29097)

I'm using this convention in other projects and I think it makes sense
for gitea too because the vitest setup file is loaded globally for all
tests, not just ones in web_src, so it makes sense to be in the root.
---
 vitest.config.js                              | 2 +-
 web_src/js/{test/setup.js => vitest.setup.js} | 0
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename web_src/js/{test/setup.js => vitest.setup.js} (100%)

diff --git a/vitest.config.js b/vitest.config.js
index 9a6cb4e560..be6c0eadfa 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -5,7 +5,7 @@ import {stringPlugin} from 'vite-string-plugin';
 export default defineConfig({
   test: {
     include: ['web_src/**/*.test.js'],
-    setupFiles: ['./web_src/js/test/setup.js'],
+    setupFiles: ['web_src/js/vitest.setup.js'],
     environment: 'jsdom',
     testTimeout: 20000,
     open: false,
diff --git a/web_src/js/test/setup.js b/web_src/js/vitest.setup.js
similarity index 100%
rename from web_src/js/test/setup.js
rename to web_src/js/vitest.setup.js

From 8c6ffdac378654f9d2171ebdbc46becf1571f7fe Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 8 Feb 2024 20:31:38 +0800
Subject: [PATCH 004/679] Remove unnecessary parameter (#29092)

The parameter extraConfigs has never been used anywhere. This PR just
removed it. It can be taken back once it's needed.
---
 models/unittest/testdb.go          | 4 ++--
 modules/setting/config_provider.go | 8 +-------
 modules/setting/setting.go         | 4 ++--
 3 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go
index 4c668ad04b..cb90c12f2b 100644
--- a/models/unittest/testdb.go
+++ b/models/unittest/testdb.go
@@ -44,12 +44,12 @@ func fatalTestError(fmtStr string, args ...any) {
 }
 
 // InitSettings initializes config provider and load common settings for tests
-func InitSettings(extraConfigs ...string) {
+func InitSettings() {
 	if setting.CustomConf == "" {
 		setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
 		_ = os.Remove(setting.CustomConf)
 	}
-	setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n"))
+	setting.InitCfgProvider(setting.CustomConf)
 	setting.LoadCommonSettings()
 
 	if err := setting.PrepareAppDataPath(); err != nil {
diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go
index 132f4acea1..3fa3f3b50b 100644
--- a/modules/setting/config_provider.go
+++ b/modules/setting/config_provider.go
@@ -196,7 +196,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
 
 // NewConfigProviderFromFile load configuration from file.
 // NOTE: do not print any log except error.
-func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
+func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
 	cfg := ini.Empty(configProviderLoadOptions())
 	loadedFromEmpty := true
 
@@ -213,12 +213,6 @@ func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvi
 		}
 	}
 
-	for _, s := range extraConfigs {
-		if err := cfg.Append([]byte(s)); err != nil {
-			return nil, fmt.Errorf("unable to append more config: %v", err)
-		}
-	}
-
 	cfg.NameMapper = ini.SnackCase
 	return &iniConfigProvider{
 		file:            file,
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index d444d9a017..72aee2a092 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -90,9 +90,9 @@ func PrepareAppDataPath() error {
 	return nil
 }
 
-func InitCfgProvider(file string, extraConfigs ...string) {
+func InitCfgProvider(file string) {
 	var err error
-	if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil {
+	if CfgProvider, err = NewConfigProviderFromFile(file); err != nil {
 		log.Fatal("Unable to init config provider from %q: %v", file, err)
 	}
 	CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls

From e600c35f066c79b717dc0c416b07d5c34502d286 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Thu, 8 Feb 2024 21:00:17 +0800
Subject: [PATCH 005/679] Only delete scheduled workflows when needed (#29091)

Fix #29040

`handleSchedules` should be called only if `DetectWorkflows` should
detect schedule workflows
---
 services/actions/notifier_helper.go | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 77173e58a3..8852f23c5f 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -157,10 +157,11 @@ func notify(ctx context.Context, input *notifyInput) error {
 
 	var detectedWorkflows []*actions_module.DetectedWorkflow
 	actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
+	shouldDetectSchedules := input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch
 	workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
 		input.Event,
 		input.Payload,
-		input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch,
+		shouldDetectSchedules,
 	)
 	if err != nil {
 		return fmt.Errorf("DetectWorkflows: %w", err)
@@ -207,8 +208,10 @@ func notify(ctx context.Context, input *notifyInput) error {
 		}
 	}
 
-	if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
-		return err
+	if shouldDetectSchedules {
+		if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
+			return err
+		}
 	}
 
 	return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)

From 96ad1d6340038c0c841d9cad9a440daee3241aac Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 8 Feb 2024 21:25:09 +0800
Subject: [PATCH 006/679] Fix push to create with capitalize repo name (#29090)

Fix #29073
---
 cmd/serv.go | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index 726663660b..3cc504beb4 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -216,16 +216,18 @@ func runServ(c *cli.Context) error {
 		}
 	}
 
-	// LowerCase and trim the repoPath as that's how they are stored.
-	repoPath = strings.ToLower(strings.TrimSpace(repoPath))
-
 	rr := strings.SplitN(repoPath, "/", 2)
 	if len(rr) != 2 {
 		return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
 	}
 
-	username := strings.ToLower(rr[0])
-	reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
+	username := rr[0]
+	reponame := strings.TrimSuffix(rr[1], ".git")
+
+	// LowerCase and trim the repoPath as that's how they are stored.
+	// This should be done after splitting the repoPath into username and reponame
+	// so that username and reponame are not affected.
+	repoPath = strings.ToLower(strings.TrimSpace(repoPath))
 
 	if alphaDashDotPattern.MatchString(reponame) {
 		return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)

From ce9978bfd4e035ed065b02b28e02905674320b6a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 8 Feb 2024 14:49:44 +0100
Subject: [PATCH 007/679] Use defaults browserslist (#29098)

IE usage has dropped enough to not be included in the defaults
browserslist anymore as per https://browsersl.ist/#q=defaults, so we can
use the defaults now.
---
 package.json | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/package.json b/package.json
index 569955d815..ef1fcca545 100644
--- a/package.json
+++ b/package.json
@@ -85,8 +85,6 @@
     "vitest": "1.2.2"
   },
   "browserslist": [
-    "defaults",
-    "not ie > 0",
-    "not ie_mob > 0"
+    "defaults"
   ]
 }

From da2f03750f9672c5aac48209539874f2af2673f1 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Thu, 8 Feb 2024 23:01:19 +0100
Subject: [PATCH 008/679] Display friendly error message (#29105)

`ctx.Error` only displays the text but `ctx.ServerError` renders the
usual error page.
---
 routers/web/repo/actions/actions.go | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index fe528a483b..5f6a1ec36a 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -61,17 +61,17 @@ func List(ctx *context.Context) {
 
 	var workflows []Workflow
 	if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("IsEmpty", err)
 		return
 	} else if !empty {
 		commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
 		if err != nil {
-			ctx.Error(http.StatusInternalServerError, err.Error())
+			ctx.ServerError("GetBranchCommit", err)
 			return
 		}
 		entries, err := actions.ListWorkflows(commit)
 		if err != nil {
-			ctx.Error(http.StatusInternalServerError, err.Error())
+			ctx.ServerError("ListWorkflows", err)
 			return
 		}
 
@@ -95,7 +95,7 @@ func List(ctx *context.Context) {
 			workflow := Workflow{Entry: *entry}
 			content, err := actions.GetContentFromEntry(entry)
 			if err != nil {
-				ctx.Error(http.StatusInternalServerError, err.Error())
+				ctx.ServerError("GetContentFromEntry", err)
 				return
 			}
 			wf, err := model.ReadWorkflow(bytes.NewReader(content))
@@ -172,7 +172,7 @@ func List(ctx *context.Context) {
 
 	runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("FindAndCount", err)
 		return
 	}
 
@@ -181,7 +181,7 @@ func List(ctx *context.Context) {
 	}
 
 	if err := actions_model.RunList(runs).LoadTriggerUser(ctx); err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("LoadTriggerUser", err)
 		return
 	}
 
@@ -189,7 +189,7 @@ func List(ctx *context.Context) {
 
 	actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("GetActors", err)
 		return
 	}
 	ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx.Doer, actors)

From a24e1da7e9e38fc5f5c84c083d122c0cc3da4b74 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 9 Feb 2024 11:02:53 +0800
Subject: [PATCH 009/679] Refactor parseSignatureFromCommitLine (#29054)

Replace #28849. Thanks to @yp05327 for the looking into the problem.
Fix #28840

The old behavior of newSignatureFromCommitline is not right. The new
parseSignatureFromCommitLine:

1. never fails
2. only accept one format (if there is any other, it could be easily added)

And add some tests.
---
 modules/git/repo.go              |  2 +-
 modules/git/repo_tag.go          |  6 +--
 modules/git/repo_tag_test.go     | 17 ++-----
 modules/git/signature.go         | 45 +++++++++++++++--
 modules/git/signature_gogit.go   | 44 -----------------
 modules/git/signature_nogogit.go | 84 +++-----------------------------
 modules/git/signature_test.go    | 47 ++++++++++++++++++
 modules/git/tag.go               |  8 ++-
 8 files changed, 104 insertions(+), 149 deletions(-)
 create mode 100644 modules/git/signature_test.go

diff --git a/modules/git/repo.go b/modules/git/repo.go
index db99d285a8..60078f3273 100644
--- a/modules/git/repo.go
+++ b/modules/git/repo.go
@@ -271,7 +271,7 @@ func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error
 		return time.Time{}, err
 	}
 	commitTime := strings.TrimSpace(stdout)
-	return time.Parse(GitTimeLayout, commitTime)
+	return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)
 }
 
 // DivergeObject represents commit count diverging commits
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index af9a75b29c..ae5dbd171f 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -183,11 +183,7 @@ func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, er
 		}
 	}
 
-	tag.Tagger, err = newSignatureFromCommitline([]byte(ref["creator"]))
-	if err != nil {
-		return nil, fmt.Errorf("parse tagger: %w", err)
-	}
-
+	tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
 	tag.Message = ref["contents"]
 	// strip PGP signature if present in contents field
 	pgpStart := strings.Index(tag.Message, beginpgp)
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
index da7b1455a8..9816e311a8 100644
--- a/modules/git/repo_tag_test.go
+++ b/modules/git/repo_tag_test.go
@@ -227,7 +227,7 @@ func TestRepository_parseTagRef(t *testing.T) {
 				ID:        MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
 				Object:    MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
 				Type:      "commit",
-				Tagger:    parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"),
+				Tagger:    parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
 				Message:   "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n",
 				Signature: nil,
 			},
@@ -256,7 +256,7 @@ func TestRepository_parseTagRef(t *testing.T) {
 				ID:        MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
 				Object:    MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
 				Type:      "tag",
-				Tagger:    parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"),
+				Tagger:    parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
 				Message:   "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n",
 				Signature: nil,
 			},
@@ -314,7 +314,7 @@ qbHDASXl
 				ID:      MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
 				Object:  MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
 				Type:    "tag",
-				Tagger:  parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"),
+				Tagger:  parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
 				Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
 				Signature: &CommitGPGSignature{
 					Signature: `-----BEGIN PGP SIGNATURE-----
@@ -363,14 +363,3 @@ Add changelog of v1.9.1 (#7859)
 		})
 	}
 }
-
-func parseAuthorLine(t *testing.T, committer string) *Signature {
-	t.Helper()
-
-	sig, err := newSignatureFromCommitline([]byte(committer))
-	if err != nil {
-		t.Fatalf("parse author line '%s': %v", committer, err)
-	}
-
-	return sig
-}
diff --git a/modules/git/signature.go b/modules/git/signature.go
index b5b17f23b0..f50a097758 100644
--- a/modules/git/signature.go
+++ b/modules/git/signature.go
@@ -4,7 +4,46 @@
 
 package git
 
-const (
-	// GitTimeLayout is the (default) time layout used by git.
-	GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
+import (
+	"strconv"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/modules/log"
 )
+
+// Helper to get a signature from the commit line, which looks like:
+//
+//	full name <user@example.com> 1378823654 +0200
+//
+// Haven't found the official reference for the standard format yet.
+// This function never fails, if the "line" can't be parsed, it returns a default Signature with "zero" time.
+func parseSignatureFromCommitLine(line string) *Signature {
+	sig := &Signature{}
+	s1, sx, ok1 := strings.Cut(line, " <")
+	s2, s3, ok2 := strings.Cut(sx, "> ")
+	if !ok1 || !ok2 {
+		sig.Name = line
+		return sig
+	}
+	sig.Name, sig.Email = s1, s2
+
+	if strings.Count(s3, " ") == 1 {
+		ts, tz, _ := strings.Cut(s3, " ")
+		seconds, _ := strconv.ParseInt(ts, 10, 64)
+		if tzTime, err := time.Parse("-0700", tz); err == nil {
+			sig.When = time.Unix(seconds, 0).In(tzTime.Location())
+		}
+	} else {
+		// the old gitea code tried to parse the date in a few different formats, but it's not clear why.
+		// according to public document, only the standard format "timestamp timezone" could be found, so drop other formats.
+		log.Error("suspicious commit line format: %q", line)
+		for _, fmt := range []string{ /*"Mon Jan _2 15:04:05 2006 -0700"*/ } {
+			if t, err := time.Parse(fmt, s3); err == nil {
+				sig.When = t
+				break
+			}
+		}
+	}
+	return sig
+}
diff --git a/modules/git/signature_gogit.go b/modules/git/signature_gogit.go
index c984ad6e20..1fc6aabceb 100644
--- a/modules/git/signature_gogit.go
+++ b/modules/git/signature_gogit.go
@@ -7,52 +7,8 @@
 package git
 
 import (
-	"bytes"
-	"strconv"
-	"strings"
-	"time"
-
 	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
 // Signature represents the Author or Committer information.
 type Signature = object.Signature
-
-// Helper to get a signature from the commit line, which looks like these:
-//
-//	author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
-//	author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
-//
-// but without the "author " at the beginning (this method should)
-// be used for author and committer.
-//
-// FIXME: include timezone for timestamp!
-func newSignatureFromCommitline(line []byte) (_ *Signature, err error) {
-	sig := new(Signature)
-	emailStart := bytes.IndexByte(line, '<')
-	if emailStart > 0 { // Empty name has already occurred, even if it shouldn't
-		sig.Name = strings.TrimSpace(string(line[:emailStart-1]))
-	}
-	emailEnd := bytes.IndexByte(line, '>')
-	sig.Email = string(line[emailStart+1 : emailEnd])
-
-	// Check date format.
-	if len(line) > emailEnd+2 {
-		firstChar := line[emailEnd+2]
-		if firstChar >= 48 && firstChar <= 57 {
-			timestop := bytes.IndexByte(line[emailEnd+2:], ' ')
-			timestring := string(line[emailEnd+2 : emailEnd+2+timestop])
-			seconds, _ := strconv.ParseInt(timestring, 10, 64)
-			sig.When = time.Unix(seconds, 0)
-		} else {
-			sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
-			if err != nil {
-				return nil, err
-			}
-		}
-	} else {
-		// Fall back to unix 0 time
-		sig.When = time.Unix(0, 0)
-	}
-	return sig, nil
-}
diff --git a/modules/git/signature_nogogit.go b/modules/git/signature_nogogit.go
index 25277f99d5..0d19c0abdc 100644
--- a/modules/git/signature_nogogit.go
+++ b/modules/git/signature_nogogit.go
@@ -7,21 +7,17 @@
 package git
 
 import (
-	"bytes"
 	"fmt"
-	"strconv"
-	"strings"
 	"time"
+
+	"code.gitea.io/gitea/modules/util"
 )
 
-// Signature represents the Author or Committer information.
+// Signature represents the Author, Committer or Tagger information.
 type Signature struct {
-	// Name represents a person name. It is an arbitrary string.
-	Name string
-	// Email is an email, but it cannot be assumed to be well-formed.
-	Email string
-	// When is the timestamp of the signature.
-	When time.Time
+	Name  string    // the committer name, it can be anything
+	Email string    // the committer email, it can be anything
+	When  time.Time // the timestamp of the signature
 }
 
 func (s *Signature) String() string {
@@ -30,71 +26,5 @@ func (s *Signature) String() string {
 
 // Decode decodes a byte array representing a signature to signature
 func (s *Signature) Decode(b []byte) {
-	sig, _ := newSignatureFromCommitline(b)
-	s.Email = sig.Email
-	s.Name = sig.Name
-	s.When = sig.When
-}
-
-// Helper to get a signature from the commit line, which looks like these:
-//
-//	author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
-//	author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
-//
-// but without the "author " at the beginning (this method should)
-// be used for author and committer.
-// FIXME: there are a lot of "return sig, err" (but the err is also nil), that's the old behavior, to avoid breaking
-func newSignatureFromCommitline(line []byte) (sig *Signature, err error) {
-	sig = new(Signature)
-	emailStart := bytes.LastIndexByte(line, '<')
-	emailEnd := bytes.LastIndexByte(line, '>')
-	if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
-		return sig, err
-	}
-
-	if emailStart > 0 { // Empty name has already occurred, even if it shouldn't
-		sig.Name = strings.TrimSpace(string(line[:emailStart-1]))
-	}
-	sig.Email = string(line[emailStart+1 : emailEnd])
-
-	hasTime := emailEnd+2 < len(line)
-	if !hasTime {
-		return sig, err
-	}
-
-	// Check date format.
-	firstChar := line[emailEnd+2]
-	if firstChar >= 48 && firstChar <= 57 {
-		idx := bytes.IndexByte(line[emailEnd+2:], ' ')
-		if idx < 0 {
-			return sig, err
-		}
-
-		timestring := string(line[emailEnd+2 : emailEnd+2+idx])
-		seconds, _ := strconv.ParseInt(timestring, 10, 64)
-		sig.When = time.Unix(seconds, 0)
-
-		idx += emailEnd + 3
-		if idx >= len(line) || idx+5 > len(line) {
-			return sig, err
-		}
-
-		timezone := string(line[idx : idx+5])
-		tzhours, err1 := strconv.ParseInt(timezone[0:3], 10, 64)
-		tzmins, err2 := strconv.ParseInt(timezone[3:], 10, 64)
-		if err1 != nil || err2 != nil {
-			return sig, err
-		}
-		if tzhours < 0 {
-			tzmins *= -1
-		}
-		tz := time.FixedZone("", int(tzhours*60*60+tzmins*60))
-		sig.When = sig.When.In(tz)
-	} else {
-		sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
-		if err != nil {
-			return sig, err
-		}
-	}
-	return sig, err
+	*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
 }
diff --git a/modules/git/signature_test.go b/modules/git/signature_test.go
new file mode 100644
index 0000000000..92681feea9
--- /dev/null
+++ b/modules/git/signature_test.go
@@ -0,0 +1,47 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseSignatureFromCommitLine(t *testing.T) {
+	tests := []struct {
+		line string
+		want *Signature
+	}{
+		{
+			line: "a b <c@d.com> 12345 +0100",
+			want: &Signature{
+				Name:  "a b",
+				Email: "c@d.com",
+				When:  time.Unix(12345, 0).In(time.FixedZone("", 3600)),
+			},
+		},
+		{
+			line: "bad line",
+			want: &Signature{Name: "bad line"},
+		},
+		{
+			line: "bad < line",
+			want: &Signature{Name: "bad < line"},
+		},
+		{
+			line: "bad > line",
+			want: &Signature{Name: "bad > line"},
+		},
+		{
+			line: "bad-line <name@example.com>",
+			want: &Signature{Name: "bad-line <name@example.com>"},
+		},
+	}
+	for _, test := range tests {
+		got := parseSignatureFromCommitLine(test.line)
+		assert.EqualValues(t, test.want, got)
+	}
+}
diff --git a/modules/git/tag.go b/modules/git/tag.go
index 01a8d6f6a5..94e5cd7c63 100644
--- a/modules/git/tag.go
+++ b/modules/git/tag.go
@@ -7,6 +7,8 @@ import (
 	"bytes"
 	"sort"
 	"strings"
+
+	"code.gitea.io/gitea/modules/util"
 )
 
 const (
@@ -59,11 +61,7 @@ l:
 				// A commit can have one or more parents
 				tag.Type = string(line[spacepos+1:])
 			case "tagger":
-				sig, err := newSignatureFromCommitline(line[spacepos+1:])
-				if err != nil {
-					return nil, err
-				}
-				tag.Tagger = sig
+				tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(line[spacepos+1:]))
 			}
 			nextline += eol + 1
 		case eol == 0:

From 9c39f8515fa88d644736c6773d7a05d070a02e82 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 9 Feb 2024 04:59:39 +0100
Subject: [PATCH 010/679] Rework spellchecking, add `lint-spell` (#29106)

- Use maintained fork https://github.com/golangci/misspell
- Rename `mispell-check` to `lint-spell`, add `lint-spell-fix`
- Run `lint-spell` in separate actions step
- Lint more files, fix discovered issues
- Remove inaccurate and outdated info in docs (we do not need GOPATH for
tools anymore)

Maybe later we can add more spellchecking tools, but I have not found
any good ones yet.
---
 .github/workflows/pull-compliance.yml         | 12 ++++++++++
 Makefile                                      | 22 +++++++++++++------
 .../config-cheat-sheet.zh-cn.md               |  2 +-
 .../development/hacking-on-gitea.en-us.md     |  4 ++--
 .../development/hacking-on-gitea.zh-cn.md     |  4 ++--
 .../content/installation/from-source.en-us.md |  8 +------
 .../content/installation/from-source.zh-cn.md |  4 +---
 templates/repo/branch_dropdown.tmpl           |  2 +-
 .../repo/issue/view_content/comments.tmpl     |  2 +-
 9 files changed, 36 insertions(+), 24 deletions(-)

diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 0472d9a9f0..391137f015 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -64,6 +64,18 @@ jobs:
       - run: make deps-frontend
       - run: make lint-swagger
 
+  lint-spell:
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
+    needs: files-changed
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+          check-latest: true
+      - run: make lint-spell
+
   lint-go-windows:
     if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
     needs: files-changed
diff --git a/Makefile b/Makefile
index 273ae1fa68..06fe70f16f 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,7 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che
 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
-MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4
+MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1
 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5
 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0
@@ -146,6 +146,8 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN
 GO_DIRS := build cmd models modules routers services tests
 WEB_DIRS := web_src/js web_src/css
 
+SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
+
 GO_SOURCES := $(wildcard *.go)
 GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
 GO_SOURCES += $(GENERATED_GO_DEST)
@@ -219,6 +221,8 @@ help:
 	@echo " - lint-swagger                     lint swagger files"
 	@echo " - lint-templates                   lint template files"
 	@echo " - lint-yaml                        lint yaml files"
+	@echo " - lint-spell                       lint spelling"
+	@echo " - lint-spell-fix                   lint spelling and fix issues"
 	@echo " - checks                           run various consistency checks"
 	@echo " - checks-frontend                  check frontend files"
 	@echo " - checks-backend                   check backend files"
@@ -308,10 +312,6 @@ fmt-check: fmt
 	  exit 1; \
 	fi
 
-.PHONY: misspell-check
-misspell-check:
-	go run $(MISSPELL_PACKAGE) -error $(GO_DIRS) $(WEB_DIRS)
-
 .PHONY: $(TAGS_EVIDENCE)
 $(TAGS_EVIDENCE):
 	@mkdir -p $(MAKE_EVIDENCE_DIR)
@@ -351,10 +351,10 @@ checks: checks-frontend checks-backend
 checks-frontend: lockfile-check svg-check
 
 .PHONY: checks-backend
-checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate security-check
+checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check
 
 .PHONY: lint
-lint: lint-frontend lint-backend
+lint: lint-frontend lint-backend lint-spell
 
 .PHONY: lint-fix
 lint-fix: lint-frontend-fix lint-backend-fix
@@ -395,6 +395,14 @@ lint-swagger: node_modules
 lint-md: node_modules
 	npx markdownlint docs *.md
 
+.PHONY: lint-spell
+lint-spell:
+	@go run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES)
+
+.PHONY: lint-spell-fix
+lint-spell-fix:
+	@go run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES)
+
 .PHONY: lint-go
 lint-go:
 	$(GO) run $(GOLANGCI_LINT_PACKAGE) run
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 2cee70daab..8236852ad3 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -29,7 +29,7 @@ menu:
 [ini](https://github.com/go-ini/ini/#recursive-values) 这里的说明。
 标注了 :exclamation: 的配置项表明除非你真的理解这个配置项的意义,否则最好使用默认值。
 
-在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`enviroment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。
+在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`environment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。
 
 包含`#`或者`;`的变量必须使用引号(`` ` ``或者`""""`)包裹,否则会被解析为注释。
 
diff --git a/docs/content/development/hacking-on-gitea.en-us.md b/docs/content/development/hacking-on-gitea.en-us.md
index 4b132c49d9..df8a9047d6 100644
--- a/docs/content/development/hacking-on-gitea.en-us.md
+++ b/docs/content/development/hacking-on-gitea.en-us.md
@@ -243,10 +243,10 @@ documentation using:
 make generate-swagger
 ```
 
-You should validate your generated Swagger file and spell-check it with:
+You should validate your generated Swagger file:
 
 ```bash
-make swagger-validate misspell-check
+make swagger-validate
 ```
 
 You should commit the changed swagger JSON file. The continuous integration
diff --git a/docs/content/development/hacking-on-gitea.zh-cn.md b/docs/content/development/hacking-on-gitea.zh-cn.md
index 364bbf1ffe..2dba3c92b6 100644
--- a/docs/content/development/hacking-on-gitea.zh-cn.md
+++ b/docs/content/development/hacking-on-gitea.zh-cn.md
@@ -228,10 +228,10 @@ Gitea Logo的 PNG 和 SVG 版本是使用 `TAGS="gitea" make generate-images` 
 make generate-swagger
 ```
 
-您应该验证生成的 Swagger 文件并使用以下命令对其进行拼写检查:
+您应该验证生成的 Swagger 文件:
 
 ```bash
-make swagger-validate misspell-check
+make swagger-validate
 ```
 
 您应该提交更改后的 swagger JSON 文件。持续集成服务器将使用以下方法检查是否已完成:
diff --git a/docs/content/installation/from-source.en-us.md b/docs/content/installation/from-source.en-us.md
index 601e074745..cd9fd56511 100644
--- a/docs/content/installation/from-source.en-us.md
+++ b/docs/content/installation/from-source.en-us.md
@@ -27,13 +27,7 @@ Next, [install Node.js with npm](https://nodejs.org/en/download/) which is
 required to build the JavaScript and CSS files. The minimum supported Node.js
 version is @minNodeVersion@ and the latest LTS version is recommended.
 
-**Note**: When executing make tasks that require external tools, like
-`make misspell-check`, Gitea will automatically download and build these as
-necessary. To be able to use these, you must have the `"$GOPATH/bin"` directory
-on the executable path. If you don't add the go bin directory to the
-executable path, you will have to manage this yourself.
-
-**Note 2**: Go version @minGoVersion@ or higher is required. However, it is recommended to
+**Note**: Go version @minGoVersion@ or higher is required. However, it is recommended to
 obtain the same version as our continuous integration, see the advice given in
 [Hacking on Gitea](development/hacking-on-gitea.md)
 
diff --git a/docs/content/installation/from-source.zh-cn.md b/docs/content/installation/from-source.zh-cn.md
index c2bd5785b2..3ff7efb4ed 100644
--- a/docs/content/installation/from-source.zh-cn.md
+++ b/docs/content/installation/from-source.zh-cn.md
@@ -21,9 +21,7 @@ menu:
 
 接下来,[安装 Node.js 和 npm](https://nodejs.org/zh-cn/download/), 这是构建 JavaScript 和 CSS 文件所需的。最低支持的 Node.js 版本是 @minNodeVersion@,建议使用最新的 LTS 版本。
 
-**注意**:当执行需要外部工具的 make 任务(如`make misspell-check`)时,Gitea 将根据需要自动下载和构建这些工具。为了能够实现这个目的,你必须将`"$GOPATH/bin"`目录添加到可执行路径中。如果没有将 Go 的二进制目录添加到可执行路径中,你需要自行解决产生的问题。
-
-**注意2**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。
+**注意**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。
 
 ## 下载
 
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index bee5363296..8a5cdc7cc7 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -1,7 +1,7 @@
 {{/* Attributes:
 * root
 * ContainerClasses
-* (TODO: search "branch_dropdown" in the template direcotry)
+* (TODO: search "branch_dropdown" in the template directory)
 */}}
 {{$defaultSelectedRefName := $.root.BranchName}}
 {{if and .root.IsViewTag (not .noTag)}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index ade0ea34cf..3cb7f7d0cf 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -367,7 +367,7 @@
 				<div class="timeline-item event">
 					{{if .OriginalAuthor}}
 					{{else}}
-					{{/* Some timeline avatars need a offset to correctly allign with their speech
+					{{/* Some timeline avatars need a offset to correctly align with their speech
 							bubble. The condition depends on review type and for positive reviews whether
 							there is a comment element or not */}}
 					<a class="timeline-avatar{{if or (and (eq .Review.Type 1) (or .Content .Attachments)) (and (eq .Review.Type 2) (or .Content .Attachments)) (eq .Review.Type 3)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>

From c7a21cbb0c5f8302495fa24baf218dc3462de2c5 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 9 Feb 2024 11:57:09 +0100
Subject: [PATCH 011/679] add lint-spell-fix to lint-fix (#29111)

Followup to https://github.com/go-gitea/gitea/pull/29106, fix this
oversight.
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 06fe70f16f..366ca6c624 100644
--- a/Makefile
+++ b/Makefile
@@ -357,7 +357,7 @@ checks-backend: tidy-check swagger-check fmt-check swagger-validate security-che
 lint: lint-frontend lint-backend lint-spell
 
 .PHONY: lint-fix
-lint-fix: lint-frontend-fix lint-backend-fix
+lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix
 
 .PHONY: lint-frontend
 lint-frontend: lint-js lint-css

From 92fda9c5a2cf6b57063050d1d0948ae885257d4a Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 9 Feb 2024 22:06:03 +0800
Subject: [PATCH 012/679] Disallow duplicate storage paths (#26484)

Replace #26380
---
 modules/setting/indexer.go    | 31 +++++++++++++++++--------------
 modules/setting/path.go       |  4 ++++
 modules/setting/repository.go |  3 +++
 modules/setting/server.go     |  7 ++++---
 modules/setting/session.go    |  6 +++---
 modules/setting/setting.go    |  9 +++++++++
 modules/setting/storage.go    |  2 ++
 7 files changed, 42 insertions(+), 20 deletions(-)

diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index 16f3d80168..15f6150242 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -53,21 +53,24 @@ var Indexer = struct {
 func loadIndexerFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("indexer")
 	Indexer.IssueType = sec.Key("ISSUE_INDEXER_TYPE").MustString("bleve")
-	Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
-	if !filepath.IsAbs(Indexer.IssuePath) {
-		Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
-	}
-	Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
-
-	if Indexer.IssueType == "meilisearch" {
-		u, err := url.Parse(Indexer.IssueConnStr)
-		if err != nil {
-			log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
-			u = &url.URL{}
+	if Indexer.IssueType == "bleve" {
+		Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
+		if !filepath.IsAbs(Indexer.IssuePath) {
+			Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
+		}
+		fatalDuplicatedPath("issue_indexer", Indexer.IssuePath)
+	} else {
+		Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
+		if Indexer.IssueType == "meilisearch" {
+			u, err := url.Parse(Indexer.IssueConnStr)
+			if err != nil {
+				log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
+				u = &url.URL{}
+			}
+			Indexer.IssueConnAuth, _ = u.User.Password()
+			u.User = nil
+			Indexer.IssueConnStr = u.String()
 		}
-		Indexer.IssueConnAuth, _ = u.User.Password()
-		u.User = nil
-		Indexer.IssueConnStr = u.String()
 	}
 
 	Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName)
diff --git a/modules/setting/path.go b/modules/setting/path.go
index 0fdc305aa1..b2cca0acbf 100644
--- a/modules/setting/path.go
+++ b/modules/setting/path.go
@@ -66,8 +66,12 @@ func init() {
 		AppWorkPath = filepath.Dir(AppPath)
 	}
 
+	fatalDuplicatedPath("app_work_path", AppWorkPath)
+
 	appWorkPathBuiltin = AppWorkPath
 	customPathBuiltin = CustomPath
+
+	fatalDuplicatedPath("custom_path", CustomPath)
 	customConfBuiltin = CustomConf
 }
 
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index a6f0ed8833..7990021aaa 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -285,6 +285,9 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
 	} else {
 		RepoRootPath = filepath.Clean(RepoRootPath)
 	}
+
+	fatalDuplicatedPath("repository.ROOT", RepoRootPath)
+
 	defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
 	for _, charset := range Repository.DetectedCharsetsOrder {
 		defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset)))
diff --git a/modules/setting/server.go b/modules/setting/server.go
index c09b91612a..0dea4e1ac7 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -7,7 +7,6 @@ import (
 	"encoding/base64"
 	"net"
 	"net/url"
-	"path"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -321,17 +320,19 @@ func loadServerFrom(rootCfg ConfigProvider) {
 	}
 	StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath)
 	StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour)
-	AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data"))
+	AppDataPath = sec.Key("APP_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data"))
 	if !filepath.IsAbs(AppDataPath) {
 		AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
 	}
+	fatalDuplicatedPath("app_data_path", AppDataPath)
 
 	EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
 	EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
-	PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(path.Join(AppWorkPath, "data/tmp/pprof"))
+	PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
 	if !filepath.IsAbs(PprofDataPath) {
 		PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
 	}
+	fatalDuplicatedPath("pprof_data_path", PprofDataPath)
 
 	landingPage := sec.Key("LANDING_PAGE").MustString("home")
 	switch landingPage {
diff --git a/modules/setting/session.go b/modules/setting/session.go
index 664c66f869..8b9b754b38 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -5,7 +5,6 @@ package setting
 
 import (
 	"net/http"
-	"path"
 	"path/filepath"
 	"strings"
 
@@ -44,9 +43,10 @@ func loadSessionFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("session")
 	SessionConfig.Provider = sec.Key("PROVIDER").In("memory",
 		[]string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
-	SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(path.Join(AppDataPath, "sessions")), "\" ")
+	SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
 	if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
-		SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig)
+		SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
+		fatalDuplicatedPath("session", SessionConfig.ProviderConfig)
 	}
 	SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
 	SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 72aee2a092..6e7ce7e67f 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -226,3 +226,12 @@ func LoadSettingsForInstall() {
 	loadServiceFrom(CfgProvider)
 	loadMailerFrom(CfgProvider)
 }
+
+var uniquePaths = make(map[string]string)
+
+func fatalDuplicatedPath(name, p string) {
+	if targetName, ok := uniquePaths[p]; ok && targetName != name {
+		log.Fatal("storage path %q is being used by %q and %q and all storage paths must be unique to prevent data loss.", p, targetName, name)
+	}
+	uniquePaths[p] = name
+}
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index f937c7cff3..23b08df101 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -240,6 +240,8 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
 		}
 	}
 
+	fatalDuplicatedPath("storage."+name, storage.Path)
+
 	return &storage, nil
 }
 

From c1f7249056d4aa38927aebcbddc6459ee714c801 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Sat, 10 Feb 2024 00:22:56 +0000
Subject: [PATCH 013/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_el-GR.ini | 154 +++++++++++++++++++++++++++++++-
 1 file changed, 153 insertions(+), 1 deletion(-)

diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 2424ee3fb6..24bcd7244c 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -4,6 +4,7 @@ explore=Εξερεύνηση
 help=Βοήθεια
 logo=Λογότυπο
 sign_in=Είσοδος
+sign_in_with_provider=Είσοδος με %s
 sign_in_or=ή
 sign_out=Έξοδος
 sign_up=Εγγραφή
@@ -16,6 +17,7 @@ template=Πρότυπο
 language=Γλώσσα
 notifications=Ειδοποιήσεις
 active_stopwatch=Ενεργή Καταγραφή Χρόνου
+tracked_time_summary=Περίληψη του χρόνου παρακολούθησης με βάση τα φίλτρα της λίστας ζητημάτων
 create_new=Δημιουργία…
 user_profile_and_more=Προφίλ και ρυθμίσεις…
 signed_in_as=Είσοδος ως
@@ -79,6 +81,7 @@ milestones=Ορόσημα
 
 ok=OK
 cancel=Ακύρωση
+retry=Επανάληψη
 rerun=Επανεκτέλεση
 rerun_all=Επανεκτέλεση όλων
 save=Αποθήκευση
@@ -88,12 +91,15 @@ remove=Αφαίρεση
 remove_all=Αφαίρεση Όλων
 remove_label_str=`Αφαίρεση του αντικειμένου "%s"`
 edit=Επεξεργασία
+view=Προβολή
 
 enabled=Ενεργοποιημένο
 disabled=Απενεργοποιημένο
+locked=Κλειδωμένο
 
 copy=Αντιγραφή
 copy_url=Αντιγραφή URL
+copy_hash=Αντιγραφή hash
 copy_content=Αντιγραφή περιεχομένου
 copy_branch=Αντιγραφή ονόματος κλάδου
 copy_success=Αντιγράφηκε!
@@ -106,6 +112,7 @@ loading=Φόρτωση…
 
 error=Σφάλμα
 error404=Η σελίδα που προσπαθείτε να φτάσετε είτε <strong>δεν υπάρχει</strong> είτε <strong>δεν είστε εξουσιοδοτημένοι</strong> για να την δείτε.
+go_back=Επιστροφή
 
 never=Ποτέ
 unknown=Άγνωστη
@@ -127,7 +134,9 @@ concept_user_organization=Οργανισμός
 show_timestamps=Εμφάνιση χρονοσημάνσεων
 show_log_seconds=Εμφάνιση δευτερολέπτων
 show_full_screen=Εμφάνιση πλήρους οθόνης
+download_logs=Λήψη καταγραφών
 
+confirm_delete_selected=Επιβεβαιώνετε τη διαγραφή όλων των επιλεγμένων στοιχείων;
 
 name=Όνομα
 value=Τιμή
@@ -166,6 +175,7 @@ string.desc=Z - A
 
 [error]
 occurred=Παρουσιάστηκε ένα σφάλμα
+report_message=Αν πιστεύετε ότι αυτό είναι ένα πρόβλημα στο Gitea, παρακαλούμε αναζητήστε ζητήματα στο <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> ή ανοίξτε ένα νέο ζήτημα εάν είναι απαραίτητο.
 missing_csrf=Bad Request: δεν υπάρχει διακριτικό CSRF
 invalid_csrf=Λάθος Αίτημα: μη έγκυρο διακριτικό CSRF
 not_found=Ο προορισμός δεν βρέθηκε.
@@ -174,6 +184,7 @@ network_error=Σφάλμα δικτύου
 [startpage]
 app_desc=Μια ανώδυνη, αυτο-φιλοξενούμενη υπηρεσία Git
 install=Εύκολο στην εγκατάσταση
+install_desc=Απλά <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">εκτελέστε το αρχείο προγράμματος</a> για την πλατφόρμα σας, χρήσιμοποιήστε το με το <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, ή εγκαταστήστε το <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">πακέτο</a>.
 platform=Πολυπλατφορμικό
 platform_desc=Ο Gitea τρέχει οπουδήποτε <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> μπορεί να γίνει compile για: Windows, macOS, Linux, ARM, κλπ. Επιλέξτε αυτό που αγαπάτε!
 lightweight=Ελαφρύ
@@ -218,6 +229,7 @@ repo_path_helper=Τα απομακρυσμένα αποθετήρια Git θα 
 lfs_path=Ριζική Διαδρομή Git LFS
 lfs_path_helper=Τα αρχεία που παρακολουθούνται από το Git LFS θα αποθηκεύονται σε αυτόν τον φάκελο. Αφήστε κενό για να το απενεργοποιήσετε.
 run_user=Εκτέλεση Σαν Χρήστη
+run_user_helper=Το όνομα του χρήστη του λειτουργικού συστήματος ο οποίος εκτελεί το Gitea. Επισημαίνεται ότι αυτός ο χρήστης πρέπει να έχει πρόσβαση στο ριζικό φάκελο του αποθετηρίου.
 domain=Domain Διακομιστή
 domain_helper=Όνομα domain διακομιστή ή η διεύθυνση του.
 ssh_port=Θύρα της υπηρεσίας SSH
@@ -290,6 +302,7 @@ password_algorithm_helper=Ορίστε τον αλγόριθμο κατακερ
 enable_update_checker=Ενεργοποίηση Ελεγκτή Ενημερώσεων
 enable_update_checker_helper=Ελέγχει περιοδικά για νέες εκδόσεις κάνοντας σύνδεση στο gitea.io.
 env_config_keys=Ρυθμίσεις Περιβάλλοντος
+env_config_keys_prompt=Οι ακόλουθες μεταβλητές περιβάλλοντος θα εφαρμοστούν επίσης στο αρχείο ρυθμίσεων σας:
 
 [home]
 uname_holder=Όνομα Χρήστη ή Διεύθυνση Email
@@ -348,9 +361,11 @@ disable_register_prompt=Η εγγραφή είναι απενεργοποιημ
 disable_register_mail=Η Επιβεβαίωση email για την εγγραφή είναι απενεργοποιημένη.
 manual_activation_only=Επικοινωνήστε με το διαχειριστή της υπηρεσίας για να ολοκληρώσετε την ενεργοποίηση.
 remember_me=Απομνημόνευση αυτής της συσκευής
+remember_me.compromised=Το διακριτικό σύνδεσης δεν είναι πλέον έγκυρο, αυτό ίσως υποδεικνύει έναν κλεμμένο λογαριασμό. Παρακαλώ ελέγξτε το λογαριασμό σας για ασυνήθιστες δραστηριότητες.
 forgot_password_title=Ξέχασα Τον Κωδικό Πρόσβασης
 forgot_password=Ξεχάσατε τον κωδικό πρόσβασης;
 sign_up_now=Χρειάζεστε λογαριασμό; Εγγραφείτε τώρα.
+sign_up_successful=Ο λογαριασμός δημιουργήθηκε επιτυχώς. Καλώς ορίσατε!
 confirmation_mail_sent_prompt=Ένα νέο email επιβεβαίωσης έχει σταλεί στο <b>%s</b>. Παρακαλώ ελέγξτε τα εισερχόμενα σας μέσα στις επόμενες %s για να ολοκληρώσετε τη διαδικασία εγγραφής.
 must_change_password=Ενημερώστε τον κωδικό πρόσβασης σας
 allow_password_change=Απαιτείται από το χρήστη να αλλάξει τον κωδικό πρόσβασης (συνιστόμενο)
@@ -358,6 +373,7 @@ reset_password_mail_sent_prompt=Ένα email επιβεβαίωσης έχει 
 active_your_account=Ενεργοποιήστε Το Λογαριασμό Σας
 account_activated=Ο λογαριασμός έχει ενεργοποιηθεί
 prohibit_login=Απαγορεύεται η Σύνδεση
+prohibit_login_desc=Ο λογαριασμός σας δεν επιτρέπεται να συνδεθεί, παρακαλούμε επικοινωνήστε με το διαχειριστή σας.
 resent_limit_prompt=Έχετε ήδη ζητήσει ένα email ενεργοποίησης πρόσφατα. Παρακαλώ περιμένετε 3 λεπτά και προσπαθήστε ξανά.
 has_unconfirmed_mail=Γεια σας %s, έχετε μια ανεπιβεβαίωτη διεύθυνση ηλεκτρονικού ταχυδρομείου (<b>%s</b>). Εάν δεν έχετε λάβει email επιβεβαίωσης ή χρειάζεται να αποστείλετε εκ νέου ένα νέο, παρακαλώ κάντε κλικ στο παρακάτω κουμπί.
 resend_mail=Κάντε κλικ εδώ για να στείλετε ξανά το email ενεργοποίησης
@@ -367,6 +383,7 @@ reset_password=Ανάκτηση Λογαριασμού
 invalid_code=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έχει λήξει.
 invalid_password=Ο κωδικός πρόσβασης σας δεν ταιριάζει με τον κωδικό που χρησιμοποιήθηκε για τη δημιουργία του λογαριασμού.
 reset_password_helper=Ανάκτηση Λογαριασμού
+reset_password_wrong_user=Έχετε συνδεθεί ως %s, αλλά ο σύνδεσμος ανάκτησης λογαριασμού προορίζεται για το %s
 password_too_short=Το μήκος του κωδικού πρόσβασης δεν μπορεί να είναι μικρότερο από %d χαρακτήρες.
 non_local_account=Οι μη τοπικοί χρήστες δεν μπορούν να ενημερώσουν τον κωδικό πρόσβασής τους μέσω του διεπαφής web του Gitea.
 verify=Επαλήθευση
@@ -391,6 +408,7 @@ openid_connect_title=Σύνδεση σε υπάρχων λογαριασμό
 openid_connect_desc=Το επιλεγμένο OpenID URI είναι άγνωστο. Συνδέστε το με ένα νέο λογαριασμό εδώ.
 openid_register_title=Δημιουργία νέου λογαριασμού
 openid_register_desc=Το επιλεγμένο OpenID URI είναι άγνωστο. Συνδέστε το με ένα νέο λογαριασμό εδώ.
+openid_signin_desc=Εισάγετε το OpenID URI σας. Για παράδειγμα: alice.openid.example.org ή https://openid.example.org/alice.
 disable_forgot_password_mail=Η ανάκτηση λογαριασμού είναι απενεργοποιημένη επειδή δεν έχει οριστεί email. Παρακαλούμε επικοινωνήστε με το διαχειριστή.
 disable_forgot_password_mail_admin=Η ανάκτηση λογαριασμού είναι διαθέσιμη μόνο όταν έχει οριστεί το email. Παρακαλούμε ορίστει το email σας για να ενεργοποιήσετε την ανάκτηση λογαριασμού.
 email_domain_blacklisted=Δεν μπορείτε να εγγραφείτε με τη διεύθυνση email σας.
@@ -400,6 +418,7 @@ authorize_application_created_by=Αυτή η εφαρμογή δημιουργή
 authorize_application_description=Εάν παραχωρήσετε την πρόσβαση, θα μπορεί να έχει πρόσβαση και να γράφει σε όλες τις πληροφορίες του λογαριασμού σας, συμπεριλαμβανομένων των ιδιωτικών αποθετηρίων και οργανισμών.
 authorize_title=Εξουσιοδότηση του "%s" για έχει πρόσβαση στο λογαριασμό σας;
 authorization_failed=Αποτυχία εξουσιοδότησης
+authorization_failed_desc=Η εξουσιοδότηση απέτυχε επειδή εντοπίστηκε μια μη έγκυρη αίτηση. Παρακαλούμε επικοινωνήστε με το συντηρητή της εφαρμογής που προσπαθήσατε να εξουσιοδοτήσετε.
 sspi_auth_failed=Αποτυχία ταυτοποίησης SSPI
 password_pwned_err=Δεν ήταν δυνατή η ολοκλήρωση του αιτήματος προς το HaveIBeenPwned
 
@@ -415,6 +434,7 @@ activate_account.text_1=Γεια σας <b>%[1]s</b>, ευχαριστούμε 
 activate_account.text_2=Παρακαλούμε κάντε κλικ στον παρακάτω σύνδεσμο για να ενεργοποιήσετε το λογαριασμό σας μέσα σε <b>%s</b>:
 
 activate_email=Επιβεβαιώστε τη διεύθυνση email σας
+activate_email.title=%s, παρακαλώ επαληθεύστε τη διεύθυνση email σας
 activate_email.text=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για να επαληθεύσετε τη διεύθυνση email σας στο <b>%s</b>:
 
 register_notify=Καλώς ήλθατε στο Gitea
@@ -584,6 +604,8 @@ user_bio=Βιογραφικό
 disabled_public_activity=Αυτός ο χρήστης έχει απενεργοποιήσει τη δημόσια προβολή της δραστηριότητας.
 email_visibility.limited=Η διεύθυνση email σας είναι ορατή σε όλους τους ταυτοποιημένους χρήστες
 email_visibility.private=Η διεύθυνση email σας είναι ορατή μόνο σε εσάς και στους διαχειριστές
+show_on_map=Εμφάνιση της τοποθεσίας στο χάρτη
+settings=Ρυθμίσεις Χρήστη
 
 form.name_reserved=Το όνομα χρήστη "%s" είναι δεσμευμένο.
 form.name_pattern_not_allowed=Το μοτίβο "%s" δεν επιτρέπεται μέσα σε ένα όνομα χρήστη.
@@ -605,9 +627,12 @@ delete=Διαγραφή Λογαριασμού
 twofa=Έλεγχος Ταυτότητας Δύο Παραγόντων
 account_link=Συνδεδεμένοι Λογαριασμοί
 organization=Οργανισμοί
+uid=UID
 webauthn=Κλειδιά Ασφαλείας
 
 public_profile=Δημόσιο Προφίλ
+biography_placeholder=Πείτε μας λίγο για τον εαυτό σας! (Μπορείτε να γράψετε με Markdown)
+location_placeholder=Μοιραστείτε την κατά προσέγγιση τοποθεσία σας με άλλους
 password_username_disabled=Οι μη τοπικοί χρήστες δεν επιτρέπεται να αλλάξουν το όνομα χρήστη τους. Επικοινωνήστε με το διαχειριστή σας για περισσότερες λεπτομέρειες.
 full_name=Πλήρες Όνομα
 website=Ιστοσελίδα
@@ -619,6 +644,7 @@ update_language_not_found=Η γλώσσα "%s" δεν είναι διαθέσι
 update_language_success=Η γλώσσα ενημερώθηκε.
 update_profile_success=Το προφίλ σας έχει ενημερωθεί.
 change_username=Το όνομα χρήστη σας έχει αλλάξει.
+change_username_redirect_prompt=Το παλιό όνομα χρήστη θα ανακατευθύνει μέχρι να ζητηθεί ξανά.
 continue=Συνέχεια
 cancel=Ακύρωση
 language=Γλώσσα
@@ -643,6 +669,7 @@ comment_type_group_project=Έργο
 comment_type_group_issue_ref=Αναφορά ζητήματος
 saved_successfully=Οι ρυθμίσεις σας αποθηκεύτηκαν επιτυχώς.
 privacy=Απόρρητο
+keep_activity_private=Απόκρυψη Δραστηριότητας από τη σελίδα προφίλ
 keep_activity_private_popup=Με αυτή την επιλογή η δραστηριότητα σας είναι ορατή μόνο σε εσάς και τους διαχειριστές
 
 lookup_avatar_by_mail=Αναζήτηση ενός Avatar με διεύθυνση email
@@ -652,12 +679,14 @@ choose_new_avatar=Επιλέξτε νέα εικόνα
 update_avatar=Ενημέρωση Εικόνας
 delete_current_avatar=Διαγραφή Τρέχουσας Εικόνας
 uploaded_avatar_not_a_image=Το αρχείο που ανεβάσατε δεν είναι εικόνα.
+uploaded_avatar_is_too_big=Το μέγεθος αρχείου που ανέβηκε (%d KiB) υπερβαίνει το μέγιστο μέγεθος (%d KiB).
 update_avatar_success=Η εικόνα σας έχει ενημερωθεί.
 update_user_avatar_success=Το avatar του χρήστη ενημερώθηκε.
 
 change_password=Ενημέρωση Κωδικού Πρόσβασης
 old_password=Τρέχων Κωδικός Πρόσβασης
 new_password=Νέος Κωδικός Πρόσβασης
+retype_new_password=Επιβεβαίωση Νέου Κωδικού Πρόσβασης
 password_incorrect=Ο τρέχων κωδικός πρόσβασης είναι λάθος.
 change_password_success=Ο κωδικός πρόσβασής σας έχει ενημερωθεί. Από εδώ και τώρα συνδέεστε χρησιμοποιώντας τον νέο κωδικό πρόσβασής σας.
 password_change_disabled=Οι μη τοπικοί χρήστες δεν μπορούν να ενημερώσουν τον κωδικό πρόσβασής τους μέσω του διεπαφής web του Gitea.
@@ -666,6 +695,7 @@ emails=Διευθύνσεις Email
 manage_emails=Διαχείριση Διευθύνσεων Email
 manage_themes=Επιλέξτε προεπιλεγμένο θέμα διεπαφής
 manage_openid=Διαχείριση Διευθύνσεων OpenID
+email_desc=Η κύρια διεύθυνση ηλεκτρονικού ταχυδρομείου σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση του κωδικού πρόσβασης και, εφόσον δεν είναι κρυμμένη, λειτουργίες Git στον ιστότοπο.
 theme_desc=Αυτό θα είναι το προεπιλεγμένο θέμα διεπαφής σας σε όλη την ιστοσελίδα.
 primary=Κύριο
 activated=Ενεργό
@@ -673,6 +703,7 @@ requires_activation=Απαιτείται ενεργοποίηση
 primary_email=Αλλαγή κυριότητας
 activate_email=Αποστολή Ενεργοποίησης
 activations_pending=Εκκρεμούν Ενεργοποιήσεις
+can_not_add_email_activations_pending=Εκκρεμεί μια ενεργοποίηση, δοκιμάστε ξανά σε λίγα λεπτά αν θέλετε να προσθέσετε ένα νέο email.
 delete_email=Αφαίρεση
 email_deletion=Αφαίρεση Διεύθυνσης Email
 email_deletion_desc=Η διεύθυνση ηλεκτρονικού ταχυδρομείου και οι σχετικές πληροφορίες θα αφαιρεθούν από τον λογαριασμό σας. Οι υποβολές Git από αυτή τη διεύθυνση email θα παραμείνουν αμετάβλητες. Συνέχεια;
@@ -790,6 +821,7 @@ permissions_access_all=Όλα (δημόσια, ιδιωτικά, και περι
 select_permissions=Επιλέξτε δικαιώματα
 permission_no_access=Καμία Πρόσβαση
 permission_read=Αναγνωσμένες
+permission_write=Ανάγνωση και Εγγραφή
 at_least_one_permission=Πρέπει να επιλέξετε τουλάχιστον ένα δικαίωμα για να δημιουργήσετε ένα διακριτικό
 permissions_list=Δικαιώματα:
 
@@ -816,6 +848,7 @@ authorized_oauth2_applications=Εξουσιοδοτημένες Εφαρμογέ
 revoke_key=Ανάκληση
 revoke_oauth2_grant=Ανάκληση Πρόσβασης
 revoke_oauth2_grant_description=Η ανάκληση πρόσβασης για αυτή την εξωτερική εφαρμογή θα αποτρέψει αυτή την εφαρμογή από την πρόσβαση στα δεδομένα σας. Σίγουρα;
+revoke_oauth2_grant_success=Η πρόσβαση ανακλήθηκε επιτυχώς.
 
 twofa_desc=Ο έλεγχος ταυτότητας δύο παραγόντων ενισχύει την ασφάλεια του λογαριασμού σας.
 twofa_is_enrolled=Ο λογαριασμός σας είναι <strong>εγγεγραμμένος</strong> σε έλεγχο ταυτότητας δύο παραγόντων.
@@ -850,6 +883,7 @@ remove_account_link_success=Ο συνδεδεμένος λογαριασμός 
 
 
 orgs_none=Δεν είστε μέλος σε κάποιο οργανισμό.
+repos_none=Δεν κατέχετε κάποιο αποθετήριο.
 
 delete_account=Διαγραφή Του Λογαριασμού Σας
 delete_prompt=Αυτή η ενέργεια θα διαγράψει μόνιμα το λογαριασμό σας. <strong>ΔΕΝ ΘΑ ΜΠΟΡΕΙ</strong> να επανέλθει.
@@ -882,6 +916,7 @@ template_helper=Μετατροπή σε πρότυπο αποθετήριο
 template_description=Τα πρότυπα αποθετήρια επιτρέπουν στους χρήστες να δημιουργήσουν νέα αποθετήρια με την ίδια δομή, αρχεία και προαιρετικές ρυθμίσεις.
 visibility=Ορατότητα
 visibility_description=Μόνο ο ιδιοκτήτης ή τα μέλη του οργανισμού εάν έχουν δικαιώματα, θα είναι σε θέση να το δουν.
+visibility_helper=Αλλάξτε το αποθετήριο σε ιδιωτικό
 visibility_helper_forced=Ο διαχειριστής σας αναγκάζει τα νέα αποθετήρια να είναι ιδιωτικά.
 visibility_fork_helper=(Αλλάζοντας αυτό θα επηρεάσει όλα τα forks.)
 clone_helper=Χρειάζεστε βοήθεια για τη κλωνοποίηση; Επισκεφθείτε τη <a target="_blank" rel="noopener noreferrer" href="%s">Βοήθεια</a>.
@@ -890,6 +925,8 @@ fork_from=Fork Από Το
 already_forked=Έχετε ήδη κάνει fork το %s
 fork_to_different_account=Fork σε διαφορετικό λογαριασμό
 fork_visibility_helper=Η ορατότητα ενός fork αποθετηρίου δεν μπορεί να αλλάξει.
+fork_branch=Κλάδος που θα κλωνοποιηθεί στο fork
+all_branches=Όλοι οι κλάδοι
 use_template=Χρήση αυτού του πρότυπου
 clone_in_vsc=Κλωνοποίηση στο VS Code
 download_zip=Λήψη ZIP
@@ -918,6 +955,7 @@ trust_model_helper_collaborator_committer=Συνεργάτης+Υποβολέα
 trust_model_helper_default=Προεπιλογή: Χρησιμοποιήστε το προεπιλεγμένο μοντέλο εμπιστοσύνης για αυτήν την εγκατάσταση
 create_repo=Δημιουργία Αποθετηρίου
 default_branch=Προεπιλεγμένος Κλάδος
+default_branch_label=προεπιλογή
 default_branch_helper=Ο προεπιλεγμένος κλάδος είναι ο βασικός κλάδος για pull requests και υποβολές κώδικα.
 mirror_prune=Καθαρισμός
 mirror_prune_desc=Αφαίρεση παρωχημένων αναφορών απομακρυσμένης-παρακολούθησης
@@ -1094,6 +1132,9 @@ file_view_rendered=Προβολή Απόδοσης
 file_view_raw=Προβολή Ακατέργαστου
 file_permalink=Permalink
 file_too_large=Το αρχείο είναι πολύ μεγάλο για να εμφανιστεί.
+invisible_runes_description=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode που δεν διακρίνονται από ανθρώπους, αλλά μπορεί να επεξεργάζονται διαφορετικά από έναν υπολογιστή. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
+ambiguous_runes_header=`Αυτό το αρχείο περιέχει ασαφείς χαρακτήρες Unicode `
+ambiguous_runes_description=`Αυτό το αρχείο περιέχει χαρακτήρες Unicode που μπορεί να συγχέονται με άλλους χαρακτήρες. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
 invisible_runes_line=`Αυτή η γραμμή έχει αόρατους χαρακτήρες unicode `
 ambiguous_runes_line=`Αυτή η γραμμή έχει ασαφείς χαρακτήρες unicode `
 ambiguous_character=`ο %[1]c [U+%04[1]X] μπορεί να μπερδευτεί με τον %[2]c [U+%04[2]X]`
@@ -1106,11 +1147,15 @@ video_not_supported_in_browser=Το πρόγραμμα περιήγησής σα
 audio_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 'audio'.
 stored_lfs=Αποθηκεύτηκε με το Git LFS
 symbolic_link=Symbolic link
+executable_file=Εκτελέσιμο Αρχείο
 commit_graph=Γράφημα Υποβολών
 commit_graph.select=Επιλογή κλάδων
 commit_graph.hide_pr_refs=Απόκρυψη Pull Requests
 commit_graph.monochrome=Μονόχρωμο
 commit_graph.color=Έγχρωμο
+commit.contained_in=Αυτή η υποβολή περιλαμβάνεται σε:
+commit.contained_in_default_branch=Αυτή η υποβολή είναι μέρος του προεπιλεγμένου κλάδου
+commit.load_referencing_branches_and_tags=Φόρτωση κλάδων και ετικετών που παραπέμπουν σε αυτήν την υποβολή
 blame=Ευθύνη
 download_file=Λήψη αρχείου
 normal_view=Κανονική Προβολή
@@ -1203,6 +1248,7 @@ commits.signed_by_untrusted_user=Υπογράφηκε από μη έμπιστο
 commits.signed_by_untrusted_user_unmatched=Υπογράφηκε από ένα μη έμπιστο χρήστη ο οποίος δεν ταιριάζει με τον υποβολέα
 commits.gpg_key_id=ID Κλειδιού GPG
 commits.ssh_key_fingerprint=Αποτύπωμα Κλειδιού SSH
+commits.view_path=Προβολή σε αυτή τη στιγμή στο ιστορικό
 
 commit.operations=Λειτουργίες
 commit.revert=Απόσυρση
@@ -1330,6 +1376,7 @@ issues.delete_branch_at=`διέγραψε το κλάδο <b>%s</b> %s`
 issues.filter_label=Σήμα
 issues.filter_label_exclude=`Χρησιμοποιήστε <code>alt</code> + <code>κάντε κλικ/Enter</code> για να εξαιρέσετε τις σημάνσεις`
 issues.filter_label_no_select=Όλα τα σήματα
+issues.filter_label_select_no_label=Χωρίς ετικέτα
 issues.filter_milestone=Ορόσημο
 issues.filter_milestone_all=Όλα τα ορόσημα
 issues.filter_milestone_none=Χωρίς ορόσημα
@@ -1380,9 +1427,10 @@ issues.opened_by_fake=άνοιξε το %[1]s από %[2]s
 issues.closed_by_fake=από %[2]s έκλεισαν %[1]s
 issues.previous=Προηγούμενο
 issues.next=Επόμενο
-issues.open_title=Ανοιχτά
+issues.open_title=Ανοικτό
 issues.closed_title=Κλειστά
 issues.draft_title=Προσχέδιο
+issues.num_comments_1=%d σχόλιο
 issues.num_comments=%d σχόλια
 issues.commented_at=`σχολίασε <a href="#%s">%s</a>`
 issues.delete_comment_confirm=Θέλετε σίγουρα να διαγράψετε αυτό το σχόλιο;
@@ -1391,6 +1439,7 @@ issues.context.quote_reply=Παράθεση Απάντησης
 issues.context.reference_issue=Αναφορά σε νέο ζήτημα
 issues.context.edit=Επεξεργασία
 issues.context.delete=Διαγραφή
+issues.no_content=Δεν υπάρχει περιγραφή.
 issues.close=Κλείσιμο Ζητήματος
 issues.comment_pull_merged_at=συγχώνευσε την υποβολή %[1]s στο %[2]s %[3]s
 issues.comment_manually_pull_merged_at=συγχώνευσε χειροκίνητα την υποβολή %[1]s στο %[2]s %[3]s
@@ -1409,8 +1458,17 @@ issues.ref_closed_from=`<a href="%[3]s">έκλεισε αυτό το ζήτημ
 issues.ref_reopened_from=`<a href="%[3]s">άνοιξε ξανά αυτό το ζήτημα %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`από %[1]s`
 issues.author=Συγγραφέας
+issues.author_helper=Αυτός ο χρήστης είναι ο συγγραφέας.
 issues.role.owner=Ιδιοκτήτης
+issues.role.owner_helper=Αυτός ο χρήστης είναι ο ιδιοκτήτης αυτού του αποθετηρίου.
 issues.role.member=Μέλος
+issues.role.member_helper=Αυτός ο χρήστης είναι μέλος του οργανισμού που κατέχει αυτό το αποθετήριο.
+issues.role.collaborator=Συνεργάτης
+issues.role.collaborator_helper=Αυτός ο χρήστης έχει προσκληθεί να συνεργαστεί στο αποθετήριο.
+issues.role.first_time_contributor=Συντελεστής για πρώτη φορά
+issues.role.first_time_contributor_helper=Αυτή είναι η πρώτη συνεισφορά αυτού του χρήστη στο αποθετήριο.
+issues.role.contributor=Συντελεστής
+issues.role.contributor_helper=Αυτός ο χρήστης έχει προηγούμενές υποβολές στο αποθετήριο.
 issues.re_request_review=Επαναίτηση ανασκόπησης
 issues.is_stale=Έχουν υπάρξει αλλαγές σε αυτό το PR από αυτή την αναθεώρηση
 issues.remove_request_review=Αφαίρεση αιτήματος αναθεώρησης
@@ -1425,6 +1483,9 @@ issues.label_title=Όνομα σήματος
 issues.label_description=Περιγραφή σήματος
 issues.label_color=Χρώμα σήματος
 issues.label_exclusive=Αποκλειστικό
+issues.label_archive=Αρχειοθέτηση Σήματος
+issues.label_archived_filter=Εμφάνιση αρχειοθετημένων σημάτων
+issues.label_archive_tooltip=Τα αρχειοθετημένα σήματα εξαιρούνται από τις προτάσεις στην αναζήτηση με σήματα.
 issues.label_exclusive_desc=Ονομάστε το σήμα <code>πεδίο/στοιχείο</code> για να το κάνετε αμοιβαία αποκλειστικό με άλλα σήματα <code>πεδίου/</code>.
 issues.label_exclusive_warning=Τυχόν συγκρουόμενα σήματα θα αφαιρεθούν κατά την επεξεργασία των σημάτων ενός ζητήματος ή pull request.
 issues.label_count=%d σήματα
@@ -1479,6 +1540,7 @@ issues.tracking_already_started=`Έχετε ήδη ξεκινήσει την κ
 issues.stop_tracking=Διακοπή Χρονομέτρου
 issues.stop_tracking_history=`σταμάτησε να εργάζεται %s`
 issues.cancel_tracking=Απόρριψη
+issues.cancel_tracking_history=`ακύρωσε τη παρακολούθηση χρόνου %s`
 issues.add_time=Χειροκίνητη Προσθήκη Ώρας
 issues.del_time=Διαγραφή αυτού του αρχείου χρόνου
 issues.add_time_short=Προσθήκη Χρόνου
@@ -1502,6 +1564,7 @@ issues.due_date_form=εεεε-μμ-ηη
 issues.due_date_form_add=Προσθήκη ημερομηνίας παράδοσης
 issues.due_date_form_edit=Επεξεργασία
 issues.due_date_form_remove=Διαγραφή
+issues.due_date_not_writer=Χρειάζεστε πρόσβαση εγγραφής στο αποθετήριο για να ενημερώσετε την ημερομηνία λήξης ενός προβλήματος.
 issues.due_date_not_set=Δεν ορίστηκε ημερομηνία παράδοσης.
 issues.due_date_added=πρόσθεσε την ημερομηνία παράδοσης %s %s
 issues.due_date_modified=τροποποίησε την ημερομηνία παράδοσης από %[2]s σε %[1]s %[3]s
@@ -1557,6 +1620,9 @@ issues.review.pending.tooltip=Αυτό το σχόλιο προς το παρό
 issues.review.review=Αξιολόγηση
 issues.review.reviewers=Εξεταστές
 issues.review.outdated=Παρωχημένο
+issues.review.outdated_description=Το περιεχόμενο άλλαξε αφού έγινε αυτό το σχόλιο
+issues.review.option.show_outdated_comments=Εμφάνιση παρωχημένων σχολίων
+issues.review.option.hide_outdated_comments=Απόκρυψη παρωχημένων σχολίων
 issues.review.show_outdated=Εμφάνιση παροχημένων
 issues.review.hide_outdated=Απόκρυψη παροχημένων
 issues.review.show_resolved=Εμφάνιση επιλυμένων
@@ -1596,6 +1662,13 @@ pulls.switch_comparison_type=Αλλαγή τύπου σύγκρισης
 pulls.switch_head_and_base=Αλλαγή κεφαλής και βάσης
 pulls.filter_branch=Φιλτράρισμα κλάδου
 pulls.no_results=Δεν βρέθηκαν αποτελέσματα.
+pulls.show_all_commits=Εμφάνιση όλων των υποβολών
+pulls.show_changes_since_your_last_review=Εμφάνιση αλλαγών από την τελευταία αξιολόγηση
+pulls.showing_only_single_commit=Εμφάνιση μόνο αλλαγών της υποβολής %[1]s
+pulls.showing_specified_commit_range=Εμφάνιση μόνο των αλλαγών μεταξύ %[1]s..%[2]s
+pulls.select_commit_hold_shift_for_range=Επιλέξτε υποβολή. Κρατήστε πατημένο το shift + κάντε κλικ για να επιλέξετε ένα εύρος
+pulls.review_only_possible_for_full_diff=Η αξιολόγηση είναι δυνατή μόνο κατά την προβολή της πλήρης διαφοράς
+pulls.filter_changes_by_commit=Φιλτράρισμα κατά υποβολή
 pulls.nothing_to_compare=Αυτοί οι κλάδοι είναι όμοιοι. Δεν υπάρχει ανάγκη να δημιουργήσετε ένα pull request.
 pulls.nothing_to_compare_and_allow_empty_pr=Αυτοί οι κλάδοι είναι ίσοι. Αυτό το PR θα είναι κενό.
 pulls.has_pull_request=`Υπάρχει ήδη pull request μεταξύ αυτών των κλάδων: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1627,6 +1700,8 @@ pulls.is_empty=Οι αλλαγές σε αυτόν τον κλάδο είναι
 pulls.required_status_check_failed=Ορισμένοι απαιτούμενοι έλεγχοι δεν ήταν επιτυχείς.
 pulls.required_status_check_missing=Λείπουν ορισμένοι απαιτούμενοι έλεγχοι.
 pulls.required_status_check_administrator=Ως διαχειριστής, μπορείτε ακόμα να συγχωνεύσετε αυτό το pull request.
+pulls.blocked_by_rejection=Αυτό το Pull Request έχει αλλαγές που ζητούνται από έναν επίσημο εξεταστή.
+pulls.blocked_by_official_review_requests=Αυτό το Pull Request έχει επίσημες αιτήσεις αξιολόγησης.
 pulls.can_auto_merge_desc=Αυτό το Pull Request μπορεί να συγχωνευθεί αυτόματα.
 pulls.cannot_auto_merge_desc=Αυτό το pull request δεν μπορεί να συγχωνευθεί αυτόματα λόγω συγκρούσεων.
 pulls.cannot_auto_merge_helper=Χειροκίνητη Συγχώνευση για την επίλυση των συγκρούσεων.
@@ -1672,6 +1747,8 @@ pulls.status_checks_failure=Κάποιοι έλεγχοι απέτυχαν
 pulls.status_checks_error=Ορισμένοι έλεγχοι ανέφεραν σφάλματα
 pulls.status_checks_requested=Απαιτείται
 pulls.status_checks_details=Λεπτομέρειες
+pulls.status_checks_hide_all=Απόκρυψη όλων των ελέγχων
+pulls.status_checks_show_all=Εμφάνιση όλων των ελέγχων
 pulls.update_branch=Ενημέρωση κλάδου με συγχώνευση
 pulls.update_branch_rebase=Ενημέρωση κλάδου με rebase
 pulls.update_branch_success=Η ενημέρωση του κλάδου ήταν επιτυχής
@@ -1680,6 +1757,7 @@ pulls.outdated_with_base_branch=Αυτός ο κλάδος δεν είναι ε
 pulls.close=Κλείσιμο Pull Request
 pulls.closed_at=`έκλεισε αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`άνοιξε ξανά αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_merge_title=Συγχώνευση
 pulls.clear_merge_message=Εκκαθάριση μηνύματος συγχώνευσης
 pulls.clear_merge_message_hint=Η εκκαθάριση του μηνύματος συγχώνευσης θα αφαιρέσει μόνο το περιεχόμενο του μηνύματος υποβολής και θα διατηρήσει τα παραγόμενα git trailers όπως "Co-Authored-By …".
 
@@ -1699,6 +1777,7 @@ pulls.delete.title=Διαγραφή αυτού του pull request;
 pulls.delete.text=Θέλετε πραγματικά να διαγράψετε αυτό το pull request; (Αυτό θα σβήσει οριστικά όλο το περιεχόμενο του. Εξετάστε αν θέλετε να το κλείσετε, αν σκοπεύεται να το αρχειοθετήσετε)
 
 
+pull.deleted_branch=(διαγράφηκε):%s
 
 milestones.new=Νέο Ορόσημο
 milestones.closed=Έκλεισε %s
@@ -1706,6 +1785,7 @@ milestones.update_ago=Ενημερώθηκε %s
 milestones.no_due_date=Δεν υπάρχει ημερομηνία παράδοσης
 milestones.open=Άνοιγμα
 milestones.close=Κλείσιμο
+milestones.new_subheader=Τα ορόσημα μπορούν να σας βοηθήσουν να οργανώσετε τα ζητήματα και να παρακολουθείτε την πρόοδό τους.
 milestones.completeness=%d%% Ολοκληρώθηκε
 milestones.create=Δημιουργία Ορόσημου
 milestones.title=Τίτλος
@@ -1722,11 +1802,16 @@ milestones.edit_success=Το ορόσημο "%s" ενημερώθηκε.
 milestones.deletion=Διαγραφή Ορόσημου
 milestones.deletion_desc=Η διαγραφή ενός ορόσημου το αφαιρεί από όλα τα συναφή ζητήματα. Συνέχεια;
 milestones.deletion_success=Το ορόσημο έχει διαγραφεί.
+milestones.filter_sort.earliest_due_data=Πλησιέστερη παράδοση
+milestones.filter_sort.latest_due_date=Απώτερη παράδοση
 milestones.filter_sort.least_complete=Λιγότερο πλήρη
 milestones.filter_sort.most_complete=Περισσότερο πλήρη
 milestones.filter_sort.most_issues=Περισσότερα ζητήματα
 milestones.filter_sort.least_issues=Λιγότερα ζητήματα
 
+signing.wont_sign.never=Οι υποβολές δεν υπογράφονται ποτέ.
+signing.wont_sign.always=Οι υποβολές υπογράφονται πάντα.
+signing.wont_sign.not_signed_in=Δεν είστε συνδεδεμένοι.
 
 ext_wiki=Πρόσβαση στο Εξωτερικό Wiki
 ext_wiki.desc=Σύνδεση σε ένα εξωτερικό wiki.
@@ -2203,16 +2288,20 @@ settings.tags.protection.create=Προστασία Ετικέτας
 settings.tags.protection.none=Δεν υπάρχουν προστατευμένες ετικέτες.
 settings.bot_token=Διακριτικό Bot
 settings.chat_id=ID Συνομιλίας
+settings.thread_id=ID Νήματος
 settings.matrix.homeserver_url=Homeserver URL
 settings.matrix.room_id=ID Δωματίου
 settings.matrix.message_type=Τύπος Μηνύματος
 settings.archive.button=Αρχειοθέτηση Αποθετηρίου
 settings.archive.header=Αρχειοθέτηση Αυτού του Αποθετηρίου
+settings.archive.text=Η αρχειοθέτηση του αποθετηρίου θα το αλλάξει σε μόνο για ανάγνωση. Δε θα φαίνεται στον αρχικό πίνακα. Κανείς (ακόμα και εσείς!) δε θα μπορεί να κάνει νέες υποβολές, ή να ανοίξει ζητήματα ή pull request.
 settings.archive.success=Το αποθετήριο αρχειοθετήθηκε με επιτυχία.
 settings.archive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια αρχειοθέτησης του αποθετηρίου. Δείτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες.
 settings.archive.error_ismirror=Δε μπορείτε να αρχειοθετήσετε ένα είδωλο αποθετηρίου.
 settings.archive.branchsettings_unavailable=Οι ρυθμίσεις του κλάδου δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο.
 settings.archive.tagsettings_unavailable=Οι ρυθμίσεις της ετικέτας δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο.
+settings.unarchive.button=Απο-Αρχειοθέτηση αποθετηρίου
+settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου
 settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί.
 settings.lfs=LFS
 settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο
@@ -2279,6 +2368,7 @@ diff.show_more=Εμφάνιση Περισσότερων
 diff.load=Φόρτωση Διαφορών
 diff.generated=δημιουργημένο
 diff.vendored=εξωτερικό
+diff.comment.add_line_comment=Προσθήκη σχολίου στη γραμμή
 diff.comment.placeholder=Αφήστε ένα σχόλιο
 diff.comment.markdown_info=Υποστηρίζεται στυλ με markdown.
 diff.comment.add_single_comment=Προσθέστε ένα σχόλιο
@@ -2371,6 +2461,7 @@ branch.default_deletion_failed=Ο κλάδος "%s" είναι ο προεπιλ
 branch.restore=`Επαναφορά του Κλάδου "%s"`
 branch.download=`Λήψη του Κλάδου "%s"`
 branch.rename=`Μετονομασία Κλάδου "%s"`
+branch.search=Αναζήτηση Κλάδου
 branch.included_desc=Αυτός ο κλάδος είναι μέρος του προεπιλεγμένου κλάδου
 branch.included=Περιλαμβάνεται
 branch.create_new_branch=Δημιουργία κλάδου από κλάδο:
@@ -2431,6 +2522,7 @@ form.create_org_not_allowed=Δεν επιτρέπεται να δημιουργ
 settings=Ρυθμίσεις
 settings.options=Οργανισμός
 settings.full_name=Πλήρες Όνομα
+settings.email=Email Επικοινωνίας
 settings.website=Ιστοσελίδα
 settings.location=Τοποθεσία
 settings.permission=Δικαιώματα
@@ -2444,6 +2536,7 @@ settings.visibility.private_shortname=Ιδιωτικός
 
 settings.update_settings=Ενημέρωση Ρυθμίσεων
 settings.update_setting_success=Οι ρυθμίσεις του οργανισμού έχουν ενημερωθεί.
+settings.change_orgname_prompt=Σημείωση: Η αλλαγή του ονόματος του οργανισμού θα αλλάξει επίσης τη διεύθυνση URL του οργανισμού σας και θα απελευθερώσει το παλιό όνομα.
 settings.change_orgname_redirect_prompt=Το παλιό όνομα θα ανακατευθύνει μέχρι να διεκδικηθεί.
 settings.update_avatar_success=Η εικόνα του οργανισμού έχει ενημερωθεί.
 settings.delete=Διαγραφή Οργανισμού
@@ -2519,15 +2612,19 @@ teams.all_repositories_helper=Η ομάδα έχει πρόσβαση σε όλ
 teams.all_repositories_read_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση <strong>Ανάγνωσης</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να δουν και να κλωνοποιήσουν αποθετήρια.
 teams.all_repositories_write_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση <strong>Εγγραφής</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να διαβάσουν και να κάνουν push σε αποθετήρια.
 teams.all_repositories_admin_permission_desc=Αυτή η ομάδα παρέχει πρόσβαση <strong>Διαχείρισης</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να διαβάσουν, να κάνουν push και να προσθέσουν συνεργάτες στα αποθετήρια.
+teams.invite.title=Έχετε προσκληθεί να συμμετάσχετε στην ομάδα <strong>%s</strong> του οργανισμού <strong>%s</strong>.
 teams.invite.by=Προσκλήθηκε από %s
 teams.invite.description=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για συμμετοχή στην ομάδα.
 
 [admin]
 dashboard=Πίνακας Ελέγχου
+identity_access=Ταυτότητα & Πρόσβαση
 users=Λογαριασμοί Χρήστη
 organizations=Οργανισμοί
+assets=Στοιχεία Κώδικα
 repositories=Αποθετήρια
 hooks=Webhooks
+integrations=Ενσωματώσεις
 authentication=Πηγές Ταυτοποίησης
 emails=Email Χρήστη
 config=Διαμόρφωση
@@ -2536,6 +2633,7 @@ monitor=Παρακολούθηση
 first_page=Πρώτο
 last_page=Τελευταίο
 total=Σύνολο: %d
+settings=Ρυθμίσεις Διαχειριστή
 
 dashboard.new_version_hint=Το Gitea %s είναι διαθέσιμο, τώρα εκτελείτε το %s. Ανατρέξτε <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">στο blog</a> για περισσότερες λεπτομέρειες.
 dashboard.statistic=Περίληψη
@@ -2613,6 +2711,8 @@ dashboard.gc_lfs=Συλλογή απορριμάτων στα μετα-αντι
 dashboard.stop_zombie_tasks=Διακοπή εργασιών ζόμπι
 dashboard.stop_endless_tasks=Διακοπή ατελείωτων εργασιών
 dashboard.cancel_abandoned_jobs=Ακύρωση εγκαταλελειμμένων εργασιών
+dashboard.start_schedule_tasks=Έναρξη προγραμματισμένων εργασιών
+dashboard.rebuild_issue_indexer=Αναδόμηση ευρετηρίου ζητημάτων
 
 users.user_manage_panel=Διαχείριση Λογαριασμών Χρηστών
 users.new_account=Δημιουργία Λογαριασμού Χρήστη
@@ -2621,6 +2721,9 @@ users.full_name=Πλήρες Όνομα
 users.activated=Ενεργοποιήθηκε
 users.admin=Διαχειριστής
 users.restricted=Περιορισμένος
+users.reserved=Δεσμευμένο
+users.bot=Bot
+users.remote=Απομακρυσμένο
 users.2fa=2FA
 users.repos=Αποθετήρια
 users.created=Δημιουργήθηκε
@@ -2667,6 +2770,7 @@ users.list_status_filter.is_prohibit_login=Απαγόρευση Σύνδεσης
 users.list_status_filter.not_prohibit_login=Επιτρέπεται η Σύνδεση
 users.list_status_filter.is_2fa_enabled=2FA Ενεργοποιημένο
 users.list_status_filter.not_2fa_enabled=2FA Απενεργοποιημένο
+users.details=Λεπτομέρειες Χρήστη
 
 emails.email_manage_panel=Διαχείριση Email Χρήστη
 emails.primary=Κύριο
@@ -2697,10 +2801,12 @@ repos.stars=Αστέρια
 repos.forks=Forks
 repos.issues=Ζητήματα
 repos.size=Μέγεθος
+repos.lfs_size=Μέγεθος LFS
 
 packages.package_manage_panel=Διαχείριση Πακέτων
 packages.total_size=Συνολικό Μέγεθος: %s
 packages.unreferenced_size=Μέγεθος Χωρίς Αναφορά: %s
+packages.cleanup=Εκκαθάριση ληγμένων δεδομένων
 packages.owner=Ιδιοκτήτης
 packages.creator=Δημιουργός
 packages.name=Όνομα
@@ -2847,6 +2953,7 @@ config.disable_router_log=Απενεργοποίηση Καταγραφής Δρ
 config.run_user=Εκτέλεση Σαν Χρήστη
 config.run_mode=Λειτουργία Εκτέλεσης
 config.git_version=Έκδοση Git
+config.app_data_path=Διαδρομή Δεδομένων Εφαρμογής
 config.repo_root_path=Ριζική Διαδρομή Αποθετηρίων
 config.lfs_root_path=Ριζική Διαδρομή LFS
 config.log_file_root_path=Διαδρομή Καταγραφών
@@ -2996,8 +3103,10 @@ monitor.queue.name=Όνομα
 monitor.queue.type=Τύπος
 monitor.queue.exemplar=Τύπος Υποδείγματος
 monitor.queue.numberworkers=Αριθμός Εργατών
+monitor.queue.activeworkers=Ενεργοί Εργάτες
 monitor.queue.maxnumberworkers=Μέγιστος Αριθμός Εργατών
 monitor.queue.numberinqueue=Πλήθος Ουράς
+monitor.queue.review_add=Εξέταση / Προσθήκη Εργατών
 monitor.queue.settings.title=Ρυθμίσεις Δεξαμενής
 monitor.queue.settings.desc=Οι δεξαμενές αυξάνονται δυναμικά όταν υπάρχει φραγή της ουράς των εργατών τους.
 monitor.queue.settings.maxnumberworkers=Μέγιστος Αριθμός Εργατών
@@ -3203,6 +3312,8 @@ pub.install=Για να εγκαταστήσετε το πακέτο μέσω τ
 pypi.requires=Απαιτεί Python
 pypi.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το pip, εκτελέστε την ακόλουθη εντολή:
 rpm.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών:
+rpm.distros.redhat=σε διανομές βασισμένες στο RedHat
+rpm.distros.suse=σε διανομές με βάση το SUSE
 rpm.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή:
 rubygems.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το gem, εκτελέστε την ακόλουθη εντολή:
 rubygems.install2=ή προσθέστε το στο Gemfile:
@@ -3235,6 +3346,7 @@ owner.settings.cargo.rebuild.success=Το ευρετήριο Cargo αναδομ
 owner.settings.cleanuprules.title=Διαχείριση Κανόνων Εκκαθάρισης
 owner.settings.cleanuprules.add=Προσθήκη Κανόνα Εκκαθάρισης
 owner.settings.cleanuprules.edit=Επεξεργασία Κανόνα Εκκαθάρισης
+owner.settings.cleanuprules.none=Δεν υπάρχουν διαθέσιμοι κανόνες εκκαθάρισης. Παρακαλούμε συμβουλευτείτε την τεκμηρίωση.
 owner.settings.cleanuprules.preview=Προεπισκόπηση Κανόνα Εκκαθάρισης
 owner.settings.cleanuprules.preview.overview=%d πακέτα έχουν προγραμματιστεί να αφαιρεθούν.
 owner.settings.cleanuprules.preview.none=Ο κανόνας εκκαθάρισης δεν ταιριάζει με κανένα πακέτο.
@@ -3279,6 +3391,7 @@ status.waiting=Αναμονή
 status.running=Εκτελείται
 status.success=Επιτυχές
 status.failure=Αποτυχία
+status.cancelled=Ακυρώθηκε
 status.skipped=Παρακάμφθηκε
 status.blocked=Αποκλείστηκε
 
@@ -3295,6 +3408,7 @@ runners.labels=Ετικέτες
 runners.last_online=Τελευταία Ώρα Σύνδεσης
 runners.runner_title=Εκτελεστής
 runners.task_list=Πρόσφατες εργασίες στον εκτελεστή
+runners.task_list.no_tasks=Δεν υπάρχει καμία εργασία ακόμα.
 runners.task_list.run=Εκτέλεση
 runners.task_list.status=Κατάσταση
 runners.task_list.repository=Αποθετήριο
@@ -3315,16 +3429,49 @@ runners.status.idle=Αδρανής
 runners.status.active=Ενεργό
 runners.status.offline=Χωρίς Σύνδεση
 runners.version=Έκδοση
+runners.reset_registration_token=Επαναφορά διακριτικού εγγραφής
 runners.reset_registration_token_success=Επιτυχής επανέκδοση διακριτικού εγγραφής του εκτελεστή
 
 runs.all_workflows=Όλες Οι Ροές Εργασίας
 runs.commit=Υποβολή
+runs.scheduled=Προγραμματισμένα
+runs.pushed_by=ωθήθηκε από
 runs.invalid_workflow_helper=Το αρχείο ροής εργασίας δεν είναι έγκυρο. Ελέγξτε το αρχείο σας: %s
+runs.no_matching_online_runner_helper=Κανένας δικτυακός δρομέας με ετικέτα: %s
+runs.actor=Φορέας
 runs.status=Κατάσταση
+runs.actors_no_select=Όλοι οι φορείς
+runs.status_no_select=Όλες οι καταστάσεις
+runs.no_results=Δεν βρέθηκαν αποτελέσματα.
+runs.no_workflows=Δεν υπάρχουν ροές εργασίας ακόμα.
+runs.no_workflows.quick_start=Δεν ξέρετε πώς να ξεκινήσετε με τις Δράσεις Gitea; Συμβουλευτείτε <a target="_blank" rel="noopener noreferrer" href="%s">τον οδηγό για γρήγορη αρχή</a>.
+runs.no_workflows.documentation=Για περισσότερες πληροφορίες σχετικά με τη Δράση Gitea, ανατρέξτε <a target="_blank" rel="noopener noreferrer" href="%s">στην τεκμηρίωση</a>.
+runs.no_runs=Η ροή εργασίας δεν έχει τρέξει ακόμα.
+runs.empty_commit_message=(κενό μήνυμα υποβολής)
 
+workflow.disable=Απενεργοποίηση Ροής Εργασιών
+workflow.disable_success=Η ροή εργασίας '%s' απενεργοποιήθηκε επιτυχώς.
+workflow.enable=Ενεργοποίηση Ροής Εργασίας
+workflow.enable_success=Η ροή εργασίας '%s' ενεργοποιήθηκε επιτυχώς.
+workflow.disabled=Η ροή εργασιών είναι απενεργοποιημένη.
 
 need_approval_desc=Πρέπει να εγκριθεί η εκτέλεση ροών εργασίας για pull request από fork.
 
+variables=Μεταβλητές
+variables.management=Διαχείριση Μεταβλητών
+variables.creation=Προσθήκη Μεταβλητής
+variables.none=Δεν υπάρχουν μεταβλητές ακόμα.
+variables.deletion=Αφαίρεση μεταβλητής
+variables.deletion.description=Η αφαίρεση μιας μεταβλητής είναι μόνιμη και δεν μπορεί να αναιρεθεί. Συνέχεια;
+variables.description=Η μεταβλητές θα δίνονται σε ορισμένες δράσεις και δεν μπορούν να διαβαστούν αλλιώς.
+variables.id_not_exist=Η μεταβλητή με id %d δεν υπάρχει.
+variables.edit=Επεξεργασία Μεταβλητής
+variables.deletion.failed=Αποτυχία αφαίρεσης της μεταβλητής.
+variables.deletion.success=Η μεταβλητή έχει αφαιρεθεί.
+variables.creation.failed=Αποτυχία προσθήκης μεταβλητής.
+variables.creation.success=Η μεταβλητή "%s" έχει προστεθεί.
+variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής.
+variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
 
 [projects]
 type-1.display_name=Ατομικό Έργο
@@ -3332,6 +3479,11 @@ type-2.display_name=Έργο Αποθετηρίου
 type-3.display_name=Έργο Οργανισμού
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Φάκελος
+normal_file=Κανονικό αρχείο
+executable_file=Εκτελέσιμο αρχείο
 symbolic_link=Symbolic link
+submodule=Υπομονάδα
 

From 5b2fd0fc19a2a77414c8e2989b4794b6617221f5 Mon Sep 17 00:00:00 2001
From: Gwyneth Morgan <gwymor@tilde.club>
Date: Sat, 10 Feb 2024 03:40:48 +0000
Subject: [PATCH 014/679] Drop "@" from email sender to avoid spam filters
 (#29109)

Commit 360b3fd17c (Include username in email headers (#28981),
2024-02-03) adds usernames to the From field of notification emails in
the form of `Display Name (@username)`, to prevent spoofing. However,
some email filtering software flags "@" in the display name part of the
From field as potential spoofing, as you could set the display name part
to another email address than the one you are sending from (e.g.
`From: "apparent@email-address" <actual@email-address>`). To avoid
being flagged, instead send emails from `Display Name (username)`.

Closes: #29107

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 models/user/user.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/models/user/user.go b/models/user/user.go
index e5245dfbb0..536ec78a0b 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -425,14 +425,14 @@ func (u *User) GetDisplayName() string {
 }
 
 // GetCompleteName returns the the full name and username in the form of
-// "Full Name (@username)" if full name is not empty, otherwise it returns
-// "@username".
+// "Full Name (username)" if full name is not empty, otherwise it returns
+// "username".
 func (u *User) GetCompleteName() string {
 	trimmedFullName := strings.TrimSpace(u.FullName)
 	if len(trimmedFullName) > 0 {
-		return fmt.Sprintf("%s (@%s)", trimmedFullName, u.Name)
+		return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name)
 	}
-	return fmt.Sprintf("@%s", u.Name)
+	return u.Name
 }
 
 func gitSafeName(name string) string {

From 5f5b5ba6e3e50ba5385e6cbf5957d4b73805769b Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 10 Feb 2024 14:55:46 +0200
Subject: [PATCH 015/679] Make blockquote border size less aggressive (#29124)

It's too thick

I made it match GitHub's size

# Before


![image](https://github.com/go-gitea/gitea/assets/20454870/08c05004-acd9-485e-9219-110d93fe1226)

# After


![image](https://github.com/go-gitea/gitea/assets/20454870/e2e32b6c-4ba8-488e-9405-95d33f80adf7)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/css/markup/content.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index caefa1605c..5eeef078a5 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -270,7 +270,7 @@
   margin-left: 0;
   padding: 0 15px;
   color: var(--color-text-light-2);
-  border-left: 4px solid var(--color-secondary);
+  border-left: 0.25em solid var(--color-secondary);
 }
 
 .markup blockquote > :first-child {

From 9063fa096386362f9ae602fdf8a39ae8c972b8e0 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 10 Feb 2024 19:18:46 +0100
Subject: [PATCH 016/679] Remove obsolete border-radius on comment content
 (#29128)

This border-radius is obsolete since we changed the comment rendering a
few months ago and it caused incorrect display on blockquotes.

Before:
<img width="160" alt="Screenshot 2024-02-10 at 18 42 48"
src="https://github.com/go-gitea/gitea/assets/115237/ccbf4660-acf9-4268-aad9-1ad49d317a67">

After:
<img width="135" alt="Screenshot 2024-02-10 at 18 42 40"
src="https://github.com/go-gitea/gitea/assets/115237/6f588e02-3b2a-49ee-b459-81d8068b2f4e">
---
 web_src/css/repo.css | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 55c6ec4817..610c3fcb55 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2289,10 +2289,6 @@
   padding: 1em;
 }
 
-.comment-body .markup {
-  border-radius: 0 0 var(--border-radius) var(--border-radius); /* don't render outside box */
-}
-
 .edit-label.modal .form .column,
 .new-label.modal .form .column {
   padding-right: 0;

From 12865ae9c6c164af6272b41e65b2cf2ea7a5e4b3 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 10 Feb 2024 20:43:09 +0200
Subject: [PATCH 017/679] Add alert blocks in markdown (#29121)

- Follows https://github.com/go-gitea/gitea/pull/21711
- Closes https://github.com/go-gitea/gitea/issues/28316

Implement GitHub's alert blocks markdown feature

Docs:
-
https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
- https://github.com/orgs/community/discussions/16925

### Before

![image](https://github.com/go-gitea/gitea/assets/20454870/14f7b02a-5de5-4fd0-8437-a055dadb31f2)

### After

![image](https://github.com/go-gitea/gitea/assets/20454870/ed06a869-e545-42f1-bf25-4ba20b1be196)

## :warning: BREAKING :warning:

The old syntax no longer works

How to migrate:

If you used
```md
> **Note** My note
```

Switch to
```md
> [!NOTE]
> My note
```

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 modules/markup/markdown/ast.go      |  7 +--
 modules/markup/markdown/goldmark.go | 77 +++++++++++++++++++++++------
 modules/markup/sanitizer.go         |  5 +-
 web_src/css/base.css                | 39 ++++++++++++---
 4 files changed, 97 insertions(+), 31 deletions(-)

diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go
index 3e6e291ab2..77ce5cb359 100644
--- a/modules/markup/markdown/ast.go
+++ b/modules/markup/markdown/ast.go
@@ -182,12 +182,7 @@ func IsColorPreview(node ast.Node) bool {
 	return ok
 }
 
-const (
-	AttentionNote    string = "Note"
-	AttentionWarning string = "Warning"
-)
-
-// Attention is an inline for a color preview
+// Attention is an inline for an attention
 type Attention struct {
 	ast.BaseInline
 	AttentionType string
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 178e3d2fdd..36ce6397f4 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -53,7 +53,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 		}
 	}
 
-	attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
 	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
 		if !entering {
 			return ast.WalkContinue, nil
@@ -197,18 +196,55 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 			if css.ColorHandler(strings.ToLower(string(colorContent))) {
 				v.AppendChild(v, NewColorPreview(colorContent))
 			}
-		case *ast.Emphasis:
-			// check if inside blockquote for attention, expected hierarchy is
-			// Emphasis < Paragraph < Blockquote
-			blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote)
-			if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) {
-				fullText := string(n.Text(reader.Source()))
-				if fullText == AttentionNote || fullText == AttentionWarning {
-					v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText)))
-					v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText))
-					attentionMarkedBlockquotes.Add(blockquote)
-				}
+		case *ast.Blockquote:
+			// We only want attention blockquotes when the AST looks like:
+			// Text: "["
+			// Text: "!TYPE"
+			// Text(SoftLineBreak): "]"
+
+			// grab these nodes and make sure we adhere to the attention blockquote structure
+			firstParagraph := v.FirstChild()
+			if firstParagraph.ChildCount() < 3 {
+				return ast.WalkContinue, nil
 			}
+			firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text)
+			if !ok || string(firstTextNode.Segment.Value(reader.Source())) != "[" {
+				return ast.WalkContinue, nil
+			}
+			secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text)
+			if !ok || !attentionTypeRE.MatchString(string(secondTextNode.Segment.Value(reader.Source()))) {
+				return ast.WalkContinue, nil
+			}
+			thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text)
+			if !ok || string(thirdTextNode.Segment.Value(reader.Source())) != "]" {
+				return ast.WalkContinue, nil
+			}
+
+			// grab attention type from markdown source
+			attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNode.Segment.Value(reader.Source())), "!"))
+
+			// color the blockquote
+			v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType))
+
+			// create an emphasis to make it bold
+			emphasis := ast.NewEmphasis(2)
+			emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
+			firstParagraph.InsertBefore(firstParagraph, firstTextNode, emphasis)
+
+			// capitalize first letter
+			attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:]))
+
+			// replace the ![TYPE] with icon+Type
+			emphasis.AppendChild(emphasis, attentionText)
+			for i := 0; i < 2; i++ {
+				lineBreak := ast.NewText()
+				lineBreak.SetSoftLineBreak(true)
+				firstParagraph.InsertAfter(firstParagraph, emphasis, lineBreak)
+			}
+			firstParagraph.InsertBefore(firstParagraph, emphasis, NewAttention(attentionType))
+			firstParagraph.RemoveChild(firstParagraph, firstTextNode)
+			firstParagraph.RemoveChild(firstParagraph, secondTextNode)
+			firstParagraph.RemoveChild(firstParagraph, thirdTextNode)
 		}
 		return ast.WalkContinue, nil
 	})
@@ -339,17 +375,23 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
 // renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
 func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	if entering {
-		_, _ = w.WriteString(`<span class="attention-icon attention-`)
+		_, _ = w.WriteString(`<span class="gt-mr-2 gt-vm attention-`)
 		n := node.(*Attention)
 		_, _ = w.WriteString(strings.ToLower(n.AttentionType))
 		_, _ = w.WriteString(`">`)
 
 		var octiconType string
 		switch n.AttentionType {
-		case AttentionNote:
+		case "note":
 			octiconType = "info"
-		case AttentionWarning:
+		case "tip":
+			octiconType = "light-bulb"
+		case "important":
+			octiconType = "report"
+		case "warning":
 			octiconType = "alert"
+		case "caution":
+			octiconType = "stop"
 		}
 		_, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType)))
 	} else {
@@ -417,7 +459,10 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
 	return ast.WalkContinue, nil
 }
 
-var validNameRE = regexp.MustCompile("^[a-z ]+$")
+var (
+	validNameRE     = regexp.MustCompile("^[a-z ]+$")
+	attentionTypeRE = regexp.MustCompile("^!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)$")
+)
 
 func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	if !entering {
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 992e85b989..ffc33c3b8e 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -64,9 +64,10 @@ func createDefaultPolicy() *bluemonday.Policy {
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
 
 	// For attention
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-py-3 attention attention-\w+$`)).OnElements("blockquote")
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+$`)).OnElements("span", "strong")
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-\w+$`)).OnElements("svg")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-mr-2 gt-vm attention-\w+$`)).OnElements("span", "strong")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-(\w|-)+$`)).OnElements("svg")
 	policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
 	policy.AllowAttrs("fill-rule", "d").OnElements("path")
 
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 198e87c0e2..ea32aac6f7 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1268,20 +1268,45 @@ img.ui.avatar,
   border-radius: var(--border-radius);
 }
 
-.attention-icon {
-  vertical-align: text-top;
+.attention {
+  color: var(--color-text) !important;
 }
 
-.attention-note {
-  font-weight: unset;
-  color: var(--color-info-text);
+blockquote.attention-note {
+  border-left-color: var(--color-blue-dark-1);
+}
+strong.attention-note, span.attention-note {
+  color: var(--color-blue-dark-1);
 }
 
-.attention-warning {
-  font-weight: unset;
+blockquote.attention-tip {
+  border-left-color: var(--color-success-text);
+}
+strong.attention-tip, span.attention-tip {
+  color: var(--color-success-text);
+}
+
+blockquote.attention-important {
+  border-left-color: var(--color-violet-dark-1);
+}
+strong.attention-important, span.attention-important {
+  color: var(--color-violet-dark-1);
+}
+
+blockquote.attention-warning {
+  border-left-color: var(--color-warning-text);
+}
+strong.attention-warning, span.attention-warning {
   color: var(--color-warning-text);
 }
 
+blockquote.attention-caution {
+  border-left-color: var(--color-red-dark-1);
+}
+strong.attention-caution, span.attention-caution {
+  color: var(--color-red-dark-1);
+}
+
 .center:not(.popup) {
   text-align: center;
 }

From 4fe37124e9ad5395b734662a7e8ab7b0025c38a3 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 11 Feb 2024 13:55:11 +0100
Subject: [PATCH 018/679] Update JS and PY dependencies (#29127)

- Update all excluding `@mcaptcha/vanilla-glue` and
`eslint-plugin-array-func`
- Remove deprecated and duplicate eslint rule
- Tested Monaco, Mermaid and Swagger
---
 .eslintrc.yaml    |   1 -
 package-lock.json | 243 +++++++++++++++++++++-------------------------
 package.json      |  22 ++---
 poetry.lock       |   8 +-
 pyproject.toml    |   2 +-
 5 files changed, 125 insertions(+), 151 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index fc6f38ec53..ed0309dbea 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -558,7 +558,6 @@ rules:
   prefer-rest-params: [2]
   prefer-spread: [2]
   prefer-template: [2]
-  quotes: [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
   radix: [2, as-needed]
   regexp/confusing-quantifier: [2]
   regexp/control-character-escape: [2]
diff --git a/package-lock.json b/package-lock.json
index 6918dc64b7..62bf36e7b7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,22 +30,22 @@
         "jquery": "3.7.1",
         "katex": "0.16.9",
         "license-checker-webpack-plugin": "0.2.1",
-        "mermaid": "10.7.0",
+        "mermaid": "10.8.0",
         "mini-css-extract-plugin": "2.8.0",
         "minimatch": "9.0.3",
-        "monaco-editor": "0.45.0",
+        "monaco-editor": "0.46.0",
         "monaco-editor-webpack-plugin": "7.1.0",
         "pdfobject": "2.2.12",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
-        "swagger-ui-dist": "5.11.2",
+        "swagger-ui-dist": "5.11.3",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
         "tippy.js": "6.3.7",
         "toastify-js": "1.12.0",
         "tributejs": "5.1.3",
         "uint8-to-base64": "0.2.0",
-        "vue": "3.4.15",
+        "vue": "3.4.18",
         "vue-bar-graph": "2.0.0",
         "vue-loader": "17.4.2",
         "vue3-calendar-heatmap": "2.0.5",
@@ -55,11 +55,11 @@
       },
       "devDependencies": {
         "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
-        "@playwright/test": "1.41.1",
+        "@playwright/test": "1.41.2",
         "@stoplight/spectral-cli": "6.11.0",
-        "@stylistic/eslint-plugin-js": "1.5.4",
+        "@stylistic/eslint-plugin-js": "1.6.1",
         "@stylistic/stylelint-plugin": "2.0.0",
-        "@vitejs/plugin-vue": "5.0.3",
+        "@vitejs/plugin-vue": "5.0.4",
         "eslint": "8.56.0",
         "eslint-plugin-array-func": "4.0.0",
         "eslint-plugin-i": "2.29.1",
@@ -68,8 +68,8 @@
         "eslint-plugin-no-use-extend-native": "0.5.0",
         "eslint-plugin-regexp": "2.2.0",
         "eslint-plugin-sonarjs": "0.23.0",
-        "eslint-plugin-unicorn": "50.0.1",
-        "eslint-plugin-vitest": "0.3.21",
+        "eslint-plugin-unicorn": "51.0.1",
+        "eslint-plugin-vitest": "0.3.22",
         "eslint-plugin-vitest-globals": "1.4.0",
         "eslint-plugin-vue": "9.21.1",
         "eslint-plugin-vue-scoped-css": "2.7.2",
@@ -81,8 +81,8 @@
         "stylelint-declaration-block-no-ignored-properties": "2.8.0",
         "stylelint-declaration-strict-value": "1.10.4",
         "svgo": "3.2.0",
-        "updates": "15.1.1",
-        "vite-string-plugin": "1.1.3",
+        "updates": "15.1.2",
+        "vite-string-plugin": "1.1.5",
         "vitest": "1.2.2"
       },
       "engines": {
@@ -1370,12 +1370,12 @@
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.41.1",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz",
-      "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==",
+      "version": "1.41.2",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
+      "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
       "dev": true,
       "dependencies": {
-        "playwright": "1.41.1"
+        "playwright": "1.41.2"
       },
       "bin": {
         "playwright": "cli.js"
@@ -2072,9 +2072,9 @@
       "dev": true
     },
     "node_modules/@stylistic/eslint-plugin-js": {
-      "version": "1.5.4",
-      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.4.tgz",
-      "integrity": "sha512-3ctWb3NvJNV1MsrZN91cYp2EGInLPSoZKphXIbIRx/zjZxKwLDr9z4LMOWtqjq14li/OgqUUcMq5pj8fgbLoTw==",
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.1.tgz",
+      "integrity": "sha512-gHRxkbA5p8S1fnChE7Yf5NFltRZCzbCuQOcoTe93PSKBC4GqVjZmlWUSLz9pJKHvDAUTjWkfttWHIOaFYPEhRQ==",
       "dev": true,
       "dependencies": {
         "acorn": "^8.11.3",
@@ -2366,9 +2366,9 @@
       "dev": true
     },
     "node_modules/@vitejs/plugin-vue": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz",
-      "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==",
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz",
+      "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==",
       "dev": true,
       "engines": {
         "node": "^18.0.0 || >=20.0.0"
@@ -2502,46 +2502,46 @@
       }
     },
     "node_modules/@vue/compiler-core": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
-      "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.18.tgz",
+      "integrity": "sha512-F7YK8lMK0iv6b9/Gdk15A67wM0KKZvxDxed0RR60C1z9tIJTKta+urs4j0RTN5XqHISzI3etN3mX0uHhjmoqjQ==",
       "dependencies": {
-        "@babel/parser": "^7.23.6",
-        "@vue/shared": "3.4.15",
+        "@babel/parser": "^7.23.9",
+        "@vue/shared": "3.4.18",
         "entities": "^4.5.0",
         "estree-walker": "^2.0.2",
         "source-map-js": "^1.0.2"
       }
     },
     "node_modules/@vue/compiler-dom": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
-      "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.18.tgz",
+      "integrity": "sha512-24Eb8lcMfInefvQ6YlEVS18w5Q66f4+uXWVA+yb7praKbyjHRNuKVWGuinfSSjM0ZIiPi++QWukhkgznBaqpEA==",
       "dependencies": {
-        "@vue/compiler-core": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-core": "3.4.18",
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
-      "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.18.tgz",
+      "integrity": "sha512-rG5tqtnzwrVpMqAQ7FHtvHaV70G6LLfJIWLYZB/jZ9m/hrnZmIQh+H3ewnC5onwe/ibljm9+ZupxeElzqCkTAw==",
       "dependencies": {
-        "@babel/parser": "^7.23.6",
-        "@vue/compiler-core": "3.4.15",
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/compiler-ssr": "3.4.15",
-        "@vue/shared": "3.4.15",
+        "@babel/parser": "^7.23.9",
+        "@vue/compiler-core": "3.4.18",
+        "@vue/compiler-dom": "3.4.18",
+        "@vue/compiler-ssr": "3.4.18",
+        "@vue/shared": "3.4.18",
         "estree-walker": "^2.0.2",
-        "magic-string": "^0.30.5",
+        "magic-string": "^0.30.6",
         "postcss": "^8.4.33",
         "source-map-js": "^1.0.2"
       }
     },
     "node_modules/@vue/compiler-sfc/node_modules/magic-string": {
-      "version": "0.30.6",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz",
-      "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==",
+      "version": "0.30.7",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
+      "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
       },
@@ -2550,57 +2550,57 @@
       }
     },
     "node_modules/@vue/compiler-ssr": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
-      "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.18.tgz",
+      "integrity": "sha512-hSlv20oUhPxo2UYUacHgGaxtqP0tvFo6ixxxD6JlXIkwzwoZ9eKK6PFQN4hNK/R13JlNyldwWt/fqGBKgWJ6nQ==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-dom": "3.4.18",
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/reactivity": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
-      "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.18.tgz",
+      "integrity": "sha512-7uda2/I0jpLiRygprDo5Jxs2HJkOVXcOMlyVlY54yRLxoycBpwGJRwJT9EdGB4adnoqJDXVT2BilUAYwI7qvmg==",
       "dependencies": {
-        "@vue/shared": "3.4.15"
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/runtime-core": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
-      "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.18.tgz",
+      "integrity": "sha512-7mU9diCa+4e+8/wZ7Udw5pwTH10A11sZ1nldmHOUKJnzCwvZxfJqAtw31mIf4T5H2FsLCSBQT3xgioA9vIjyDQ==",
       "dependencies": {
-        "@vue/reactivity": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/reactivity": "3.4.18",
+        "@vue/shared": "3.4.18"
       }
     },
     "node_modules/@vue/runtime-dom": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
-      "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.18.tgz",
+      "integrity": "sha512-2y1Mkzcw1niSfG7z3Qx+2ir9Gb4hdTkZe5p/I8x1aTIKQE0vY0tPAEUPhZm5tx6183gG3D/KwHG728UR0sIufA==",
       "dependencies": {
-        "@vue/runtime-core": "3.4.15",
-        "@vue/shared": "3.4.15",
+        "@vue/runtime-core": "3.4.18",
+        "@vue/shared": "3.4.18",
         "csstype": "^3.1.3"
       }
     },
     "node_modules/@vue/server-renderer": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
-      "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.18.tgz",
+      "integrity": "sha512-YJd1wa7mzUN3NRqLEsrwEYWyO+PUBSROIGlCc3J/cvn7Zu6CxhNLgXa8Z4zZ5ja5/nviYO79J1InoPeXgwBTZA==",
       "dependencies": {
-        "@vue/compiler-ssr": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-ssr": "3.4.18",
+        "@vue/shared": "3.4.18"
       },
       "peerDependencies": {
-        "vue": "3.4.15"
+        "vue": "3.4.18"
       }
     },
     "node_modules/@vue/shared": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
-      "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.18.tgz",
+      "integrity": "sha512-CxouGFxxaW5r1WbrSmWwck3No58rApXgRSBxrqgnY1K+jk20F6DrXJkHdH9n4HVT+/B6G2CAn213Uq3npWiy8Q=="
     },
     "node_modules/@webassemblyjs/ast": {
       "version": "1.11.6",
@@ -3766,30 +3766,6 @@
         "cytoscape": "^3.2.0"
       }
     },
-    "node_modules/cytoscape-fcose": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
-      "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
-      "dependencies": {
-        "cose-base": "^2.2.0"
-      },
-      "peerDependencies": {
-        "cytoscape": "^3.2.0"
-      }
-    },
-    "node_modules/cytoscape-fcose/node_modules/cose-base": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
-      "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
-      "dependencies": {
-        "layout-base": "^2.0.0"
-      }
-    },
-    "node_modules/cytoscape-fcose/node_modules/layout-base": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
-      "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
-    },
     "node_modules/d3": {
       "version": "7.8.5",
       "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
@@ -5012,9 +4988,9 @@
       }
     },
     "node_modules/eslint-plugin-unicorn": {
-      "version": "50.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
-      "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
+      "version": "51.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz",
+      "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==",
       "dev": true,
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
@@ -5045,12 +5021,12 @@
       }
     },
     "node_modules/eslint-plugin-vitest": {
-      "version": "0.3.21",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.21.tgz",
-      "integrity": "sha512-oYwR1MrwaBw/OG6CKU+SJYleAc442w6CWL1RTQl5WLwy8X3sh0bgHIQk5iEtmTak3Q+XAvZglr0bIoDOjFdkcw==",
+      "version": "0.3.22",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.22.tgz",
+      "integrity": "sha512-atkFGQ7aVgcuSeSMDqnyevIyUpfBPMnosksgEPrKE7Y8xQlqG/5z2IQ6UDau05zXaaFv7Iz8uzqvIuKshjZ0Zw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/utils": "^6.20.0"
+        "@typescript-eslint/utils": "^6.21.0"
       },
       "engines": {
         "node": "^18.0.0 || >= 20.0.0"
@@ -7341,16 +7317,15 @@
       }
     },
     "node_modules/mermaid": {
-      "version": "10.7.0",
-      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.7.0.tgz",
-      "integrity": "sha512-PsvGupPCkN1vemAAjScyw4pw34p4/0dZkSrqvAB26hUvJulOWGIwt35FZWmT9wPIi4r0QLa5X0PB4YLIGn0/YQ==",
+      "version": "10.8.0",
+      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.8.0.tgz",
+      "integrity": "sha512-9CzfSreRjdDJxX796+jW4zjEq0DVw5xVF0nWsqff8OTbrt+ml0TZ5PyYUjjUZJa2NYxYJZZXewEquxGiM8qZEA==",
       "dependencies": {
         "@braintree/sanitize-url": "^6.0.1",
         "@types/d3-scale": "^4.0.3",
         "@types/d3-scale-chromatic": "^3.0.0",
-        "cytoscape": "^3.23.0",
+        "cytoscape": "^3.28.1",
         "cytoscape-cose-bilkent": "^4.1.0",
-        "cytoscape-fcose": "^2.1.0",
         "d3": "^7.4.0",
         "d3-sankey": "^0.12.3",
         "dagre-d3-es": "7.0.10",
@@ -7904,9 +7879,9 @@
       }
     },
     "node_modules/monaco-editor": {
-      "version": "0.45.0",
-      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz",
-      "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA=="
+      "version": "0.46.0",
+      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz",
+      "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ=="
     },
     "node_modules/monaco-editor-webpack-plugin": {
       "version": "7.1.0",
@@ -8491,12 +8466,12 @@
       "dev": true
     },
     "node_modules/playwright": {
-      "version": "1.41.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz",
-      "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==",
+      "version": "1.41.2",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
+      "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
       "dev": true,
       "dependencies": {
-        "playwright-core": "1.41.1"
+        "playwright-core": "1.41.2"
       },
       "bin": {
         "playwright": "cli.js"
@@ -8509,9 +8484,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.41.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz",
-      "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==",
+      "version": "1.41.2",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
+      "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -10135,9 +10110,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.11.2",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz",
-      "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A=="
+      "version": "5.11.3",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.3.tgz",
+      "integrity": "sha512-vQ+Pe73xt7vMVbX40L6nHu4sDmNCM6A+eMVJPGvKrifHQ4LO3smH0jCiiefKzsVl7OlOcVEnrZ9IFzYwElfMkA=="
     },
     "node_modules/symbol-tree": {
       "version": "3.2.4",
@@ -10620,9 +10595,9 @@
       }
     },
     "node_modules/updates": {
-      "version": "15.1.1",
-      "resolved": "https://registry.npmjs.org/updates/-/updates-15.1.1.tgz",
-      "integrity": "sha512-dMz/4251b0lV7yR58tuydCKaiWxOa18YM8fnRgtiDVzQ5ALopTZhMckv00w0nSMj6OFMFKLshTZGkX4dAebaaw==",
+      "version": "15.1.2",
+      "resolved": "https://registry.npmjs.org/updates/-/updates-15.1.2.tgz",
+      "integrity": "sha512-+/JT4NChl82iexV9G80TY5HF3ubQ5O9UTOk3LlCo4Y4aRCYvo1h4bJE8YkP0PE7KiFRWIQq/rPmUYrY2QF8wVA==",
       "dev": true,
       "bin": {
         "updates": "bin/updates.js"
@@ -10795,9 +10770,9 @@
       }
     },
     "node_modules/vite-string-plugin": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.3.tgz",
-      "integrity": "sha512-uHL8BV2tBf32T2slYpS0vRzGVrAS3iuivtGknjzyecvpSq2AiBSkyLAjEvvIZuZGDDGFHyGX+5+yc3OBPjWDlA==",
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.5.tgz",
+      "integrity": "sha512-KRCIFX3PWVUuEjpi9O7EKLT9E27OqOA3RimIvVx6cziLAUxvnk2VvHQfMrP+mKkqyqqSmnnYyTig3OyDnK/zlA==",
       "dev": true
     },
     "node_modules/vite/node_modules/@types/estree": {
@@ -10931,15 +10906,15 @@
       }
     },
     "node_modules/vue": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
-      "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
+      "version": "3.4.18",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.18.tgz",
+      "integrity": "sha512-0zLRYamFRe0wF4q2L3O24KQzLyLpL64ye1RUToOgOxuWZsb/FhaNRdGmeozdtVYLz6tl94OXLaK7/WQIrVCw1A==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/compiler-sfc": "3.4.15",
-        "@vue/runtime-dom": "3.4.15",
-        "@vue/server-renderer": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-dom": "3.4.18",
+        "@vue/compiler-sfc": "3.4.18",
+        "@vue/runtime-dom": "3.4.18",
+        "@vue/server-renderer": "3.4.18",
+        "@vue/shared": "3.4.18"
       },
       "peerDependencies": {
         "typescript": "*"
diff --git a/package.json b/package.json
index ef1fcca545..46dfdd1055 100644
--- a/package.json
+++ b/package.json
@@ -29,22 +29,22 @@
     "jquery": "3.7.1",
     "katex": "0.16.9",
     "license-checker-webpack-plugin": "0.2.1",
-    "mermaid": "10.7.0",
+    "mermaid": "10.8.0",
     "mini-css-extract-plugin": "2.8.0",
     "minimatch": "9.0.3",
-    "monaco-editor": "0.45.0",
+    "monaco-editor": "0.46.0",
     "monaco-editor-webpack-plugin": "7.1.0",
     "pdfobject": "2.2.12",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
-    "swagger-ui-dist": "5.11.2",
+    "swagger-ui-dist": "5.11.3",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
     "tippy.js": "6.3.7",
     "toastify-js": "1.12.0",
     "tributejs": "5.1.3",
     "uint8-to-base64": "0.2.0",
-    "vue": "3.4.15",
+    "vue": "3.4.18",
     "vue-bar-graph": "2.0.0",
     "vue-loader": "17.4.2",
     "vue3-calendar-heatmap": "2.0.5",
@@ -54,11 +54,11 @@
   },
   "devDependencies": {
     "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
-    "@playwright/test": "1.41.1",
+    "@playwright/test": "1.41.2",
     "@stoplight/spectral-cli": "6.11.0",
-    "@stylistic/eslint-plugin-js": "1.5.4",
+    "@stylistic/eslint-plugin-js": "1.6.1",
     "@stylistic/stylelint-plugin": "2.0.0",
-    "@vitejs/plugin-vue": "5.0.3",
+    "@vitejs/plugin-vue": "5.0.4",
     "eslint": "8.56.0",
     "eslint-plugin-array-func": "4.0.0",
     "eslint-plugin-i": "2.29.1",
@@ -67,8 +67,8 @@
     "eslint-plugin-no-use-extend-native": "0.5.0",
     "eslint-plugin-regexp": "2.2.0",
     "eslint-plugin-sonarjs": "0.23.0",
-    "eslint-plugin-unicorn": "50.0.1",
-    "eslint-plugin-vitest": "0.3.21",
+    "eslint-plugin-unicorn": "51.0.1",
+    "eslint-plugin-vitest": "0.3.22",
     "eslint-plugin-vitest-globals": "1.4.0",
     "eslint-plugin-vue": "9.21.1",
     "eslint-plugin-vue-scoped-css": "2.7.2",
@@ -80,8 +80,8 @@
     "stylelint-declaration-block-no-ignored-properties": "2.8.0",
     "stylelint-declaration-strict-value": "1.10.4",
     "svgo": "3.2.0",
-    "updates": "15.1.1",
-    "vite-string-plugin": "1.1.3",
+    "updates": "15.1.2",
+    "vite-string-plugin": "1.1.5",
     "vitest": "1.2.2"
   },
   "browserslist": [
diff --git a/poetry.lock b/poetry.lock
index 74d202c919..4897496a40 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -342,13 +342,13 @@ telegram = ["requests"]
 
 [[package]]
 name = "yamllint"
-version = "1.33.0"
+version = "1.34.0"
 description = "A linter for YAML files."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "yamllint-1.33.0-py3-none-any.whl", hash = "sha256:28a19f5d68d28d8fec538a1db21bb2d84c7dc2e2ea36266da8d4d1c5a683814d"},
-    {file = "yamllint-1.33.0.tar.gz", hash = "sha256:2dceab9ef2d99518a2fcf4ffc964d44250ac4459be1ba3ca315118e4a1a81f7d"},
+    {file = "yamllint-1.34.0-py3-none-any.whl", hash = "sha256:33b813f6ff2ffad2e57a288281098392b85f7463ce1f3d5cd45aa848b916a806"},
+    {file = "yamllint-1.34.0.tar.gz", hash = "sha256:7f0a6a41e8aab3904878da4ae34b6248b6bc74634e0d3a90f0fb2d7e723a3d4f"},
 ]
 
 [package.dependencies]
@@ -361,4 +361,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "175c87d138a47ba190a2c3f16b801f694915cc6f2367a358585df9cd1b17ff96"
+content-hash = "e4ea4301a70487379fce7008493d15c005af3aada7d88fbf0bd3167147ec6502"
diff --git a/pyproject.toml b/pyproject.toml
index d999a1476c..eb6d4b2311 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ python = "^3.8"
 
 [tool.poetry.group.dev.dependencies]
 djlint = "1.34.1"
-yamllint = "1.33.0"
+yamllint = "1.34.0"
 
 [tool.djlint]
 profile="golang"

From 28db539d9c0fa0b7c9411724d8b4bf6f371651a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Nicas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Sun, 11 Feb 2024 15:10:04 +0100
Subject: [PATCH 019/679] Show more settings for empty repositories (#29130)

Shows more settings for empty repositories (Fixes #29060)
---
 templates/repo/settings/navbar.tmpl | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
index 3bef0fa4c1..0b0ef0b6e8 100644
--- a/templates/repo/settings/navbar.tmpl
+++ b/templates/repo/settings/navbar.tmpl
@@ -12,10 +12,12 @@
 				{{ctx.Locale.Tr "repo.settings.hooks"}}
 			</a>
 		{{end}}
-		{{if and (.Repository.UnitEnabled $.Context $.UnitTypeCode) (not .Repository.IsEmpty)}}
-			<a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches">
-				{{ctx.Locale.Tr "repo.settings.branches"}}
-			</a>
+		{{if .Repository.UnitEnabled $.Context $.UnitTypeCode}}
+			{{if not .Repository.IsEmpty}}
+				<a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches">
+					{{ctx.Locale.Tr "repo.settings.branches"}}
+				</a>
+			{{end}}
 			<a class="{{if .PageIsSettingsTags}}active {{end}}item" href="{{.RepoLink}}/settings/tags">
 				{{ctx.Locale.Tr "repo.settings.tags"}}
 			</a>

From 470a3e3f89af1c772beb761dcdea937964b69de1 Mon Sep 17 00:00:00 2001
From: me2seeks <96464454+me2seeks@users.noreply.github.com>
Date: Mon, 12 Feb 2024 01:03:49 +0800
Subject: [PATCH 020/679] Update some translations and fix markdown formatting
 (#29099)

Update `docs/content/administration/backup-and-restore.zh-cn.md`
`docs/content/contributing/guidelines-frontend.zh-cn.md`
`docs/content/help/support.zh-cn.md`
`docs/content/installation/database-preparation.zh-cn.md`
`docs/content/installation/windows-service.zh-cn.md`
`docs/content/usage/profile-readme.zh-cn.md` to be consistent with the
English document
---
 .../backup-and-restore.zh-cn.md               | 98 +++++++++++++++++--
 .../contributing/guidelines-frontend.zh-cn.md | 16 ++-
 docs/content/help/support.zh-cn.md            | 65 ++++++++++--
 .../database-preparation.zh-cn.md             | 14 ++-
 .../installation/windows-service.zh-cn.md     | 17 +++-
 docs/content/usage/profile-readme.zh-cn.md    |  6 +-
 6 files changed, 193 insertions(+), 23 deletions(-)

diff --git a/docs/content/administration/backup-and-restore.zh-cn.md b/docs/content/administration/backup-and-restore.zh-cn.md
index 98d378d5dc..db7eba84f7 100644
--- a/docs/content/administration/backup-and-restore.zh-cn.md
+++ b/docs/content/administration/backup-and-restore.zh-cn.md
@@ -19,6 +19,12 @@ menu:
 
 Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一个zip压缩文件。该压缩文件可以被用来进行数据恢复。
 
+## 备份一致性
+
+为了确保 Gitea 实例的一致性,在备份期间必须关闭它。
+
+Gitea 包括数据库、文件和 Git 仓库,当它被使用时所有这些都会发生变化。例如,当迁移正在进行时,在数据库中创建一个事务,而 Git 仓库正在被复制。如果备份发生在迁移的中间,Git 仓库可能是不完整的,尽管数据库声称它是完整的,因为它是在之后被转储的。避免这种竞争条件的唯一方法是在备份期间停止 Gitea 实例。
+
 ## 备份命令 (`dump`)
 
 先转到git用户的权限: `su git`. 再Gitea目录运行 `./gitea dump`。一般会显示类似如下的输出:
@@ -34,15 +40,43 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一
 
 最后生成的 `gitea-dump-1482906742.zip` 文件将会包含如下内容:
 
-* `custom` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
-* `data` - 数据目录下的所有内容不包含使用文件session的文件。该目录包含 `attachments`, `avatars`, `lfs`, `indexers`, 如果使用sqlite 还会包含 sqlite 数据库文件。
+* `app.ini` - 如果原先存储在默认的 custom/ 目录之外,则是配置文件的可选副本
+* `custom/` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
+* `data/` - 数据目录(APP_DATA_PATH),如果使用文件会话,则不包括会话。该目录包括 `attachments`、`avatars`、`lfs`、`indexers`、如果使用 SQLite 则包括 SQLite 文件。
+* `repos/` - 仓库目录的完整副本。
 * `gitea-db.sql` - 数据库dump出来的 SQL。
-* `gitea-repo.zip` - Git仓库压缩文件。
 * `log/` - Logs文件,如果用作迁移不是必须的。
 
 中间备份文件将会在临时目录进行创建,如果您要重新指定临时目录,可以用 `--tempdir` 参数,或者用 `TMPDIR` 环境变量。
 
-## Restore Command (`restore`)
+## 备份数据库
+
+`gitea dump` 创建的 SQL 转储使用 XORM,Gitea 管理员可能更喜欢使用本地的 MySQL 和 PostgreSQL 转储工具。使用 XORM 转储数据库时仍然存在一些问题,可能会导致在尝试恢复时出现问题。
+
+```sh
+# mysql
+mysqldump -u$USER -p$PASS --database $DATABASE > gitea-db.sql
+# postgres
+pg_dump -U $USER $DATABASE > gitea-db.sql
+```
+
+### 使用Docker (`dump`)
+
+在使用 Docker 时,使用 `dump` 命令有一些注意事项。
+
+必须以 `gitea/conf/app.ini` 中指定的 `RUN_USER = <OS_USERNAME>` 执行该命令;并且,为了让备份文件夹的压缩过程能够顺利执行,`docker exec` 命令必须在 `--tempdir` 内部执行。
+
+示例:
+
+```none
+docker exec -u <OS_USERNAME> -it -w <--tempdir> $(docker ps -qf 'name=^<NAME_OF_DOCKER_CONTAINER>$') bash -c '/usr/local/bin/gitea dump -c </path/to/app.ini>'
+```
+
+\*注意:`--tempdir` 指的是 Gitea 使用的 Docker 环境的临时目录;如果您没有指定自定义的 `--tempdir`,那么 Gitea 将使用 `/tmp` 或 Docker 容器的 `TMPDIR` 环境变量。对于 `--tempdir`,请相应调整您的 `docker exec` 命令选项。
+
+结果应该是一个文件,存储在指定的 `--tempdir` 中,类似于:`gitea-dump-1482906742.zip`
+
+## 恢复命令 (`restore`)
 
 当前还没有恢复命令,恢复需要人工进行。主要是把文件和数据库进行恢复。
 
@@ -51,10 +85,10 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一
 ```sh
 unzip gitea-dump-1610949662.zip
 cd gitea-dump-1610949662
-mv data/conf/app.ini /etc/gitea/conf/app.ini
+mv app.ini /etc/gitea/conf/app.ini
 mv data/* /var/lib/gitea/data/
 mv log/* /var/lib/gitea/log/
-mv repos/* /var/lib/gitea/repositories/
+mv repos/* /var/lib/gitea/gitea-repositories/
 chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea
 
 # mysql
@@ -66,3 +100,55 @@ psql -U $USER -d $DATABASE < gitea-db.sql
 
 service gitea restart
 ```
+
+如果安装方式发生了变化(例如 二进制 -> Docker),或者 Gitea 安装到了与之前安装不同的目录,则需要重新生成仓库 Git 钩子。
+
+在 Gitea 运行时,并从 Gitea 二进制文件所在的目录执行:`./gitea admin regenerate hooks`
+
+这样可以确保仓库 Git 钩子中的应用程序和配置文件路径与当前安装一致。如果这些路径没有更新,仓库的 `push` 操作将失败。
+
+### 使用 Docker (`restore`)
+
+在基于 Docker 的 Gitea 实例中,也没有恢复命令的支持。恢复过程与前面描述的步骤相同,但路径不同。
+
+示例:
+
+```sh
+# 在容器中打开 bash 会话
+docker exec --user git -it 2a83b293548e bash
+# 在容器内解压您的备份文件
+unzip gitea-dump-1610949662.zip
+cd gitea-dump-1610949662
+# 恢复 Gitea 数据
+mv data/* /data/gitea
+# 恢复仓库本身
+mv repos/* /data/git/gitea-repositories/
+# 调整文件权限
+chown -R git:git /data
+# 重新生成 Git 钩子
+/usr/local/bin/gitea -c '/data/gitea/conf/app.ini' admin regenerate hooks
+```
+
+Gitea 容器中的默认用户是 `git`(1000:1000)。请用您的 Gitea 容器 ID 或名称替换 `2a83b293548e`。
+
+### 使用 Docker-rootless (`restore`)
+
+在 Docker-rootless 容器中的恢复工作流程只是要使用的目录不同:
+
+```sh
+# 在容器中打开 bash 会话
+docker exec --user git -it 2a83b293548e bash
+# 在容器内解压您的备份文件
+unzip gitea-dump-1610949662.zip
+cd gitea-dump-1610949662
+# 恢复 app.ini
+mv data/conf/app.ini /etc/gitea/app.ini
+# 恢复 Gitea 数据
+mv data/* /var/lib/gitea
+# 恢复仓库本身
+mv repos/* /var/lib/gitea/git/gitea-repositories
+# 调整文件权限
+chown -R git:git /etc/gitea/app.ini /var/lib/gitea
+# 重新生成 Git 钩子
+/usr/local/bin/gitea -c '/etc/gitea/app.ini' admin regenerate hooks
+```
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index 66a4d4b4d6..365144ee7c 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -48,6 +48,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
 11. 推荐使用自定义事件名称前缀`ce-`。
 12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。
+13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
 
 ### 可访问性 / ARIA
 
@@ -64,18 +65,21 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari
 
 * Vue + Vanilla JS
 * Fomantic-UI(jQuery)
+* htmx (部分页面重新加载其他静态组件)
 * Vanilla JS
 
 不推荐的实现方式:
 
 * Vue + Fomantic-UI(jQuery)
 * jQuery + Vanilla JS
+* htmx + 任何其他需要大量 JavaScript 代码或不必要的功能,如 htmx 脚本 (`hx-on`)
 
 为了保持界面一致,Vue 组件可以使用 Fomantic-UI 的 CSS 类。
 尽管不建议混合使用不同的框架,
+我们使用 htmx 进行简单的交互。您可以在此 [PR](https://github.com/go-gitea/gitea/pull/28908) 中查看一个简单交互的示例,其中应使用 htmx。如果您需要更高级的反应性,请不要使用 htmx,请使用其他框架(Vue/Vanilla JS)。
 但如果混合使用是必要的,并且代码设计良好且易于维护,也可以工作。
 
-### async 函数
+### `async` 函数
 
 只有当函数内部存在`await`调用或返回`Promise`时,才将函数标记为`async`。
 
@@ -91,6 +95,12 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari
 这是有意为之的,我们想调用异步函数并忽略Promise。
 一些 lint 规则和 IDE 也会在未处理返回的 Promise 时发出警告。
 
+### 获取数据
+
+要获取数据,请使用`modules/fetch.js`中的包装函数`GET`、`POST`等。他们
+接受内容的`data`选项,将自动设置 CSRF 令牌并返回
+[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)。
+
 ### HTML 属性和 dataset
 
 禁止使用`dataset`,它的驼峰命名行为使得搜索属性变得困难。
@@ -132,3 +142,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari
 ### Vue3 和 JSX
 
 Gitea 现在正在使用 Vue3。我们决定不引入 JSX,以保持 HTML 代码和 JavaScript 代码分离。
+
+### UI示例
+
+Gitea 使用一些自制的 UI 元素并自定义其他元素,以将它们更好地集成到通用 UI 方法中。当在开发模式(`RUN_MODE=dev`)下运行 Gitea 时,在 `http(s)://your-gitea-url:port/devtest` 下会提供一个包含一些标准化 UI 示例的页面。
diff --git a/docs/content/help/support.zh-cn.md b/docs/content/help/support.zh-cn.md
index de56d8abe0..91b37c586c 100644
--- a/docs/content/help/support.zh-cn.md
+++ b/docs/content/help/support.zh-cn.md
@@ -15,11 +15,64 @@ menu:
     identifier: "support"
 ---
 
-## 需要帮助?
+# 支持选项
 
-如果您在使用或者开发过程中遇到问题,请到以下渠道咨询:
+- [付费商业支持](https://about.gitea.com/)
+- [Discord](https://discord.gg/Gitea)
+- [Discourse 论坛](https://discourse.gitea.io/)
+- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
+  - 注意:大多数 Matrix 频道都与 Discord 中的对应频道桥接,可能在桥接过程中会出现一定程度的不稳定性。
+- 中文支持
+  - [Discourse 中文分类](https://discourse.gitea.io/c/5-category/5)
+  - QQ 群 328432459
 
-- 到 [GitHub Issue](https://github.com/go-gitea/gitea/issues) 提问(因为项目维护人员来自世界各地,为保证沟通顺畅,请使用英文提问)
-- 中文问题到 [Gitea 论坛](https://discourse.gitea.io/c/5-category/5) 提问
-- 访问 [Discord Gitea 聊天室 - 英文](https://discord.gg/Gitea)
-- 加入 QQ群 328432459 获得进一步的支持
+# Bug 报告
+
+如果您发现了 Bug,请在 GitHub 上 [创建一个问题](https://github.com/go-gitea/gitea/issues)。
+
+**注意:** 在请求支持时,可能需要准备以下信息,以便帮助者获得所需的所有信息:
+
+1. 您的 `app.ini`(将任何敏感数据进行必要的清除)。
+2. 您看到的任何错误消息。
+3. Gitea 日志以及与情况相关的所有其他日志。
+   - 收集 `trace` / `debug` 级别的日志更有用(参见下一节)。
+   - 在使用 systemd 时,使用 `journalctl --lines 1000 --unit gitea` 收集日志。
+   - 在使用 Docker 时,使用 `docker logs --tail 1000 <gitea-container>` 收集日志。
+4. 可重现的步骤,以便他人能够更快速、更容易地重现和理解问题。
+   - [try.gitea.io](https://try.gitea.io) 可用于重现问题。
+5. 如果遇到慢速/挂起/死锁等问题,请在出现问题时报告堆栈跟踪。
+   转到 "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report"。
+
+# 高级 Bug 报告提示
+
+## 更多日志的配置选项
+
+默认情况下,日志以 `info` 级别输出到控制台。
+如果您需要设置日志级别和/或从文件中收集日志,
+您只需将以下配置复制到您的 `app.ini` 中(删除所有其他 `[log]` 部分),
+然后您将在 Gitea 的日志目录中找到 `*.log` 文件(默认为 `%(GITEA_WORK_DIR)/log`)。
+
+```ini
+; 要显示所有 SQL 日志,您还可以在 [database] 部分中设置 LOG_SQL=true
+[log]
+LEVEL=debug
+MODE=console,file
+```
+
+## 使用命令行收集堆栈跟踪
+
+Gitea 可以使用 Golang 的 pprof 处理程序和工具链来收集堆栈跟踪和其他运行时信息。
+
+如果 Web UI 停止工作,您可以尝试通过命令行收集堆栈跟踪:
+
+1. 设置 app.ini:
+
+    ```
+    [server]
+    ENABLE_PPROF = true
+    ```
+
+2. 重新启动 Gitea
+
+3. 尝试触发bug,当请求卡住一段时间,使用或浏览器访问:获取堆栈跟踪。
+`curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=1`
diff --git a/docs/content/installation/database-preparation.zh-cn.md b/docs/content/installation/database-preparation.zh-cn.md
index d651088395..3fde004a8c 100644
--- a/docs/content/installation/database-preparation.zh-cn.md
+++ b/docs/content/installation/database-preparation.zh-cn.md
@@ -17,7 +17,9 @@ menu:
 
 # 数据库准备
 
-在使用 Gitea 前,您需要准备一个数据库。Gitea 支持 PostgreSQL(>= 12)、MySQL(>= 8.0)、SQLite 和 MSSQL(>= 2012 SP4)这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite,则可以忽略本章内容。
+在使用 Gitea 前,您需要准备一个数据库。Gitea 支持 PostgreSQL(>= 12)、MySQL(>= 8.0)、MariaDB(>= 10.4)、SQLite(内置) 和 MSSQL(>= 2012 SP4)这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite,则可以忽略本章内容。
+
+如果您使用不受支持的数据库版本,请通过 [联系我们](/help/support) 以获取有关我们的扩展支持的信息。我们可以为旧数据库提供测试和支持,并将这些修复集成到 Gitea 代码库中。
 
 数据库实例可以与 Gitea 实例在相同机器上(本地数据库),也可以与 Gitea 实例在不同机器上(远程数据库)。
 
@@ -61,7 +63,9 @@ menu:
 
 4. 使用 UTF-8 字符集和大小写敏感的排序规则创建数据库。
 
-    Gitea 启动后会尝试把数据库修改为更合适的字符集,如果你想指定自己的字符集规则,可以在 app.ini 中设置 `[database].CHARSET_COLLATION`。
+    `utf8mb4_bin` 是 MySQL/MariaDB 的通用排序规则。
+    Gitea 启动后会尝试把数据库修改为更合适的字符集 (`utf8mb4_0900_as_cs` 或者 `uca1400_as_cs`) 并在可能的情况下更改数据库。
+    如果你想指定自己的字符集规则,可以在 `app.ini` 中设置 `[database].CHARSET_COLLATION`。
 
     ```sql
     CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin';
@@ -85,7 +89,7 @@ menu:
     FLUSH PRIVILEGES;
     ```
 
-6. 通过 exit 退出数据库控制台。
+6. 通过 `exit` 退出数据库控制台。
 
 7. 在您的 Gitea 服务器上,测试与数据库的连接:
 
@@ -93,13 +97,13 @@ menu:
     mysql -u gitea -h 203.0.113.3 -p giteadb
     ```
 
-    其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 -h 选项。
+    其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 `-h` 选项。
 
     到此您应该能够连接到数据库了。
 
 ## PostgreSQL
 
-1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 listen_addresses 将 PostgreSQL 配置为监听您的 IP 地址:
+1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 `listen_addresses` 将 `PostgreSQL` 配置为监听您的 IP 地址:
 
     ```ini
     listen_addresses = 'localhost, 203.0.113.3'
diff --git a/docs/content/installation/windows-service.zh-cn.md b/docs/content/installation/windows-service.zh-cn.md
index 985492b7e8..d5761d2c19 100644
--- a/docs/content/installation/windows-service.zh-cn.md
+++ b/docs/content/installation/windows-service.zh-cn.md
@@ -15,7 +15,7 @@ menu:
     identifier: "windows-service"
 ---
 
-# 准备工作
+## 准备工作
 
 在 C:\gitea\custom\conf\app.ini 中进行了以下更改:
 
@@ -27,7 +27,7 @@ RUN_USER = COMPUTERNAME$
 
 COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应。如果响应是 `USER-PC`,那么 `RUN_USER = USER-PC$`。
 
-## 使用绝对路径
+### 使用绝对路径
 
 如果您使用 SQLite3,请将 `PATH` 更改为包含完整路径:
 
@@ -36,7 +36,7 @@ COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应
 PATH     = c:/gitea/data/gitea.db
 ```
 
-# 注册为Windows服务
+## 注册为Windows服务
 
 要注册为Windows服务,首先以Administrator身份运行 `cmd`,然后执行以下命令:
 
@@ -48,7 +48,16 @@ sc.exe create gitea start= auto binPath= "\"C:\gitea\gitea.exe\" web --config \"
 
 之后在控制面板打开 "Windows Services",搜索 "gitea",右键选择 "Run"。在浏览器打开 `http://localhost:3000` 就可以访问了。(如果你修改了端口,请访问对应的端口,3000是默认端口)。
 
-## 添加启动依赖项
+### 服务启动类型
+
+据观察,在启动期间加载的系统上,Gitea 服务可能无法启动,并在 Windows 事件日志中记录超时。
+在这种情况下,将启动类型更改为`Automatic-Delayed`。这可以在服务创建期间完成,或者通过运行配置命令来完成。
+
+```
+sc.exe config gitea start= delayed-auto
+```
+
+### 添加启动依赖项
 
 要将启动依赖项添加到 Gitea Windows 服务(例如 Mysql、Mariadb),作为管理员,然后运行以下命令:
 
diff --git a/docs/content/usage/profile-readme.zh-cn.md b/docs/content/usage/profile-readme.zh-cn.md
index 804f69d2e6..b69d4aa921 100644
--- a/docs/content/usage/profile-readme.zh-cn.md
+++ b/docs/content/usage/profile-readme.zh-cn.md
@@ -15,6 +15,10 @@ menu:
 
 # 个人资料 README
 
-要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 ".profile" 的仓库,并编辑其中的 README.md 文件。Gitea 将自动获取该文件并在您的仓库上方显示。
+要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 `.profile` 的仓库,并编辑其中的 `README.md` 文件。Gitea 将自动获取该文件并在您的仓库上方显示。
 
 注意:您可以将此仓库设为私有。这样可以隐藏您的源文件,使其对公众不可见,并允许您将某些文件设为私有。但是,README.md 文件将是您个人资料上唯一存在的文件。如果您希望完全私有化 .profile 仓库,则需删除或重命名 README.md 文件。
+
+用户示例 `.profile/README.md`:
+
+![个人资料自述文件截图](/images/usage/profile-readme.png)

From d75708736a2189e7fdbed60444e3bbeef1c5270a Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 12 Feb 2024 00:24:21 +0000
Subject: [PATCH 021/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_el-GR.ini | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 24bcd7244c..5204e88be1 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -843,6 +843,8 @@ oauth2_regenerate_secret=Αναδημιουργία Μυστικού
 oauth2_regenerate_secret_hint=Χάσατε το μυστικό σας;
 oauth2_application_edit=Επεξεργασία
 oauth2_application_create_description=Οι εφαρμογές OAuth2 δίνει πρόσβαση στην εξωτερική εφαρμογή σας σε λογαριασμούς χρηστών σε αυτή την υπηρεσία.
+oauth2_application_remove_description=Αφαιρώντας μια εφαρμογή OAuth2 θα αποτραπεί η πρόσβαση αυτής, σε εξουσιοδοτημένους λογαριασμούς χρηστών σε αυτή την υπηρεσία. Συνέχεια;
+oauth2_application_locked=Το Gitea κάνει προεγγραφή σε μερικές εφαρμογές OAuth2 κατά την εκκίνηση αν είναι ενεργοποιημένες στις ρυθμίσεις. Για την αποφυγή απροσδόκητης συμπεριφοράς, αυτές δεν μπορούν ούτε να επεξεργαστούν ούτε να καταργηθούν. Παρακαλούμε ανατρέξτε στην τεκμηρίωση OAuth2 για περισσότερες πληροφορίες.
 
 authorized_oauth2_applications=Εξουσιοδοτημένες Εφαρμογές OAuth2
 revoke_key=Ανάκληση

From ee242a08e98f5d754b5ed7846f6c7847bbf5d3da Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 12 Feb 2024 13:04:10 +0800
Subject: [PATCH 022/679] Refactor issue template parsing and fix API endpoint
 (#29069)

The old code `GetTemplatesFromDefaultBranch(...) ([]*api.IssueTemplate,
map[string]error)` doesn't really follow Golang's habits, then the
second returned value might be misused. For example, the API function
`GetIssueTemplates` incorrectly checked the second returned value and
always responds 500 error.

This PR refactors GetTemplatesFromDefaultBranch to
ParseTemplatesFromDefaultBranch and clarifies its behavior, and fixes the
API endpoint bug, and adds some tests.

And by the way, add proper prefix `X-` for the header generated in
`checkDeprecatedAuthMethods`, because non-standard HTTP headers should
have `X-` prefix, and it is also consistent with the new code in
`GetIssueTemplates`
---
 routers/api/v1/api.go                         |  2 +-
 routers/api/v1/repo/repo.go                   | 10 ++--
 routers/web/repo/issue.go                     | 16 +++---
 routers/web/repo/milestone.go                 |  4 +-
 services/issue/template.go                    | 30 +++++-----
 tests/integration/api_issue_templates_test.go | 55 +++++++++++++++++++
 6 files changed, 87 insertions(+), 30 deletions(-)
 create mode 100644 tests/integration/api_issue_templates_test.go

diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index cb1803f7c6..f3082e4fa0 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -811,7 +811,7 @@ func individualPermsChecker(ctx *context.APIContext) {
 // check for and warn against deprecated authentication options
 func checkDeprecatedAuthMethods(ctx *context.APIContext) {
 	if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" {
-		ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
+		ctx.Resp.Header().Set("X-Gitea-Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
 	}
 }
 
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 2efdccb569..d1b2c99d0c 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"net/http"
 	"slices"
+	"strconv"
 	"strings"
 	"time"
 
@@ -1161,12 +1162,11 @@ func GetIssueTemplates(ctx *context.APIContext) {
 	//     "$ref": "#/responses/IssueTemplates"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
-	ret, err := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetTemplatesFromDefaultBranch", err)
-		return
+	ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	if cnt := len(ret.TemplateErrors); cnt != 0 {
+		ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt))
 	}
-	ctx.JSON(http.StatusOK, ret)
+	ctx.JSON(http.StatusOK, ret.IssueTemplates)
 }
 
 // GetIssueConfig returns the issue config for a repo
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index c8c9924a9e..aa0cad98b7 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -993,17 +993,17 @@ func NewIssue(ctx *context.Context) {
 	}
 	ctx.Data["Tags"] = tags
 
-	_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
 	templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
 	for k, v := range errs {
-		templateErrs[k] = v
+		ret.TemplateErrors[k] = v
 	}
 	if ctx.Written() {
 		return
 	}
 
-	if len(templateErrs) > 0 {
-		ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+	if len(ret.TemplateErrors) > 0 {
+		ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
 	}
 
 	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
@@ -1046,11 +1046,11 @@ func NewIssueChooseTemplate(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
 	ctx.Data["PageIsIssueList"] = true
 
-	issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
-	ctx.Data["IssueTemplates"] = issueTemplates
+	ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	ctx.Data["IssueTemplates"] = ret.IssueTemplates
 
-	if len(errs) > 0 {
-		ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
+	if len(ret.TemplateErrors) > 0 {
+		ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
 	}
 
 	if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) {
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 19db2abd68..400748b963 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -294,8 +294,8 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
 
 	issues(ctx, milestoneID, projectID, util.OptionalBoolNone)
 
-	ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
-	ctx.Data["NewIssueChooseTemplate"] = len(ret) > 0
+	ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
 
 	ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
 	ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
diff --git a/services/issue/template.go b/services/issue/template.go
index b6ae077987..dd9d015f0f 100644
--- a/services/issue/template.go
+++ b/services/issue/template.go
@@ -109,21 +109,23 @@ func IsTemplateConfig(path string) bool {
 	return false
 }
 
-// GetTemplatesFromDefaultBranch checks for issue templates in the repo's default branch,
-// returns valid templates and the errors of invalid template files.
-func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) ([]*api.IssueTemplate, map[string]error) {
-	var issueTemplates []*api.IssueTemplate
-
+// ParseTemplatesFromDefaultBranch parses the issue templates in the repo's default branch,
+// returns valid templates and the errors of invalid template files (the errors map is guaranteed to be non-nil).
+func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (ret struct {
+	IssueTemplates []*api.IssueTemplate
+	TemplateErrors map[string]error
+},
+) {
+	ret.TemplateErrors = map[string]error{}
 	if repo.IsEmpty {
-		return issueTemplates, nil
+		return ret
 	}
 
 	commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
 	if err != nil {
-		return issueTemplates, nil
+		return ret
 	}
 
-	invalidFiles := map[string]error{}
 	for _, dirName := range templateDirCandidates {
 		tree, err := commit.SubTree(dirName)
 		if err != nil {
@@ -133,7 +135,7 @@ func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repositor
 		entries, err := tree.ListEntries()
 		if err != nil {
 			log.Debug("list entries in %s: %v", dirName, err)
-			return issueTemplates, nil
+			return ret
 		}
 		for _, entry := range entries {
 			if !template.CouldBe(entry.Name()) {
@@ -141,16 +143,16 @@ func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repositor
 			}
 			fullName := path.Join(dirName, entry.Name())
 			if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
-				invalidFiles[fullName] = err
+				ret.TemplateErrors[fullName] = err
 			} else {
 				if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
 					it.Ref = git.BranchPrefix + it.Ref
 				}
-				issueTemplates = append(issueTemplates, it)
+				ret.IssueTemplates = append(ret.IssueTemplates, it)
 			}
 		}
 	}
-	return issueTemplates, invalidFiles
+	return ret
 }
 
 // GetTemplateConfigFromDefaultBranch returns the issue config for this repo.
@@ -179,8 +181,8 @@ func GetTemplateConfigFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repo
 }
 
 func HasTemplatesOrContactLinks(repo *repo.Repository, gitRepo *git.Repository) bool {
-	ret, _ := GetTemplatesFromDefaultBranch(repo, gitRepo)
-	if len(ret) > 0 {
+	ret := ParseTemplatesFromDefaultBranch(repo, gitRepo)
+	if len(ret.IssueTemplates) > 0 {
 		return true
 	}
 
diff --git a/tests/integration/api_issue_templates_test.go b/tests/integration/api_issue_templates_test.go
new file mode 100644
index 0000000000..6b65e6e086
--- /dev/null
+++ b/tests/integration/api_issue_templates_test.go
@@ -0,0 +1,55 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"net/url"
+	"testing"
+
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAPIIssueTemplateList(t *testing.T) {
+	onGiteaRun(t, func(*testing.T, *url.URL) {
+		var issueTemplates []*api.IssueTemplate
+
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
+
+		// no issue template
+		req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates")
+		resp := MakeRequest(t, req, http.StatusOK)
+		issueTemplates = nil
+		DecodeJSON(t, resp, &issueTemplates)
+		assert.Empty(t, issueTemplates)
+
+		// one correct issue template and some incorrect issue templates
+		err := createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-ok.md", repo.DefaultBranch, `----
+name: foo
+about: bar
+----
+`)
+		assert.NoError(t, err)
+
+		err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err1.yml", repo.DefaultBranch, `name: '`)
+		assert.NoError(t, err)
+
+		err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err2.yml", repo.DefaultBranch, `other: `)
+		assert.NoError(t, err)
+
+		req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates")
+		resp = MakeRequest(t, req, http.StatusOK)
+		issueTemplates = nil
+		DecodeJSON(t, resp, &issueTemplates)
+		assert.Len(t, issueTemplates, 1)
+		assert.Equal(t, "foo", issueTemplates[0].Name)
+		assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning"))
+	})
+}

From f9c3459831659d37fd885dd1a9db32dcf19420e4 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Mon, 12 Feb 2024 23:14:24 +0200
Subject: [PATCH 023/679] Use Markdown alert syntax for notes in README
 (#29150)

- It looks nicer
- This syntax is supported on both GitHub and Gitea

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 README.md | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index 5356fcfacd..174e37769c 100644
--- a/README.md
+++ b/README.md
@@ -97,17 +97,17 @@ More info: https://docs.gitea.com/installation/install-from-source
 
     ./gitea web
 
-NOTE: If you're interested in using our APIs, we have experimental
-support with [documentation](https://try.gitea.io/api/swagger).
+> [!NOTE]
+> If you're interested in using our APIs, we have experimental support with [documentation](https://try.gitea.io/api/swagger).
 
 ## Contributing
 
 Expected workflow is: Fork -> Patch -> Push -> Pull Request
 
-NOTES:
-
-1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
-2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
+> [!NOTE]
+>
+> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
+> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
 
 ## Translating
 

From 47b59658629f47e0ac559559a305b867740cae9c Mon Sep 17 00:00:00 2001
From: Chris Copeland <chris@chrisnc.net>
Date: Mon, 12 Feb 2024 14:37:23 -0800
Subject: [PATCH 024/679] Add merge style `fast-forward-only` (#28954)

With this option, it is possible to require a linear commit history with
the following benefits over the next best option `Rebase+fast-forward`:
The original commits continue existing, with the original signatures
continuing to stay valid instead of being rewritten, there is no merge
commit, and reverting commits becomes easier.

Closes #24906
---
 custom/conf/app.example.ini                   |  2 +-
 .../config-cheat-sheet.en-us.md               |  2 +-
 .../config-cheat-sheet.zh-cn.md               |  2 +-
 models/error.go                               | 17 ++++
 models/repo/git.go                            |  2 +
 models/repo/repo_unit.go                      |  2 +
 modules/git/error.go                          |  2 +-
 modules/repository/create.go                  |  6 +-
 modules/structs/repo.go                       |  5 +-
 options/locale/locale_en-US.ini               |  1 +
 routers/api/v1/repo/repo.go                   |  4 +
 routers/api/v1/repo/repo_test.go              |  2 +
 routers/web/repo/issue.go                     |  2 +
 routers/web/repo/setting/setting.go           |  1 +
 services/convert/repository.go                |  3 +
 services/forms/repo_form.go                   |  5 +-
 services/pull/merge.go                        | 11 +++
 services/pull/merge_ff_only.go                | 21 +++++
 services/pull/merge_merge.go                  |  2 +-
 templates/repo/issue/view_content/pull.tmpl   |  9 +-
 .../view_content/pull_merge_instruction.tmpl  |  4 +
 templates/repo/settings/options.tmpl          | 11 +++
 templates/swagger/v1_json.tmpl                | 12 ++-
 tests/integration/api_repo_edit_test.go       |  3 +
 tests/integration/pull_merge_test.go          | 84 +++++++++++++++++++
 25 files changed, 204 insertions(+), 11 deletions(-)
 create mode 100644 services/pull/merge_ff_only.go

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 363bbcb151..4aae1c497f 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1044,7 +1044,7 @@ LEVEL = Info
 ;; List of keywords used in Pull Request comments to automatically reopen a related issue
 ;REOPEN_KEYWORDS = reopen,reopens,reopened
 ;;
-;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash
+;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash, fast-forward-only
 ;DEFAULT_MERGE_STYLE = merge
 ;;
 ;; In the default merge message for squash commits include at most this many commits
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 33732d080b..415176d4ff 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -126,7 +126,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build
  keywords used in Pull Request comments to automatically close a related issue
 - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen
  a related issue
-- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`
+- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only`
 - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits
 - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit. Only used if `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES` is `true`.
 - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 8236852ad3..01906930cb 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -125,7 +125,7 @@ menu:
 - `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: 在拉取请求评论中用于自动关闭相关问题的关键词列表。
 - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: 在拉取请求评论中用于自动重新打开相关问题的
 关键词列表。
-- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`
+- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only`
 - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: 在默认合并消息中,对于`squash`提交,最多包括此数量的提交。设置为 -1 以包括所有提交。
 - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: 在默认的合并消息中,对于`squash`提交,限制提交消息的大小。设置为 `-1`以取消限制。仅在`POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`为`true`时使用。
 - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: 在默认合并消息中,对于`squash`提交,遍历所有提交以包括所有作者的`Co-authored-by`,否则仅使用限定列表中的作者。
diff --git a/models/error.go b/models/error.go
index 83dfe29805..75c53245de 100644
--- a/models/error.go
+++ b/models/error.go
@@ -493,6 +493,23 @@ func (err ErrMergeUnrelatedHistories) Error() string {
 	return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
 }
 
+// ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge
+type ErrMergeDivergingFastForwardOnly struct {
+	StdOut string
+	StdErr string
+	Err    error
+}
+
+// IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly.
+func IsErrMergeDivergingFastForwardOnly(err error) bool {
+	_, ok := err.(ErrMergeDivergingFastForwardOnly)
+	return ok
+}
+
+func (err ErrMergeDivergingFastForwardOnly) Error() string {
+	return fmt.Sprintf("Merge DivergingFastForwardOnly Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
+}
+
 // ErrRebaseConflicts represents an error if rebase fails with a conflict
 type ErrRebaseConflicts struct {
 	Style     repo_model.MergeStyle
diff --git a/models/repo/git.go b/models/repo/git.go
index 610c554296..388bf86522 100644
--- a/models/repo/git.go
+++ b/models/repo/git.go
@@ -21,6 +21,8 @@ const (
 	MergeStyleRebaseMerge MergeStyle = "rebase-merge"
 	// MergeStyleSquash squash commits into single commit before merging
 	MergeStyleSquash MergeStyle = "squash"
+	// MergeStyleFastForwardOnly fast-forward merge if possible, otherwise fail
+	MergeStyleFastForwardOnly MergeStyle = "fast-forward-only"
 	// MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
 	MergeStyleManuallyMerged MergeStyle = "manually-merged"
 	// MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 8a3ba1ee89..31a2a2e248 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -122,6 +122,7 @@ type PullRequestsConfig struct {
 	AllowRebase                   bool
 	AllowRebaseMerge              bool
 	AllowSquash                   bool
+	AllowFastForwardOnly          bool
 	AllowManualMerge              bool
 	AutodetectManualMerge         bool
 	AllowRebaseUpdate             bool
@@ -148,6 +149,7 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
 		mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
 		mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
 		mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
+		mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly ||
 		mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
 }
 
diff --git a/modules/git/error.go b/modules/git/error.go
index dc10d451b3..91d25eca69 100644
--- a/modules/git/error.go
+++ b/modules/git/error.go
@@ -96,7 +96,7 @@ func (err ErrBranchNotExist) Unwrap() error {
 	return util.ErrNotExist
 }
 
-// ErrPushOutOfDate represents an error if merging fails due to unrelated histories
+// ErrPushOutOfDate represents an error if merging fails due to the base branch being updated
 type ErrPushOutOfDate struct {
 	StdOut string
 	StdErr string
diff --git a/modules/repository/create.go b/modules/repository/create.go
index 7c954a1412..ca2150b972 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -87,7 +87,11 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
 			units = append(units, repo_model.RepoUnit{
 				RepoID: repo.ID,
 				Type:   tp,
-				Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true},
+				Config: &repo_model.PullRequestsConfig{
+					AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
+					DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
+					AllowRebaseUpdate: true,
+				},
 			})
 		} else {
 			units = append(units, repo_model.RepoUnit{
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 51e175fba8..56d6158bd8 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -98,6 +98,7 @@ type Repository struct {
 	AllowRebase                   bool             `json:"allow_rebase"`
 	AllowRebaseMerge              bool             `json:"allow_rebase_explicit"`
 	AllowSquash                   bool             `json:"allow_squash_merge"`
+	AllowFastForwardOnly          bool             `json:"allow_fast_forward_only_merge"`
 	AllowRebaseUpdate             bool             `json:"allow_rebase_update"`
 	DefaultDeleteBranchAfterMerge bool             `json:"default_delete_branch_after_merge"`
 	DefaultMergeStyle             string           `json:"default_merge_style"`
@@ -195,6 +196,8 @@ type EditRepoOption struct {
 	AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"`
 	// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.
 	AllowSquash *bool `json:"allow_squash_merge,omitempty"`
+	// either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.
+	AllowFastForwardOnly *bool `json:"allow_fast_forward_only_merge,omitempty"`
 	// either `true` to allow mark pr as merged manually, or `false` to prevent it.
 	AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
 	// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
@@ -203,7 +206,7 @@ type EditRepoOption struct {
 	AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
 	// set to `true` to delete pr branch after merge by default
 	DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
-	// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash".
+	// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
 	DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
 	// set to `true` to allow edits from maintainers by default
 	DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9af4d70171..96345f51f8 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1775,6 +1775,7 @@ pulls.merge_pull_request = Create merge commit
 pulls.rebase_merge_pull_request = Rebase then fast-forward
 pulls.rebase_merge_commit_pull_request = Rebase then create merge commit
 pulls.squash_merge_pull_request = Create squash commit
+pulls.fast_forward_only_merge_pull_request = Fast-forward only
 pulls.merge_manually = Manually merged
 pulls.merge_commit_id = The merge commit ID
 pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index d1b2c99d0c..40de8853d8 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -885,6 +885,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 					AllowRebase:                   true,
 					AllowRebaseMerge:              true,
 					AllowSquash:                   true,
+					AllowFastForwardOnly:          true,
 					AllowManualMerge:              true,
 					AutodetectManualMerge:         false,
 					AllowRebaseUpdate:             true,
@@ -911,6 +912,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 			if opts.AllowSquash != nil {
 				config.AllowSquash = *opts.AllowSquash
 			}
+			if opts.AllowFastForwardOnly != nil {
+				config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
+			}
 			if opts.AllowManualMerge != nil {
 				config.AllowManualMerge = *opts.AllowManualMerge
 			}
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
index 29e2d1f21d..08ba7fabac 100644
--- a/routers/api/v1/repo/repo_test.go
+++ b/routers/api/v1/repo/repo_test.go
@@ -35,6 +35,7 @@ func TestRepoEdit(t *testing.T) {
 	allowRebase := false
 	allowRebaseMerge := false
 	allowSquashMerge := false
+	allowFastForwardOnlyMerge := false
 	archived := true
 	opts := api.EditRepoOption{
 		Name:                      &ctx.Repo.Repository.Name,
@@ -50,6 +51,7 @@ func TestRepoEdit(t *testing.T) {
 		AllowRebase:               &allowRebase,
 		AllowRebaseMerge:          &allowRebaseMerge,
 		AllowSquash:               &allowSquashMerge,
+		AllowFastForwardOnly:      &allowFastForwardOnlyMerge,
 		Archived:                  &archived,
 	}
 
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index aa0cad98b7..a85f6e7666 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1862,6 +1862,8 @@ func ViewIssue(ctx *context.Context) {
 				mergeStyle = repo_model.MergeStyleRebaseMerge
 			} else if prConfig.AllowSquash {
 				mergeStyle = repo_model.MergeStyleSquash
+			} else if prConfig.AllowFastForwardOnly {
+				mergeStyle = repo_model.MergeStyleFastForwardOnly
 			} else if prConfig.AllowManualMerge {
 				mergeStyle = repo_model.MergeStyleManuallyMerged
 			}
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 8c1daf52bc..3b11638a92 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -576,6 +576,7 @@ func SettingsPost(ctx *context.Context) {
 					AllowRebase:                   form.PullsAllowRebase,
 					AllowRebaseMerge:              form.PullsAllowRebaseMerge,
 					AllowSquash:                   form.PullsAllowSquash,
+					AllowFastForwardOnly:          form.PullsAllowFastForwardOnly,
 					AllowManualMerge:              form.PullsAllowManualMerge,
 					AutodetectManualMerge:         form.EnableAutodetectManualMerge,
 					AllowRebaseUpdate:             form.PullsAllowRebaseUpdate,
diff --git a/services/convert/repository.go b/services/convert/repository.go
index c16180c0af..9184bc05c7 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -93,6 +93,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 	allowRebase := false
 	allowRebaseMerge := false
 	allowSquash := false
+	allowFastForwardOnly := false
 	allowRebaseUpdate := false
 	defaultDeleteBranchAfterMerge := false
 	defaultMergeStyle := repo_model.MergeStyleMerge
@@ -105,6 +106,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		allowRebase = config.AllowRebase
 		allowRebaseMerge = config.AllowRebaseMerge
 		allowSquash = config.AllowSquash
+		allowFastForwardOnly = config.AllowFastForwardOnly
 		allowRebaseUpdate = config.AllowRebaseUpdate
 		defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
 		defaultMergeStyle = config.GetDefaultMergeStyle()
@@ -219,6 +221,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		AllowRebase:                   allowRebase,
 		AllowRebaseMerge:              allowRebaseMerge,
 		AllowSquash:                   allowSquash,
+		AllowFastForwardOnly:          allowFastForwardOnly,
 		AllowRebaseUpdate:             allowRebaseUpdate,
 		DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
 		DefaultMergeStyle:             string(defaultMergeStyle),
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 845eccf817..60fa0ab363 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -151,6 +151,7 @@ type RepoSettingForm struct {
 	PullsAllowRebase                      bool
 	PullsAllowRebaseMerge                 bool
 	PullsAllowSquash                      bool
+	PullsAllowFastForwardOnly             bool
 	PullsAllowManualMerge                 bool
 	PullsDefaultMergeStyle                string
 	EnableAutodetectManualMerge           bool
@@ -598,8 +599,8 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
 // swagger:model MergePullRequestOption
 type MergePullRequestForm struct {
 	// required: true
-	// enum: merge,rebase,rebase-merge,squash,manually-merged
-	Do                     string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"`
+	// enum: merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged
+	Do                     string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"`
 	MergeTitleField        string
 	MergeMessageField      string
 	MergeCommitID          string // only used for manually-merged
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 63f0268beb..d4c0c821d6 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -267,6 +267,10 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
 		if err := doMergeStyleSquash(mergeCtx, message); err != nil {
 			return "", err
 		}
+	case repo_model.MergeStyleFastForwardOnly:
+		if err := doMergeStyleFastForwardOnly(mergeCtx); err != nil {
+			return "", err
+		}
 	default:
 		return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
 	}
@@ -377,6 +381,13 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g
 				StdErr: ctx.errbuf.String(),
 				Err:    err,
 			}
+		} else if mergeStyle == repo_model.MergeStyleFastForwardOnly && strings.Contains(ctx.errbuf.String(), "Not possible to fast-forward, aborting") {
+			log.Debug("MergeDivergingFastForwardOnly %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
+			return models.ErrMergeDivergingFastForwardOnly{
+				StdOut: ctx.outbuf.String(),
+				StdErr: ctx.errbuf.String(),
+				Err:    err,
+			}
 		}
 		log.Error("git merge %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
 		return fmt.Errorf("git merge %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
diff --git a/services/pull/merge_ff_only.go b/services/pull/merge_ff_only.go
new file mode 100644
index 0000000000..f57c732104
--- /dev/null
+++ b/services/pull/merge_ff_only.go
@@ -0,0 +1,21 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+)
+
+// doMergeStyleFastForwardOnly merges the tracking into the current HEAD - which is assumed to be staging branch (equal to the pr.BaseBranch)
+func doMergeStyleFastForwardOnly(ctx *mergeContext) error {
+	cmd := git.NewCommand(ctx, "merge", "--ff-only").AddDynamicArguments(trackingBranch)
+	if err := runMergeCommand(ctx, repo_model.MergeStyleFastForwardOnly, cmd); err != nil {
+		log.Error("%-v Unable to merge tracking into base: %v", ctx.pr, err)
+		return err
+	}
+
+	return nil
+}
diff --git a/services/pull/merge_merge.go b/services/pull/merge_merge.go
index 0f7664297a..bf56c071db 100644
--- a/services/pull/merge_merge.go
+++ b/services/pull/merge_merge.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 )
 
-// doMergeStyleMerge merges the tracking into the current HEAD - which is assumed to tbe staging branch (equal to the pr.BaseBranch)
+// doMergeStyleMerge merges the tracking branch into the current HEAD - which is assumed to be the staging branch (equal to the pr.BaseBranch)
 func doMergeStyleMerge(ctx *mergeContext, message string) error {
 	cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch)
 	if err := runMergeCommand(ctx, repo_model.MergeStyleMerge, cmd); err != nil {
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 2b5776ea03..f1ab53eb67 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -197,7 +197,7 @@
 				{{if .AllowMerge}} {{/* user is allowed to merge */}}
 					{{$prUnit := .Repository.MustGetUnit $.Context $.UnitTypePullRequests}}
 					{{$approvers := (.Issue.PullRequest.GetApprovers ctx)}}
-					{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}}
+					{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash $prUnit.PullRequestsConfig.AllowFastForwardOnly}}
 						{{$hasPendingPullRequestMergeTip := ""}}
 						{{if .HasPendingPullRequestMerge}}
 							{{$createdPRMergeStr := TimeSinceUnix .PendingPullRequestMerge.CreatedUnix ctx.Locale}}
@@ -268,6 +268,13 @@
 									'mergeMessageFieldText': {{.GetCommitMessages}} + defaultSquashMergeMessage,
 									'hideAutoMerge': generalHideAutoMerge,
 								},
+								{
+									'name': 'fast-forward-only',
+									'allowed': {{and $prUnit.PullRequestsConfig.AllowFastForwardOnly (eq .Issue.PullRequest.CommitsBehind 0)}},
+									'textDoMerge': {{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}},
+									'hideMergeMessageTexts': true,
+									'hideAutoMerge': generalHideAutoMerge,
+								},
 								{
 									'name': 'manually-merged',
 									'allowed': {{$prUnit.PullRequestsConfig.AllowManualMerge}},
diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
index 3dab44710e..a214f29786 100644
--- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl
+++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
@@ -35,6 +35,10 @@
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --squash {{$localBranch}}</div>
 		</div>
+		<div class="gt-hidden" data-pull-merge-style="fast-forward-only">
+			<div>git checkout {{.PullRequest.BaseBranch}}</div>
+			<div>git merge --ff-only {{$localBranch}}</div>
+		</div>
 		<div class="gt-hidden" data-pull-merge-style="manually-merged">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge {{$localBranch}}</div>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index dfb909e743..f7f448fdf2 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -528,6 +528,12 @@
 								<label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label>
 							</div>
 						</div>
+						<div class="field">
+							<div class="ui checkbox">
+								<input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}>
+								<label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label>
+							</div>
+						</div>
 						<div class="field">
 							<div class="ui checkbox">
 								<input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}>
@@ -545,6 +551,7 @@
 									<option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option>
 									<option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option>
 									<option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option>
+									<option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option>
 								</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 								<div class="default text">
 									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}
@@ -559,12 +566,16 @@
 									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}
 										{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}
 									{{end}}
+									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}
+										{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}
+									{{end}}
 								</div>
 								<div class="menu">
 									<div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div>
 									<div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
 									<div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
 									<div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div>
+									<div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div>
 								</div>
 							</div>
 						</div>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 403f241d72..a881afaf0e 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -19195,6 +19195,11 @@
       "description": "EditRepoOption options when editing a repository's properties",
       "type": "object",
       "properties": {
+        "allow_fast_forward_only_merge": {
+          "description": "either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.",
+          "type": "boolean",
+          "x-go-name": "AllowFastForwardOnly"
+        },
         "allow_manual_merge": {
           "description": "either `true` to allow mark pr as merged manually, or `false` to prevent it.",
           "type": "boolean",
@@ -19251,7 +19256,7 @@
           "x-go-name": "DefaultDeleteBranchAfterMerge"
         },
         "default_merge_style": {
-          "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", or \"squash\".",
+          "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", \"squash\", or \"fast-forward-only\".",
           "type": "string",
           "x-go-name": "DefaultMergeStyle"
         },
@@ -20650,6 +20655,7 @@
             "rebase",
             "rebase-merge",
             "squash",
+            "fast-forward-only",
             "manually-merged"
           ]
         },
@@ -22036,6 +22042,10 @@
       "description": "Repository represents a repository",
       "type": "object",
       "properties": {
+        "allow_fast_forward_only_merge": {
+          "type": "boolean",
+          "x-go-name": "AllowFastForwardOnly"
+        },
         "allow_merge_commits": {
           "type": "boolean",
           "x-go-name": "AllowMerge"
diff --git a/tests/integration/api_repo_edit_test.go b/tests/integration/api_repo_edit_test.go
index c4fc2177b4..7de8910ee0 100644
--- a/tests/integration/api_repo_edit_test.go
+++ b/tests/integration/api_repo_edit_test.go
@@ -65,6 +65,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
 	allowRebase := false
 	allowRebaseMerge := false
 	allowSquash := false
+	allowFastForwardOnly := false
 	if unit, err := repo.GetUnit(db.DefaultContext, unit_model.TypePullRequests); err == nil {
 		config := unit.PullRequestsConfig()
 		hasPullRequests = true
@@ -73,6 +74,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
 		allowRebase = config.AllowRebase
 		allowRebaseMerge = config.AllowRebaseMerge
 		allowSquash = config.AllowSquash
+		allowFastForwardOnly = config.AllowFastForwardOnly
 	}
 	archived := repo.IsArchived
 	return &api.EditRepoOption{
@@ -92,6 +94,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
 		AllowRebase:               &allowRebase,
 		AllowRebaseMerge:          &allowRebaseMerge,
 		AllowSquash:               &allowSquash,
+		AllowFastForwardOnly:      &allowFastForwardOnly,
 		Archived:                  &archived,
 	}
 }
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index fcd7fecd52..5205df2f8e 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -365,6 +365,90 @@ func TestCantMergeUnrelated(t *testing.T) {
 	})
 }
 
+func TestFastForwardOnlyMerge(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		session := loginUser(t, "user1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n")
+
+		// Use API to create a pr from update to master
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
+			Head:  "update",
+			Base:  "master",
+			Title: "create a pr that can be fast-forward-only merged",
+		}).AddTokenAuth(token)
+		session.MakeRequest(t, req, http.StatusCreated)
+
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+			Name: "user1",
+		})
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
+			OwnerID: user1.ID,
+			Name:    "repo1",
+		})
+
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+			HeadRepoID: repo1.ID,
+			BaseRepoID: repo1.ID,
+			HeadBranch: "update",
+			BaseBranch: "master",
+		})
+
+		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
+		assert.NoError(t, err)
+
+		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
+
+		assert.NoError(t, err)
+
+		gitRepo.Close()
+	})
+}
+
+func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		session := loginUser(t, "user1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n")
+		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n")
+
+		// Use API to create a pr from diverging to update
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
+			Head:  "diverging",
+			Base:  "master",
+			Title: "create a pr from a diverging branch",
+		}).AddTokenAuth(token)
+		session.MakeRequest(t, req, http.StatusCreated)
+
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+			Name: "user1",
+		})
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
+			OwnerID: user1.ID,
+			Name:    "repo1",
+		})
+
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+			HeadRepoID: repo1.ID,
+			BaseRepoID: repo1.ID,
+			HeadBranch: "diverging",
+			BaseBranch: "master",
+		})
+
+		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
+		assert.NoError(t, err)
+
+		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
+
+		assert.Error(t, err, "Merge should return an error due to being for a diverging branch")
+		assert.True(t, models.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
+
+		gitRepo.Close()
+	})
+}
+
 func TestConflictChecking(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})

From 33d939096d93a1014d4961374939376260740cbc Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 13 Feb 2024 00:24:22 +0000
Subject: [PATCH 025/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_el-GR.ini | 36 +++++++++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 5204e88be1..5164217616 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -420,6 +420,7 @@ authorize_title=Εξουσιοδότηση του "%s" για έχει πρόσ
 authorization_failed=Αποτυχία εξουσιοδότησης
 authorization_failed_desc=Η εξουσιοδότηση απέτυχε επειδή εντοπίστηκε μια μη έγκυρη αίτηση. Παρακαλούμε επικοινωνήστε με το συντηρητή της εφαρμογής που προσπαθήσατε να εξουσιοδοτήσετε.
 sspi_auth_failed=Αποτυχία ταυτοποίησης SSPI
+password_pwned=Ο κωδικός πρόσβασης που επιλέξατε είναι σε μια λίστα <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">κλεμμένων κωδικών πρόσβασης</a> που προηγουμένως εκτέθηκαν σε παραβίαση δημόσιων δεδομένων. Παρακαλώ δοκιμάστε ξανά με διαφορετικό κωδικό πρόσβασης και σκεφτείτε να αλλάξετε αυτόν τον κωδικό πρόσβασης όπου αλλού χρησιμοποιείται.
 password_pwned_err=Δεν ήταν δυνατή η ολοκλήρωση του αιτήματος προς το HaveIBeenPwned
 
 [mail]
@@ -633,6 +634,7 @@ webauthn=Κλειδιά Ασφαλείας
 public_profile=Δημόσιο Προφίλ
 biography_placeholder=Πείτε μας λίγο για τον εαυτό σας! (Μπορείτε να γράψετε με Markdown)
 location_placeholder=Μοιραστείτε την κατά προσέγγιση τοποθεσία σας με άλλους
+profile_desc=Ελέγξτε πώς εμφανίζεται το προφίλ σας σε άλλους χρήστες. Η κύρια διεύθυνση email σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση κωδικού πρόσβασης και λειτουργίες Git που βασίζονται στο web.
 password_username_disabled=Οι μη τοπικοί χρήστες δεν επιτρέπεται να αλλάξουν το όνομα χρήστη τους. Επικοινωνήστε με το διαχειριστή σας για περισσότερες λεπτομέρειες.
 full_name=Πλήρες Όνομα
 website=Ιστοσελίδα
@@ -644,6 +646,7 @@ update_language_not_found=Η γλώσσα "%s" δεν είναι διαθέσι
 update_language_success=Η γλώσσα ενημερώθηκε.
 update_profile_success=Το προφίλ σας έχει ενημερωθεί.
 change_username=Το όνομα χρήστη σας έχει αλλάξει.
+change_username_prompt=Σημείωση: Αλλάζοντας το όνομα χρήστη σας αλλάζει επίσης το URL του λογαριασμού σας.
 change_username_redirect_prompt=Το παλιό όνομα χρήστη θα ανακατευθύνει μέχρι να ζητηθεί ξανά.
 continue=Συνέχεια
 cancel=Ακύρωση
@@ -722,6 +725,7 @@ add_email_success=Η νέα διεύθυνση email έχει προστεθεί
 email_preference_set_success=Οι προτιμήσεις email έχουν οριστεί επιτυχώς.
 add_openid_success=Προστέθηκε η νέα διεύθυνση OpenID.
 keep_email_private=Απόκρυψη Διεύθυνσης Email
+keep_email_private_popup=Αυτό θα κρύψει τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας από το προφίλ σας, καθώς και όταν κάνετε ένα pull request ή επεξεργαστείτε ένα αρχείο χρησιμοποιώντας τη διεπαφή ιστού. Οι ωθούμενες υποβολές δεν θα τροποποιηθούν. Χρησιμοποιήστε το %s στις υποβολές για να τις συσχετίσετε με το λογαριασμό σας.
 openid_desc=Το OpenID σας επιτρέπει να αναθέσετε τον έλεγχο ταυτότητας σε έναν εξωτερικό πάροχο.
 
 manage_ssh_keys=Διαχείριση SSH Κλειδιών
@@ -800,7 +804,9 @@ ssh_disabled=SSH Απενεργοποιημένο
 ssh_signonly=Το SSH είναι απενεργοποιημένο αυτή τη στιγμή, έτσι αυτά τα κλειδιά είναι μόνο για την επαλήθευση υπογραφής των υποβολών.
 ssh_externally_managed=Αυτό το κλειδί SSH διαχειρίζεται εξωτερικά για αυτόν το χρήστη
 manage_social=Διαχείριση Συσχετιζόμενων Λογαριασμών Κοινωνικών Δικτύων
+social_desc=Αυτοί οι κοινωνικοί λογαριασμοί μπορούν να χρησιμοποιηθούν για να συνδεθείτε στο λογαριασμό σας. Βεβαιωθείτε ότι τους αναγνωρίζετε όλους.
 unbind=Αποσύνδεση
+unbind_success=Ο κοινωνικός λογαριασμός έχει διαγραφεί επιτυχώς.
 
 manage_access_token=Διαχείριση Διακριτικών Πρόσβασης
 generate_new_token=Δημιουργία Νέου Διακριτικού
@@ -822,6 +828,7 @@ select_permissions=Επιλέξτε δικαιώματα
 permission_no_access=Καμία Πρόσβαση
 permission_read=Αναγνωσμένες
 permission_write=Ανάγνωση και Εγγραφή
+access_token_desc=Τα επιλεγμένα δικαιώματα διακριτικών περιορίζουν την άδεια μόνο στις αντίστοιχες διαδρομές <a %s>API</a>. Διαβάστε την τεκμηρίωση <a %s></a> για περισσότερες πληροφορίες.
 at_least_one_permission=Πρέπει να επιλέξετε τουλάχιστον ένα δικαίωμα για να δημιουργήσετε ένα διακριτικό
 permissions_list=Δικαιώματα:
 
@@ -833,6 +840,8 @@ remove_oauth2_application_desc=Η αφαίρεση μιας εφαρμογής O
 remove_oauth2_application_success=Η εφαρμογή έχει διαγραφεί.
 create_oauth2_application=Δημιουργία νέας εφαρμογής OAuth2
 create_oauth2_application_button=Δημιουργία Εφαρμογής
+create_oauth2_application_success=Έχετε δημιουργήσει με επιτυχία μια νέα εφαρμογή OAuth2.
+update_oauth2_application_success=Έχετε ενημερώσει με επιτυχία την εφαρμογή OAuth2.
 oauth2_application_name=Όνομα Εφαρμογής
 oauth2_confidential_client=Εμπιστευτικός Πελάτης. Επιλέξτε το για εφαρμογές που διατηρούν το μυστικό κωδικό κρυφό, όπως πχ οι εφαρμογές ιστού. Μην επιλέγετε για εγγενείς εφαρμογές, συμπεριλαμβανομένων εφαρμογών επιφάνειας εργασίας και εφαρμογών για κινητά.
 oauth2_redirect_uris=URI Ανακατεύθυνσης. Χρησιμοποιήστε μια νέα γραμμή για κάθε URI.
@@ -841,12 +850,14 @@ oauth2_client_id=Ταυτότητα Πελάτη
 oauth2_client_secret=Μυστικό Πελάτη
 oauth2_regenerate_secret=Αναδημιουργία Μυστικού
 oauth2_regenerate_secret_hint=Χάσατε το μυστικό σας;
+oauth2_client_secret_hint=Το μυστικό δε θα εμφανιστεί ξανά αν κλείσετε ή ανανεώσετε αυτή τη σελίδα. Παρακαλώ βεβαιωθείτε ότι το έχετε αποθηκεύσει.
 oauth2_application_edit=Επεξεργασία
 oauth2_application_create_description=Οι εφαρμογές OAuth2 δίνει πρόσβαση στην εξωτερική εφαρμογή σας σε λογαριασμούς χρηστών σε αυτή την υπηρεσία.
 oauth2_application_remove_description=Αφαιρώντας μια εφαρμογή OAuth2 θα αποτραπεί η πρόσβαση αυτής, σε εξουσιοδοτημένους λογαριασμούς χρηστών σε αυτή την υπηρεσία. Συνέχεια;
 oauth2_application_locked=Το Gitea κάνει προεγγραφή σε μερικές εφαρμογές OAuth2 κατά την εκκίνηση αν είναι ενεργοποιημένες στις ρυθμίσεις. Για την αποφυγή απροσδόκητης συμπεριφοράς, αυτές δεν μπορούν ούτε να επεξεργαστούν ούτε να καταργηθούν. Παρακαλούμε ανατρέξτε στην τεκμηρίωση OAuth2 για περισσότερες πληροφορίες.
 
 authorized_oauth2_applications=Εξουσιοδοτημένες Εφαρμογές OAuth2
+authorized_oauth2_applications_description=Έχετε χορηγήσει πρόσβαση στον προσωπικό σας λογαριασμό σε αυτές τις εφαρμογές τρίτων. Ανακαλέστε την πρόσβαση για εφαρμογές που δεν χρειάζεστε πλέον.
 revoke_key=Ανάκληση
 revoke_oauth2_grant=Ανάκληση Πρόσβασης
 revoke_oauth2_grant_description=Η ανάκληση πρόσβασης για αυτή την εξωτερική εφαρμογή θα αποτρέψει αυτή την εφαρμογή από την πρόσβαση στα δεδομένα σας. Σίγουρα;
@@ -966,6 +977,8 @@ mirror_interval_invalid=Το χρονικό διάστημα του ειδώλο
 mirror_sync_on_commit=Συγχρονισμός κατά την ώθηση
 mirror_address=Κλωνοποίηση Από Το URL
 mirror_address_desc=Τοποθετήστε όλα τα απαιτούμενα διαπιστευτήρια στην ενότητα Εξουσιοδότηση.
+mirror_address_url_invalid=Η διεύθυνση URL που δόθηκε δεν είναι έγκυρη. Πρέπει να κάνετε escape όλα τα στοιχεία του url σωστά.
+mirror_address_protocol_invalid=Η παρεχόμενη διεύθυνση URL δεν είναι έγκυρη. Μόνο οι τοποθεσίες http(s):// ή git:// μπορούν να χρησιμοποιηθούν για τη δημιουργία ειδώλου.
 mirror_lfs=Large File Storage (LFS)
 mirror_lfs_desc=Ενεργοποίηση αντικατοπτρισμού δεδομένων LFS.
 mirror_lfs_endpoint=Άκρο LFS
@@ -1738,6 +1751,7 @@ pulls.rebase_conflict_summary=Μήνυμα Σφάλματος
 pulls.unrelated_histories=H Συγχώνευση Απέτυχε: Η κεφαλή και η βάση της συγχώνευσης δεν μοιράζονται μια κοινή ιστορία. Συμβουλή: Δοκιμάστε μια διαφορετική στρατηγική
 pulls.merge_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, η βάση ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά.
 pulls.head_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, το HEAD ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά.
+pulls.has_merged=Αποτυχία: Το pull request έχει συγχωνευθεί, δεν είναι δυνατή η συγχώνευση ξανά ή να αλλάξει ο κλάδος προορισμού.
 pulls.push_rejected=Η συγχώνευση απέτυχε: Η ώθηση απορρίφθηκε. Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο.
 pulls.push_rejected_summary=Μήνυμα Πλήρους Απόρριψης
 pulls.push_rejected_no_message=H Συγχώνευση Aπέτυχε: Η ώθηση απορρίφθηκε, αλλά δεν υπήρχε απομακρυσμένο μήνυμα.<br>Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο
@@ -1759,7 +1773,11 @@ pulls.outdated_with_base_branch=Αυτός ο κλάδος δεν είναι ε
 pulls.close=Κλείσιμο Pull Request
 pulls.closed_at=`έκλεισε αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`άνοιξε ξανά αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_hint=`Δείτε τις <a class="show-instruction">οδηγίες γραμμής εντολών</a>.`
+pulls.cmd_instruction_checkout_title=Έλεγχος
+pulls.cmd_instruction_checkout_desc=Από το αποθετήριο του έργου σας, ελέγξτε έναν νέο κλάδο και δοκιμάστε τις αλλαγές.
 pulls.cmd_instruction_merge_title=Συγχώνευση
+pulls.cmd_instruction_merge_desc=Συγχώνευση των αλλαγών και ενημέρωση στο Gitea.
 pulls.clear_merge_message=Εκκαθάριση μηνύματος συγχώνευσης
 pulls.clear_merge_message_hint=Η εκκαθάριση του μηνύματος συγχώνευσης θα αφαιρέσει μόνο το περιεχόμενο του μηνύματος υποβολής και θα διατηρήσει τα παραγόμενα git trailers όπως "Co-Authored-By …".
 
@@ -1778,6 +1796,7 @@ pulls.auto_merge_canceled_schedule_comment=`ακύρωσε την αυτόματ
 pulls.delete.title=Διαγραφή αυτού του pull request;
 pulls.delete.text=Θέλετε πραγματικά να διαγράψετε αυτό το pull request; (Αυτό θα σβήσει οριστικά όλο το περιεχόμενο του. Εξετάστε αν θέλετε να το κλείσετε, αν σκοπεύεται να το αρχειοθετήσετε)
 
+pulls.recently_pushed_new_branches=Ωθήσατε στο κλάδο <strong>%[1]s</strong> %[2]s
 
 pull.deleted_branch=(διαγράφηκε):%s
 
@@ -1811,8 +1830,13 @@ milestones.filter_sort.most_complete=Περισσότερο πλήρη
 milestones.filter_sort.most_issues=Περισσότερα ζητήματα
 milestones.filter_sort.least_issues=Λιγότερα ζητήματα
 
+signing.will_sign=Αυτή η υποβολή θα υπογραφεί με το κλειδί "%s".
+signing.wont_sign.error=Παρουσιάστηκε σφάλμα κατά τον έλεγχο για το αν η υποβολή μπορεί να υπογραφεί.
 signing.wont_sign.never=Οι υποβολές δεν υπογράφονται ποτέ.
 signing.wont_sign.always=Οι υποβολές υπογράφονται πάντα.
+signing.wont_sign.parentsigned=Η υποβολή δε θα υπογραφεί καθώς η γονική υποβολή δεν έχει υπογραφεί.
+signing.wont_sign.basesigned=Η συγχώνευση δε θα υπογραφεί καθώς η βασική υποβολή δεν έχει υπογραφή της βάσης.
+signing.wont_sign.headsigned=Η συγχώνευση δε θα υπογραφεί καθώς δεν έχει υπογραφή η υποβολή της κεφαλής.
 signing.wont_sign.not_signed_in=Δεν είστε συνδεδεμένοι.
 
 ext_wiki=Πρόσβαση στο Εξωτερικό Wiki
@@ -1953,6 +1977,7 @@ settings.mirror_settings.last_update=Τελευταία ενημέρωση
 settings.mirror_settings.push_mirror.none=Δεν έχουν ρυθμιστεί είδωλα ώθησης
 settings.mirror_settings.push_mirror.remote_url=URL Απομακρυσμένου Αποθετηρίου Git
 settings.mirror_settings.push_mirror.add=Προσθήκη Είδωλου Push
+settings.mirror_settings.push_mirror.edit_sync_time=Επεξεργασία διαστήματος συγχρονισμού ειδώλου
 
 settings.sync_mirror=Συγχρονισμός Τώρα
 settings.site=Ιστοσελίδα
@@ -2087,12 +2112,14 @@ settings.webhook_deletion_desc=Η αφαίρεση ενός webhook διαγρά
 settings.webhook_deletion_success=Το webhook έχει αφαιρεθεί.
 settings.webhook.test_delivery=Δοκιμή Παράδοσης
 settings.webhook.test_delivery_desc=Δοκιμάστε αυτό το webhook με ένα ψεύτικο συμβάν.
+settings.webhook.test_delivery_desc_disabled=Για να δοκιμάσετε αυτό το webhook με μια ψεύτικη κλήση, ενεργοποιήστε το.
 settings.webhook.request=Αίτημα
 settings.webhook.response=Απάντηση
 settings.webhook.headers=Κεφαλίδες
 settings.webhook.payload=Περιεχόμενο
 settings.webhook.body=Σώμα
 settings.webhook.replay.description=Επανάληψη αυτού του webhook.
+settings.webhook.replay.description_disabled=Για να επαναλάβετε αυτό το webhook, ενεργοποιήστε το.
 settings.webhook.delivery.success=Ένα γεγονός έχει προστεθεί στην ουρά παράδοσης. Μπορεί να χρειαστούν λίγα δευτερόλεπτα μέχρι να εμφανιστεί στο ιστορικό.
 settings.githooks_desc=Τα Άγκιστρα Git παρέχονται από το ίδιο το Git. Μπορείτε να επεξεργαστείτε τα αρχεία αγκίστρων παρακάτω για να ρυθμίσετε προσαρμοσμένες λειτουργίες.
 settings.githook_edit_desc=Αν το hook είναι ανενεργό, θα παρουσιαστεί ένα παράδειγμα. Αφήνοντας το περιεχόμενο του hook κενό θα το απενεργοποιήσετε.
@@ -2252,6 +2279,7 @@ settings.dismiss_stale_approvals_desc=Όταν οι νέες υποβολές π
 settings.require_signed_commits=Απαιτούνται Υπογεγραμμένες Υποβολές
 settings.require_signed_commits_desc=Απόρριψη νέων υποβολών σε αυτόν τον κλάδο εάν είναι μη υπογεγραμμένες ή μη επαληθεύσιμες.
 settings.protect_branch_name_pattern=Μοτίβο Προστατευμένου Ονόματος Κλάδου
+settings.protect_branch_name_pattern_desc=Μοτίβα ονόματος προστατευμένων κλάδων. Δείτε <a href="https://github.com/gobwas/glob">την τεκμηρίωση</a> για σύνταξη μοτίβου. Παραδείγματα: main, release/**
 settings.protect_patterns=Μοτίβα
 settings.protect_protected_file_patterns=Μοτίβα προστατευμένων αρχείων (διαχωρισμένα με ερωτηματικό ';'):
 settings.protect_protected_file_patterns_desc=Τα προστατευόμενα αρχεία δεν επιτρέπεται να αλλάξουν άμεσα, ακόμη και αν ο χρήστης έχει δικαιώματα να προσθέσει, να επεξεργαστεί ή να διαγράψει αρχεία σε αυτόν τον κλάδο. Επιπλέων μοτίβα μπορούν να διαχωριστούν με ερωτηματικό (';'). Δείτε την τεκμηρίωση <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> για τη σύνταξη του μοτίβου. Πχ: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2288,6 +2316,7 @@ settings.tags.protection.allowed.teams=Επιτρεπόμενες ομάδες
 settings.tags.protection.allowed.noone=Καμία
 settings.tags.protection.create=Προστασία Ετικέτας
 settings.tags.protection.none=Δεν υπάρχουν προστατευμένες ετικέτες.
+settings.tags.protection.pattern.description=Μπορείτε να χρησιμοποιήσετε ένα μόνο όνομα ή ένα μοτίβο τύπου glob ή κανονική έκφραση για να ταιριάξετε πολλαπλές ετικέτες. Διαβάστε περισσότερα στον <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">οδηγό προστατευμένων ετικετών</a>.
 settings.bot_token=Διακριτικό Bot
 settings.chat_id=ID Συνομιλίας
 settings.thread_id=ID Νήματος
@@ -2304,6 +2333,8 @@ settings.archive.branchsettings_unavailable=Οι ρυθμίσεις του κλ
 settings.archive.tagsettings_unavailable=Οι ρυθμίσεις της ετικέτας δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο.
 settings.unarchive.button=Απο-Αρχειοθέτηση αποθετηρίου
 settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου
+settings.unarchive.text=Η απο-αρχειοθέτηση του αποθετηρίου θα αποκαταστήσει την ικανότητά του να λαμβάνει υποβολές και ωθήσεις, καθώς και νέα ζητήματα και pull-requests.
+settings.unarchive.success=Το αποθετήριο απο-αρχειοθετήθηκε με επιτυχία.
 settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί.
 settings.lfs=LFS
 settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο
@@ -2486,6 +2517,7 @@ tag.create_success=Η ετικέτα "%s" δημιουργήθηκε.
 topic.manage_topics=Διαχείριση Θεμάτων
 topic.done=Ολοκληρώθηκε
 topic.count_prompt=Δεν μπορείτε να επιλέξετε περισσότερα από 25 θέματα
+topic.format_prompt=Τα θέματα πρέπει να ξεκινούν με γράμμα ή αριθμό, μπορούν να περιλαμβάνουν παύλες ('-') και τελείες ('.'), μπορεί να είναι μέχρι 35 χαρακτήρες. Τα γράμματα πρέπει να είναι πεζά.
 
 find_file.go_to_file=Αναζήτηση αρχείου
 find_file.no_matching=Δεν ταιριάζει κανένα αρχείο
@@ -2648,11 +2680,13 @@ dashboard.clean_unbind_oauth=Εκκαθάριση μη δεσμευμένων σ
 dashboard.clean_unbind_oauth_success=Όλες οι μη δεσμευμένες συνδέσεις OAuth διαγράφηκαν.
 dashboard.task.started=Εκκίνηση Εργασίας: %[1]s
 dashboard.task.process=Εργασία: %[1]s
+dashboard.task.cancelled=Εργασία: %[1]ακυρώθηκε: %[3]s
 dashboard.task.error=Σφάλμα στην Εργασία: %[1]s: %[3]s
 dashboard.task.finished=Εργασία: %[1]s που εκκινήθηκε από %[2]s τελείωσε
 dashboard.task.unknown=Άγνωστη εργασία: %[1]s
 dashboard.cron.started=Εκκίνηση Προγραμματισμένης Εργασίας: %[1]s
 dashboard.cron.process=Προγραμματισμένη Εργασία: %[1]s
+dashboard.cron.cancelled=Προγραμματισμένη εργασία: %[1]s ακυρώθηκε: %[3]s
 dashboard.cron.error=Σφάλμα στη Προγραμματισμένη Εργασία: %s: %[3]s
 dashboard.cron.finished=Προγραμματισμένη Εργασία: %[1]s τελείωσε
 dashboard.delete_inactive_accounts=Διαγραφή όλων των μη ενεργοποιημένων λογαριασμών
@@ -2662,6 +2696,7 @@ dashboard.delete_repo_archives.started=Η διαγραφή όλων των αρ
 dashboard.delete_missing_repos=Διαγραφή όλων των αποθετηρίων που δεν έχουν τα αρχεία Git τους
 dashboard.delete_missing_repos.started=Η διαγραφή όλων των αποθετηρίων που δεν έχουν αρχεία Git τους, ξεκίνησε.
 dashboard.delete_generated_repository_avatars=Διαγραφή δημιουργημένων εικόνων αποθετηρίων
+dashboard.sync_repo_branches=Συγχρονισμός κλάδων που λείπουν, από τα δεδομένα git στις βάσεις δεδομένων
 dashboard.update_mirrors=Ενημέρωση Ειδώλων
 dashboard.repo_health_check=Έλεγχος υγείας σε όλα τα αποθετήρια
 dashboard.check_repo_stats=Έλεγχος όλων των στατιστικών αποθετηρίων
@@ -2714,6 +2749,7 @@ dashboard.stop_zombie_tasks=Διακοπή εργασιών ζόμπι
 dashboard.stop_endless_tasks=Διακοπή ατελείωτων εργασιών
 dashboard.cancel_abandoned_jobs=Ακύρωση εγκαταλελειμμένων εργασιών
 dashboard.start_schedule_tasks=Έναρξη προγραμματισμένων εργασιών
+dashboard.sync_branch.started=Ο Συγχρονισμός των Κλάδων ξεκίνησε
 dashboard.rebuild_issue_indexer=Αναδόμηση ευρετηρίου ζητημάτων
 
 users.user_manage_panel=Διαχείριση Λογαριασμών Χρηστών

From b85e4a64fa26e1f20321c3a7cedf9fa05640ca48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Nicas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Tue, 13 Feb 2024 09:07:59 +0100
Subject: [PATCH 026/679] Show `View at this point in history` for every commit
 (#29122)

Shows the 'View at this point in history'-link (from #27354) for every
commit

before:

![image](https://github.com/go-gitea/gitea/assets/72873130/0e5cd763-e099-4bb4-9519-653fe21f85a6)

after:

![image](https://github.com/go-gitea/gitea/assets/72873130/2b57346f-51e3-4901-b85e-63a690878939)
---
 templates/repo/commits_list.tmpl | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index 7702770c40..4eb31e0e8e 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -78,9 +78,12 @@
 						{{end}}
 						<td class="text right aligned gt-py-0">
 							<button class="btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
-							{{if $.FileName}}
-								<a class="btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}">{{svg "octicon-file-code"}}</a>
-							{{end}}
+							<a
+								class="btn interact-bg gt-p-3"
+								data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}"
+								href="{{if $.FileName}}{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}{{else}}{{printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}{{end}}">
+								{{svg "octicon-file-code"}}
+							</a>
 						</td>
 					</tr>
 				{{end}}

From 6fad2c874438275d3f69bb1cc223708bd2d27ff6 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 13 Feb 2024 09:45:31 +0100
Subject: [PATCH 027/679] Dont load Review if Comment is
 CommentTypeReviewRequest (#28551)

RequestReview get deleted on review.
So we don't have to try to load them on comments.

broken out #28544
---
 models/issues/comment.go      | 7 +++++++
 models/issues/comment_list.go | 3 ++-
 models/issues/review.go       | 3 +++
 3 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/models/issues/comment.go b/models/issues/comment.go
index c63fcab894..a586caf1b5 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -695,8 +695,15 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
 }
 
 func (c *Comment) loadReview(ctx context.Context) (err error) {
+	if c.ReviewID == 0 {
+		return nil
+	}
 	if c.Review == nil {
 		if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil {
+			// review request which has been replaced by actual reviews doesn't exist in database anymore, so ignorem them.
+			if c.Type == CommentTypeReviewRequest {
+				return nil
+			}
 			return err
 		}
 	}
diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index 93af45870e..cb7df3270d 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -430,7 +430,8 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
 	for _, comment := range comments {
 		comment.Review = reviews[comment.ReviewID]
 		if comment.Review == nil {
-			if comment.ReviewID > 0 {
+			// review request which has been replaced by actual reviews doesn't exist in database anymore, so don't log errors for them.
+			if comment.ReviewID > 0 && comment.Type != CommentTypeReviewRequest {
 				log.Error("comment with review id [%d] but has no review record", comment.ReviewID)
 			}
 			continue
diff --git a/models/issues/review.go b/models/issues/review.go
index f2022ae0aa..ba4e02f765 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -621,6 +621,9 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
 		return nil, err
 	}
 
+	// func caller use the created comment to retrieve created review too.
+	comment.Review = review
+
 	return comment, committer.Commit()
 }
 

From a8748eedae3518550bd43fd592d206df2bea6bef Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 13 Feb 2024 16:13:06 +0200
Subject: [PATCH 028/679] Remove jQuery from the user search form in admin page
 (#29151)

- Switched to plain JavaScript
- Tested the form and it works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/admin/users.js | 45 +++++++++++++++++-------------
 1 file changed, 25 insertions(+), 20 deletions(-)

diff --git a/web_src/js/features/admin/users.js b/web_src/js/features/admin/users.js
index c8edaab549..7cac603b5c 100644
--- a/web_src/js/features/admin/users.js
+++ b/web_src/js/features/admin/users.js
@@ -1,34 +1,39 @@
-import $ from 'jquery';
-
 export function initAdminUserListSearchForm() {
   const searchForm = window.config.pageData.adminUserListSearchForm;
   if (!searchForm) return;
 
-  const $form = $('#user-list-search-form');
-  if (!$form.length) return;
+  const form = document.querySelector('#user-list-search-form');
+  if (!form) return;
 
-  $form.find(`button[name=sort][value=${searchForm.SortType}]`).addClass('active');
+  for (const button of form.querySelectorAll(`button[name=sort][value="${searchForm.SortType}"]`)) {
+    button.classList.add('active');
+  }
 
   if (searchForm.StatusFilterMap) {
     for (const [k, v] of Object.entries(searchForm.StatusFilterMap)) {
       if (!v) continue;
-      $form.find(`input[name="status_filter[${k}]"][value=${v}]`).prop('checked', true);
+      for (const input of form.querySelectorAll(`input[name="status_filter[${k}]"][value="${v}"]`)) {
+        input.checked = true;
+      }
     }
   }
 
-  $form.find(`input[type=radio]`).on('click', () => {
-    $form.trigger('submit');
-    return false;
-  });
-
-  $form.find('.j-reset-status-filter').on('click', () => {
-    $form.find(`input[type=radio]`).each((_, e) => {
-      const $e = $(e);
-      if ($e.attr('name').startsWith('status_filter[')) {
-        $e.prop('checked', false);
-      }
+  for (const radio of form.querySelectorAll('input[type=radio]')) {
+    radio.addEventListener('click', () => {
+      form.submit();
     });
-    $form.trigger('submit');
-    return false;
-  });
+  }
+
+  const resetButtons = form.querySelectorAll('.j-reset-status-filter');
+  for (const button of resetButtons) {
+    button.addEventListener('click', (e) => {
+      e.preventDefault();
+      for (const input of form.querySelectorAll('input[type=radio]')) {
+        if (input.name.startsWith('status_filter[')) {
+          input.checked = false;
+        }
+      }
+      form.submit();
+    });
+  }
 }

From 4f346916838fcc95c6d7eb574145c8b78f7ac726 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 14 Feb 2024 00:57:55 +0200
Subject: [PATCH 029/679] Fix Gitpod logic of setting ROOT_URL (#29162)

---
 .gitpod.yml | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/.gitpod.yml b/.gitpod.yml
index 35b22c45ae..ed2f57f4bf 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -10,10 +10,19 @@ tasks:
   - name: Run backend
     command: |
       gp sync-await setup
-      if [ ! -f custom/conf/app.ini ]
-      then
+
+      # Get the URL and extract the domain
+      url=$(gp url 3000)
+      domain=$(echo $url | awk -F[/:] '{print $4}')
+
+      if [ -f custom/conf/app.ini ]; then
+        sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini
+        sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini
+        sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini
+        sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini
+      else
         mkdir -p custom/conf/
-        echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini
+        echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini
         echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
       fi
       export TAGS="sqlite sqlite_unlock_notify"

From 4635e6d2a660fffb112b74cac967b9fb015b8d9a Mon Sep 17 00:00:00 2001
From: Scott Yeager <yeagersm@gmail.com>
Date: Tue, 13 Feb 2024 15:24:35 -0800
Subject: [PATCH 030/679] Runner tokens are multi use (#29153)

Fixes https://github.com/go-gitea/gitea/issues/28911.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 docs/content/usage/actions/act-runner.en-us.md | 2 ++
 docs/content/usage/actions/quickstart.en-us.md | 4 ++--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/docs/content/usage/actions/act-runner.en-us.md b/docs/content/usage/actions/act-runner.en-us.md
index 3fad9cbfe8..b2806bf5dd 100644
--- a/docs/content/usage/actions/act-runner.en-us.md
+++ b/docs/content/usage/actions/act-runner.en-us.md
@@ -120,6 +120,8 @@ A registration token can also be obtained from the gitea [command-line interface
 gitea --config /etc/gitea/app.ini actions generate-runner-token
 ```
 
+Tokens are valid for registering multiple runners, until they are revoked and replaced by a new token using the token reset link in the web interface.
+
 ### Register the runner
 
 The act runner can be registered by running the following command:
diff --git a/docs/content/usage/actions/quickstart.en-us.md b/docs/content/usage/actions/quickstart.en-us.md
index 2a2cf72584..0514b6ddf2 100644
--- a/docs/content/usage/actions/quickstart.en-us.md
+++ b/docs/content/usage/actions/quickstart.en-us.md
@@ -61,8 +61,8 @@ It is always a bad idea to use a loopback address such as `127.0.0.1` or `localh
 If you are unsure which address to use, the LAN address is usually the right choice.
 
 `token` is used for authentication and identification, such as `P2U1U0oB4XaRCi8azcngmPCLbRpUGapalhmddh23`.
-It is one-time use only and cannot be used to register multiple runners.
-You can obtain different levels of 'tokens' from the following places to create the corresponding level of' runners':
+Each token can be used to create multiple runners, until it is replaced with a new token using the reset link.
+You can obtain different levels of 'tokens' from the following places to create the corresponding level of 'runners':
 
 - Instance level: The admin settings page, like `<your_gitea.com>/admin/actions/runners`.
 - Organization level: The organization settings page, like `<your_gitea.com>/<org>/settings/actions/runners`.

From 4feb91f8574e2363d2120655562e6f09bbf1ffcb Mon Sep 17 00:00:00 2001
From: delvh <dev.lh@web.de>
Date: Wed, 14 Feb 2024 11:10:16 +0100
Subject: [PATCH 031/679] Document how the TOC election process works (#29135)

This is supposed to prevent a power vacuum so that a problem similar to
the 2024 election will not happen again

Additionally, update current TOC members from 2023 to 2024.
---
 CONTRIBUTING.md | 36 +++++++++++++++++++++++++++---------
 1 file changed, 27 insertions(+), 9 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 96b02edd5b..f9b9a421a3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -47,6 +47,7 @@
   - [Release Cycle](#release-cycle)
   - [Maintainers](#maintainers)
   - [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
+    - [TOC election process](#toc-election-process)
     - [Current TOC members](#current-toc-members)
     - [Previous TOC/owners members](#previous-tocowners-members)
   - [Governance Compensation](#governance-compensation)
@@ -486,36 +487,53 @@ if possible provide GPG signed commits.
 https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
 https://help.github.com/articles/signing-commits-with-gpg/
 
+Furthermore, any account with write access (like bots and TOC members) **must** use 2FA.
+https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+
 ## Technical Oversight Committee (TOC)
 
-At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions would be elected as it has been over the past years, and the other three would consist of appointed members from the Gitea company.
+At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
 https://blog.gitea.com/quarterly-23q1/
 
-When the new community members have been elected, the old members will give up ownership to the newly elected members. For security reasons, TOC members or any account with write access (like a bot) must use 2FA.
-https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+### TOC election process
+
+Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
+A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
+If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
+
+The TOC is elected for one year, the TOC election happens yearly.
+After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
+If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
+Refusals result in the person with the next highest vote getting the same choice.
+As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
+
+If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
 
 ### Current TOC members
 
-- 2023-01-01 ~ 2023-12-31 - https://blog.gitea.com/quarterly-23q1/
+- 2024-01-01 ~ 2024-12-31
   - Company
     - [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
     - [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
-    - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.io>
+    - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
   - Community
     - [6543](https://gitea.com/6543) <6543@obermui.de>
-    - [Andrew Thornton](https://gitea.com/zeripath) <art27@cantab.net>
+    - [delvh](https://gitea.com/delvh) <dev.lh@web.de>
     - [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com>
 
 ### Previous TOC/owners members
 
 Here's the history of the owners and the time they served:
 
-- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
+- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
 - [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017
 - [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017
 - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801)
-- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
-- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
+- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
+- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
+- [6543](https://gitea.com/6543) - 2023
+- [John Olheiser](https://gitea.com/jolheiser) - 2023
+- [Jason Song](https://gitea.com/wolfogre) - 2023
 
 ## Governance Compensation
 

From 37061e8266806c0b2b66ac64138e725632b295db Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Wed, 14 Feb 2024 17:31:51 +0100
Subject: [PATCH 032/679] Use ghost user if user was not found (#29161)

Fixes #29159
---
 models/issues/comment_list.go | 4 ++++
 models/issues/review.go       | 8 ++++++++
 2 files changed, 12 insertions(+)

diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index cb7df3270d..30a437ea50 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -225,6 +225,10 @@ func (comments CommentList) loadAssignees(ctx context.Context) error {
 
 	for _, comment := range comments {
 		comment.Assignee = assignees[comment.AssigneeID]
+		if comment.Assignee == nil {
+			comment.AssigneeID = user_model.GhostUserID
+			comment.Assignee = user_model.NewGhostUser()
+		}
 	}
 	return nil
 }
diff --git a/models/issues/review.go b/models/issues/review.go
index ba4e02f765..3aa9d3e2a8 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -159,6 +159,14 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
 		return err
 	}
 	r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
+	if err != nil {
+		if !user_model.IsErrUserNotExist(err) {
+			return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
+		}
+		r.ReviewerID = user_model.GhostUserID
+		r.Reviewer = user_model.NewGhostUser()
+		return nil
+	}
 	return err
 }
 

From d0183dfa49125a9e533dbe8beab4d8a3724a4f07 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 15 Feb 2024 01:18:30 +0800
Subject: [PATCH 033/679] Refactor git version functions and check
 compatibility (#29155)

Introduce a new function checkGitVersionCompatibility, when the git
version can't be used by Gitea, tell the end users to downgrade or
upgrade. The refactored functions are related to make the code easier to
test.

And simplify the comments for "safe.directory"

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 modules/git/git.go      | 77 +++++++++++++++++++++++++----------------
 modules/git/git_test.go | 23 ++++++++++++
 2 files changed, 70 insertions(+), 30 deletions(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index 89c23ff230..8621df0f49 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -39,36 +39,37 @@ var (
 	gitVersion *version.Version
 )
 
-// loadGitVersion returns current Git version from shell. Internal usage only.
-func loadGitVersion() (*version.Version, error) {
+// loadGitVersion tries to get the current git version and stores it into a global variable
+func loadGitVersion() error {
 	// doesn't need RWMutex because it's executed by Init()
 	if gitVersion != nil {
-		return gitVersion, nil
+		return nil
 	}
 
 	stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil)
 	if runErr != nil {
-		return nil, runErr
+		return runErr
 	}
 
-	fields := strings.Fields(stdout)
+	ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
+	if err == nil {
+		gitVersion = ver
+	}
+	return err
+}
+
+func parseGitVersionLine(s string) (*version.Version, error) {
+	fields := strings.Fields(s)
 	if len(fields) < 3 {
-		return nil, fmt.Errorf("invalid git version output: %s", stdout)
+		return nil, fmt.Errorf("invalid git version: %q", s)
 	}
 
-	var versionString string
-
-	// Handle special case on Windows.
-	i := strings.Index(fields[2], "windows")
-	if i >= 1 {
-		versionString = fields[2][:i-1]
-	} else {
-		versionString = fields[2]
+	// version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1"
+	versionString := fields[2]
+	if pos := strings.Index(versionString, "windows"); pos >= 1 {
+		versionString = versionString[:pos-1]
 	}
-
-	var err error
-	gitVersion, err = version.NewVersion(versionString)
-	return gitVersion, err
+	return version.NewVersion(versionString)
 }
 
 // SetExecutablePath changes the path of git executable and checks the file permission and version.
@@ -83,8 +84,7 @@ func SetExecutablePath(path string) error {
 	}
 	GitExecutable = absPath
 
-	_, err = loadGitVersion()
-	if err != nil {
+	if err = loadGitVersion(); err != nil {
 		return fmt.Errorf("unable to load git version: %w", err)
 	}
 
@@ -105,6 +105,9 @@ func SetExecutablePath(path string) error {
 		return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint)
 	}
 
+	if err = checkGitVersionCompatibility(gitVersion); err != nil {
+		return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", gitVersion.String(), err)
+	}
 	return nil
 }
 
@@ -262,19 +265,18 @@ func syncGitConfig() (err error) {
 		}
 	}
 
-	// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user
-	// however, some docker users and samba users find it difficult to configure their systems so that Gitea's git repositories are owned by the Gitea user. (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
-	// see issue: https://github.com/go-gitea/gitea/issues/19455
-	// Fundamentally the problem lies with the uid-gid-mapping mechanism for filesystems in docker on windows (and to a lesser extent samba).
-	// Docker's configuration mechanism for local filesystems provides no way of setting this mapping and although there is a mechanism for setting this uid through using cifs mounting it is complicated and essentially undocumented
-	// Thus the owner uid/gid for files on these filesystems will be marked as root.
+	// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
+	// However, some docker users and samba users find it difficult to configure their systems correctly,
+	// so that Gitea's git repositories are owned by the Gitea user.
+	// (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
+	// See issue: https://github.com/go-gitea/gitea/issues/19455
 	// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
 	// it is now safe to set "safe.directory=*" for internal usage only.
-	// Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later
-	// Although only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - this setting is tolerated by earlier versions
+	// Although this setting is only supported by some new git versions, it is also tolerated by earlier versions
 	if err := configAddNonExist("safe.directory", "*"); err != nil {
 		return err
 	}
+
 	if runtime.GOOS == "windows" {
 		if err := configSet("core.longpaths", "true"); err != nil {
 			return err
@@ -307,8 +309,8 @@ func syncGitConfig() (err error) {
 
 // CheckGitVersionAtLeast check git version is at least the constraint version
 func CheckGitVersionAtLeast(atLeast string) error {
-	if _, err := loadGitVersion(); err != nil {
-		return err
+	if gitVersion == nil {
+		panic("git module is not initialized") // it shouldn't happen
 	}
 	atLeastVersion, err := version.NewVersion(atLeast)
 	if err != nil {
@@ -320,6 +322,21 @@ func CheckGitVersionAtLeast(atLeast string) error {
 	return nil
 }
 
+func checkGitVersionCompatibility(gitVer *version.Version) error {
+	badVersions := []struct {
+		Version *version.Version
+		Reason  string
+	}{
+		{version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"},
+	}
+	for _, bad := range badVersions {
+		if gitVer.Equal(bad.Version) {
+			return errors.New(bad.Reason)
+		}
+	}
+	return nil
+}
+
 func configSet(key, value string) error {
 	stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
 	if err != nil && !err.IsExitCode(1) {
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 37ab669ea4..fc92bebe04 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -13,6 +13,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
+	"github.com/hashicorp/go-version"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -93,3 +94,25 @@ func TestSyncConfig(t *testing.T) {
 	assert.True(t, gitConfigContains("[sync-test]"))
 	assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
 }
+
+func TestParseGitVersion(t *testing.T) {
+	v, err := parseGitVersionLine("git version 2.29.3")
+	assert.NoError(t, err)
+	assert.Equal(t, "2.29.3", v.String())
+
+	v, err = parseGitVersionLine("git version 2.29.3.windows.1")
+	assert.NoError(t, err)
+	assert.Equal(t, "2.29.3", v.String())
+
+	_, err = parseGitVersionLine("git version")
+	assert.Error(t, err)
+
+	_, err = parseGitVersionLine("git version windows")
+	assert.Error(t, err)
+}
+
+func TestCheckGitVersionCompatibility(t *testing.T) {
+	assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.0"))))
+	assert.ErrorContains(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.1"))), "regression bug of GIT_FLUSH")
+	assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.2"))))
+}

From 155269fa586c41a268530c3bb56349e68e6761d7 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Wed, 14 Feb 2024 18:50:10 +0100
Subject: [PATCH 034/679] Remove unused `KeyID`. (#29167)

`KeyID` is never set.
---
 models/organization/org.go   |  4 +---
 models/user/email_address.go |  4 +---
 models/user/error.go         |  7 +++----
 models/user/user.go          | 14 +++++++-------
 4 files changed, 12 insertions(+), 17 deletions(-)

diff --git a/models/organization/org.go b/models/organization/org.go
index 23a4e2f96a..b4919defb4 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -594,9 +594,7 @@ func GetOrgByID(ctx context.Context, id int64) (*Organization, error) {
 		return nil, err
 	} else if !has {
 		return nil, user_model.ErrUserNotExist{
-			UID:   id,
-			Name:  "",
-			KeyID: 0,
+			UID: id,
 		}
 	}
 	return u, nil
diff --git a/models/user/email_address.go b/models/user/email_address.go
index 957e72fe89..216840916d 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -332,9 +332,7 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
 		return err
 	} else if !has {
 		return ErrUserNotExist{
-			UID:   email.UID,
-			Name:  "",
-			KeyID: 0,
+			UID: email.UID,
 		}
 	}
 
diff --git a/models/user/error.go b/models/user/error.go
index ef572c178a..cbf19998d1 100644
--- a/models/user/error.go
+++ b/models/user/error.go
@@ -31,9 +31,8 @@ func (err ErrUserAlreadyExist) Unwrap() error {
 
 // ErrUserNotExist represents a "UserNotExist" kind of error.
 type ErrUserNotExist struct {
-	UID   int64
-	Name  string
-	KeyID int64
+	UID  int64
+	Name string
 }
 
 // IsErrUserNotExist checks if an error is a ErrUserNotExist.
@@ -43,7 +42,7 @@ func IsErrUserNotExist(err error) bool {
 }
 
 func (err ErrUserNotExist) Error() string {
-	return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID)
+	return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
 }
 
 // Unwrap unwraps this error as a ErrNotExist error
diff --git a/models/user/user.go b/models/user/user.go
index 536ec78a0b..f31dfb76bb 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -835,7 +835,7 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
 	if err != nil {
 		return nil, err
 	} else if !has {
-		return nil, ErrUserNotExist{id, "", 0}
+		return nil, ErrUserNotExist{UID: id}
 	}
 	return u, nil
 }
@@ -885,14 +885,14 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
 // GetUserByNameCtx returns user by given name.
 func GetUserByName(ctx context.Context, name string) (*User, error) {
 	if len(name) == 0 {
-		return nil, ErrUserNotExist{0, name, 0}
+		return nil, ErrUserNotExist{Name: name}
 	}
 	u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual}
 	has, err := db.GetEngine(ctx).Get(u)
 	if err != nil {
 		return nil, err
 	} else if !has {
-		return nil, ErrUserNotExist{0, name, 0}
+		return nil, ErrUserNotExist{Name: name}
 	}
 	return u, nil
 }
@@ -1033,7 +1033,7 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) []
 // GetUserByEmail returns the user object by given e-mail if exists.
 func GetUserByEmail(ctx context.Context, email string) (*User, error) {
 	if len(email) == 0 {
-		return nil, ErrUserNotExist{0, email, 0}
+		return nil, ErrUserNotExist{Name: email}
 	}
 
 	email = strings.ToLower(email)
@@ -1060,7 +1060,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
 		}
 	}
 
-	return nil, ErrUserNotExist{0, email, 0}
+	return nil, ErrUserNotExist{Name: email}
 }
 
 // GetUser checks if a user already exists
@@ -1071,7 +1071,7 @@ func GetUser(ctx context.Context, user *User) (bool, error) {
 // GetUserByOpenID returns the user object by given OpenID if exists.
 func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
 	if len(uri) == 0 {
-		return nil, ErrUserNotExist{0, uri, 0}
+		return nil, ErrUserNotExist{Name: uri}
 	}
 
 	uri, err := openid.Normalize(uri)
@@ -1091,7 +1091,7 @@ func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
 		return GetUserByID(ctx, oid.UID)
 	}
 
-	return nil, ErrUserNotExist{0, uri, 0}
+	return nil, ErrUserNotExist{Name: uri}
 }
 
 // GetAdminUser returns the first administrator

From a346a8c852a44c9c22eeb06701a384bb17a7ac0b Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Wed, 14 Feb 2024 13:19:57 -0500
Subject: [PATCH 035/679] bump to use go 1.22 (#29119)

---
 .devcontainer/devcontainer.json    |  2 +-
 Dockerfile                         |  2 +-
 Dockerfile.rootless                |  2 +-
 Makefile                           |  4 ++--
 models/issues/issue_xref.go        |  6 ++---
 models/issues/review_list.go       | 12 +++++-----
 modules/git/log_name_status.go     | 14 +++++------
 modules/repository/generate.go     |  2 +-
 routers/web/org/projects.go        |  8 +++----
 routers/web/repo/projects.go       |  8 +++----
 routers/web/user/home.go           |  8 +++----
 services/repository/create_test.go | 38 +++++++++++++++---------------
 snap/snapcraft.yaml                |  2 +-
 13 files changed, 54 insertions(+), 54 deletions(-)

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 9e290fb6a5..1051b0f2a2 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,6 +1,6 @@
 {
   "name": "Gitea DevContainer",
-  "image": "mcr.microsoft.com/devcontainers/go:1.21-bullseye",
+  "image": "mcr.microsoft.com/devcontainers/go:1.22-bullseye",
   "features": {
     // installs nodejs into container
     "ghcr.io/devcontainers/features/node:1": {
diff --git a/Dockerfile b/Dockerfile
index 325b0255df..b647c0cd59 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index 6f27c698ac..dd7da97278 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
diff --git a/Makefile b/Makefile
index 366ca6c624..da4806d9c4 100644
--- a/Makefile
+++ b/Makefile
@@ -23,12 +23,12 @@ SHASUM ?= shasum -a 256
 HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
 COMMA := ,
 
-XGO_VERSION := go-1.21.x
+XGO_VERSION := go-1.22.x
 
 AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0
 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
-GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1
 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1
 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5
diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go
index 77ef53a013..cfc3c1683c 100644
--- a/models/issues/issue_xref.go
+++ b/models/issues/issue_xref.go
@@ -46,10 +46,10 @@ func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error
 	for i, c := range active {
 		ids[i] = c.ID
 	}
-	return neuterCrossReferencesIds(ctx, ids)
+	return neuterCrossReferencesIDs(ctx, ids)
 }
 
-func neuterCrossReferencesIds(ctx context.Context, ids []int64) error {
+func neuterCrossReferencesIDs(ctx context.Context, ids []int64) error {
 	_, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered})
 	return err
 }
@@ -100,7 +100,7 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe
 			}
 		}
 		if len(ids) > 0 {
-			if err = neuterCrossReferencesIds(stdCtx, ids); err != nil {
+			if err = neuterCrossReferencesIDs(stdCtx, ids); err != nil {
 				return err
 			}
 		}
diff --git a/models/issues/review_list.go b/models/issues/review_list.go
index ed3d0bd028..282f18b4f7 100644
--- a/models/issues/review_list.go
+++ b/models/issues/review_list.go
@@ -18,11 +18,11 @@ type ReviewList []*Review
 
 // LoadReviewers loads reviewers
 func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
-	reviewerIds := make([]int64, len(reviews))
+	reviewerIDs := make([]int64, len(reviews))
 	for i := 0; i < len(reviews); i++ {
-		reviewerIds[i] = reviews[i].ReviewerID
+		reviewerIDs[i] = reviews[i].ReviewerID
 	}
-	reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIds)
+	reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs)
 	if err != nil {
 		return err
 	}
@@ -38,12 +38,12 @@ func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
 }
 
 func (reviews ReviewList) LoadIssues(ctx context.Context) error {
-	issueIds := container.Set[int64]{}
+	issueIDs := container.Set[int64]{}
 	for i := 0; i < len(reviews); i++ {
-		issueIds.Add(reviews[i].IssueID)
+		issueIDs.Add(reviews[i].IssueID)
 	}
 
-	issues, err := GetIssuesByIDs(ctx, issueIds.Values())
+	issues, err := GetIssuesByIDs(ctx, issueIDs.Values())
 	if err != nil {
 		return err
 	}
diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go
index 26a0d28098..9e345f3ee0 100644
--- a/modules/git/log_name_status.go
+++ b/modules/git/log_name_status.go
@@ -143,19 +143,19 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
 	}
 
 	// Our "line" must look like: <commitid> SP (<parent> SP) * NUL
-	commitIds := string(g.next)
+	commitIDs := string(g.next)
 	if g.buffull {
 		more, err := g.rd.ReadString('\x00')
 		if err != nil {
 			return nil, err
 		}
-		commitIds += more
+		commitIDs += more
 	}
-	commitIds = commitIds[:len(commitIds)-1]
-	splitIds := strings.Split(commitIds, " ")
-	ret.CommitID = splitIds[0]
-	if len(splitIds) > 1 {
-		ret.ParentIDs = splitIds[1:]
+	commitIDs = commitIDs[:len(commitIDs)-1]
+	splitIDs := strings.Split(commitIDs, " ")
+	ret.CommitID = splitIDs[0]
+	if len(splitIDs) > 1 {
+		ret.ParentIDs = splitIDs[1:]
 	}
 
 	// now read the next "line"
diff --git a/modules/repository/generate.go b/modules/repository/generate.go
index 013dd8f76f..f622383bb5 100644
--- a/modules/repository/generate.go
+++ b/modules/repository/generate.go
@@ -94,7 +94,7 @@ type GiteaTemplate struct {
 }
 
 // Globs parses the .gitea/template globs or returns them if they were already parsed
-func (gt GiteaTemplate) Globs() []glob.Glob {
+func (gt *GiteaTemplate) Globs() []glob.Glob {
 	if gt.globs != nil {
 		return gt.globs
 	}
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 03798a712c..f65cc6e679 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -377,16 +377,16 @@ func ViewProject(ctx *context.Context) {
 	linkedPrsMap := make(map[int64][]*issues_model.Issue)
 	for _, issuesList := range issuesMap {
 		for _, issue := range issuesList {
-			var referencedIds []int64
+			var referencedIDs []int64
 			for _, comment := range issue.Comments {
 				if comment.RefIssueID != 0 && comment.RefIsPull {
-					referencedIds = append(referencedIds, comment.RefIssueID)
+					referencedIDs = append(referencedIDs, comment.RefIssueID)
 				}
 			}
 
-			if len(referencedIds) > 0 {
+			if len(referencedIDs) > 0 {
 				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
-					IssueIDs: referencedIds,
+					IssueIDs: referencedIDs,
 					IsPull:   util.OptionalBoolTrue,
 				}); err == nil {
 					linkedPrsMap[issue.ID] = linkedPrs
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 4908bb796d..001f0752c3 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -339,16 +339,16 @@ func ViewProject(ctx *context.Context) {
 	linkedPrsMap := make(map[int64][]*issues_model.Issue)
 	for _, issuesList := range issuesMap {
 		for _, issue := range issuesList {
-			var referencedIds []int64
+			var referencedIDs []int64
 			for _, comment := range issue.Comments {
 				if comment.RefIssueID != 0 && comment.RefIsPull {
-					referencedIds = append(referencedIds, comment.RefIssueID)
+					referencedIDs = append(referencedIDs, comment.RefIssueID)
 				}
 			}
 
-			if len(referencedIds) > 0 {
+			if len(referencedIDs) > 0 {
 				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
-					IssueIDs: referencedIds,
+					IssueIDs: referencedIDs,
 					IsPull:   util.OptionalBoolTrue,
 				}); err == nil {
 					linkedPrsMap[issue.ID] = linkedPrs
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 44920817c9..83fc4d7162 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -296,17 +296,17 @@ func Milestones(ctx *context.Context) {
 		}
 	}
 
-	showRepoIds := make(container.Set[int64], len(showRepos))
+	showRepoIDs := make(container.Set[int64], len(showRepos))
 	for _, repo := range showRepos {
 		if repo.ID > 0 {
-			showRepoIds.Add(repo.ID)
+			showRepoIDs.Add(repo.ID)
 		}
 	}
 	if len(repoIDs) == 0 {
-		repoIDs = showRepoIds.Values()
+		repoIDs = showRepoIDs.Values()
 	}
 	repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool {
-		return !showRepoIds.Contains(v)
+		return !showRepoIDs.Contains(v)
 	})
 
 	var pagerCount int
diff --git a/services/repository/create_test.go b/services/repository/create_test.go
index b3e1f0550c..41e6b615db 100644
--- a/services/repository/create_test.go
+++ b/services/repository/create_test.go
@@ -21,12 +21,12 @@ import (
 func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testTeamRepositories := func(teamID int64, repoIds []int64) {
+	testTeamRepositories := func(teamID int64, repoIDs []int64) {
 		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 		assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name)
 		assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name)
-		assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name)
-		for i, rid := range repoIds {
+		assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name)
+		for i, rid := range repoIDs {
 			if rid > 0 {
 				assert.True(t, HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i)
 			}
@@ -52,12 +52,12 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
 
 	// Create repos.
-	repoIds := make([]int64, 0)
+	repoIDs := make([]int64, 0)
 	for i := 0; i < 3; i++ {
 		r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
 		assert.NoError(t, err, "CreateRepository %d", i)
 		if r != nil {
-			repoIds = append(repoIds, r.ID)
+			repoIDs = append(repoIDs, r.ID)
 		}
 	}
 	// Get fresh copy of Owner team after creating repos.
@@ -93,10 +93,10 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 		},
 	}
 	teamRepos := [][]int64{
-		repoIds,
-		repoIds,
+		repoIDs,
+		repoIDs,
 		{},
-		repoIds,
+		repoIDs,
 		{},
 	}
 	for i, team := range teams {
@@ -109,7 +109,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	// Update teams and check repositories.
 	teams[3].IncludesAllRepositories = false
 	teams[4].IncludesAllRepositories = true
-	teamRepos[4] = repoIds
+	teamRepos[4] = repoIDs
 	for i, team := range teams {
 		assert.NoError(t, models.UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name)
 		testTeamRepositories(team.ID, teamRepos[i])
@@ -119,27 +119,27 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: "repo-last"})
 	assert.NoError(t, err, "CreateRepository last")
 	if r != nil {
-		repoIds = append(repoIds, r.ID)
+		repoIDs = append(repoIDs, r.ID)
 	}
-	teamRepos[0] = repoIds
-	teamRepos[1] = repoIds
-	teamRepos[4] = repoIds
+	teamRepos[0] = repoIDs
+	teamRepos[1] = repoIDs
+	teamRepos[4] = repoIDs
 	for i, team := range teams {
 		testTeamRepositories(team.ID, teamRepos[i])
 	}
 
 	// Remove repo and check teams repositories.
-	assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIds[0]), "DeleteRepository")
-	teamRepos[0] = repoIds[1:]
-	teamRepos[1] = repoIds[1:]
-	teamRepos[3] = repoIds[1:3]
-	teamRepos[4] = repoIds[1:]
+	assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository")
+	teamRepos[0] = repoIDs[1:]
+	teamRepos[1] = repoIDs[1:]
+	teamRepos[3] = repoIDs[1:3]
+	teamRepos[4] = repoIDs[1:]
 	for i, team := range teams {
 		testTeamRepositories(team.ID, teamRepos[i])
 	}
 
 	// Wipe created items.
-	for i, rid := range repoIds {
+	for i, rid := range repoIDs {
 		if i > 0 { // first repo already deleted.
 			assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i)
 		}
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 7c10074bc5..4c09a9d588 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -44,7 +44,7 @@ parts:
     source: .
     stage-packages: [ git, sqlite3, openssh-client ]
     build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential]
-    build-snaps: [ go/1.21/stable, node/18/stable ]
+    build-snaps: [ go/1.22/stable, node/20/stable ]
     build-environment:
       - LDFLAGS: ""
     override-pull: |

From 94d06be035bac468129903c9f32e785baf3c1c3b Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Wed, 14 Feb 2024 19:50:31 +0100
Subject: [PATCH 036/679] Extract linguist code to method (#29168)

---
 routers/web/repo/blame.go            | 29 +++++---------------------
 routers/web/repo/view.go             | 29 +++++---------------------
 services/repository/files/content.go | 31 ++++++++++++++++++++++++++++
 3 files changed, 41 insertions(+), 48 deletions(-)

diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index d414779a14..c7875ea0cb 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
+	files_service "code.gitea.io/gitea/services/repository/files"
 )
 
 type blameRow struct {
@@ -247,31 +248,11 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
 func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
 	repoLink := ctx.Repo.RepoLink
 
-	language := ""
-
-	indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
-	if err == nil {
-		defer deleteTemporaryFile()
-
-		filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
-			CachedOnly: true,
-			Attributes: []string{"linguist-language", "gitlab-language"},
-			Filenames:  []string{ctx.Repo.TreePath},
-			IndexFile:  indexFilename,
-			WorkTree:   worktree,
-		})
-		if err != nil {
-			log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
-		}
-
-		language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
-		if language == "" || language == "unspecified" {
-			language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
-		}
-		if language == "unspecified" {
-			language = ""
-		}
+	language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+	if err != nil {
+		log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
 	}
+
 	lines := make([]string, 0)
 	rows := make([]*blameRow, 0)
 	escapeStatus := &charset.EscapeStatus{}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index af3021da11..75051d1995 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -49,6 +49,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
 	issue_service "code.gitea.io/gitea/services/issue"
+	files_service "code.gitea.io/gitea/services/repository/files"
 
 	"github.com/nektos/act/pkg/model"
 
@@ -553,31 +554,11 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			}
 			ctx.Data["NumLinesSet"] = true
 
-			language := ""
-
-			indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
-			if err == nil {
-				defer deleteTemporaryFile()
-
-				filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
-					CachedOnly: true,
-					Attributes: []string{"linguist-language", "gitlab-language"},
-					Filenames:  []string{ctx.Repo.TreePath},
-					IndexFile:  indexFilename,
-					WorkTree:   worktree,
-				})
-				if err != nil {
-					log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
-				}
-
-				language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
-				if language == "" || language == "unspecified" {
-					language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
-				}
-				if language == "unspecified" {
-					language = ""
-				}
+			language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+			if err != nil {
+				log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
 			}
+
 			fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
 			ctx.Data["LexerName"] = lexerName
 			if err != nil {
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index c278d7f835..f2a7677688 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -270,3 +270,34 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 		Content:  content,
 	}, nil
 }
+
+// TryGetContentLanguage tries to get the (linguist) language of the file content
+func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (string, error) {
+	indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitID)
+	if err != nil {
+		return "", err
+	}
+
+	defer deleteTemporaryFile()
+
+	filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
+		CachedOnly: true,
+		Attributes: []string{"linguist-language", "gitlab-language"},
+		Filenames:  []string{treePath},
+		IndexFile:  indexFilename,
+		WorkTree:   worktree,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	language := filename2attribute2info[treePath]["linguist-language"]
+	if language == "" || language == "unspecified" {
+		language = filename2attribute2info[treePath]["gitlab-language"]
+	}
+	if language == "unspecified" {
+		language = ""
+	}
+
+	return language, nil
+}

From f3eb835886031df7a562abc123c3f6011c81eca8 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 15 Feb 2024 05:48:45 +0800
Subject: [PATCH 037/679] Refactor locale&string&template related code (#29165)

Clarify when "string" should be used (and be escaped), and when
"template.HTML" should be used (no need to escape)

And help PRs like  #29059 , to render the error messages correctly.
---
 models/actions/runner.go                      |  2 +-
 models/actions/status.go                      |  2 +-
 models/git/commit_status.go                   |  2 +-
 models/issues/comment.go                      |  4 +-
 models/shared/types/ownertype.go              | 10 ++--
 modules/auth/password/password.go             |  9 +--
 modules/charset/escape_stream.go              |  2 +-
 modules/context/api.go                        |  2 +-
 modules/context/base.go                       |  5 +-
 modules/context/context.go                    | 23 ++++---
 modules/context/context_response.go           |  5 +-
 modules/context/repo.go                       |  3 +-
 modules/csv/csv.go                            |  4 +-
 modules/markup/html.go                        |  2 +-
 modules/markup/markdown/toc.go                |  2 +-
 modules/migration/messenger.go                |  2 +-
 modules/templates/helper.go                   | 46 +++++++++++---
 modules/timeutil/since.go                     | 36 +++++------
 modules/translation/i18n/i18n.go              | 17 +++---
 modules/translation/i18n/i18n_test.go         | 32 ++++++----
 modules/translation/i18n/localestore.go       | 41 +++++++------
 modules/translation/mock.go                   | 15 +++--
 modules/translation/translation.go            | 16 ++++-
 modules/web/middleware/binding.go             | 34 +++++------
 modules/web/middleware/flash.go               | 40 +++++++++----
 routers/api/v1/repo/file.go                   |  6 +-
 routers/api/v1/repo/issue_comment.go          |  2 +-
 routers/web/admin/auths.go                    |  6 +-
 routers/web/auth/password.go                  |  2 +-
 routers/web/feed/convert.go                   | 60 ++++++++++---------
 routers/web/feed/profile.go                   |  2 +-
 routers/web/feed/release.go                   |  4 +-
 routers/web/feed/repo.go                      |  2 +-
 routers/web/org/org.go                        |  4 +-
 routers/web/org/projects.go                   |  4 +-
 routers/web/repo/actions/actions.go           |  4 +-
 routers/web/repo/actions/view.go              |  6 +-
 routers/web/repo/cherry_pick.go               |  4 +-
 routers/web/repo/compare.go                   |  2 +-
 routers/web/repo/editor.go                    | 10 ++--
 routers/web/repo/issue.go                     |  6 +-
 routers/web/repo/issue_content_history.go     |  6 +-
 routers/web/repo/issue_label_test.go          |  2 +-
 routers/web/repo/patch.go                     |  2 +-
 routers/web/repo/projects.go                  |  4 +-
 routers/web/repo/pull.go                      |  4 +-
 routers/web/repo/pull_review.go               |  4 +-
 routers/web/repo/setting/avatar.go            |  4 +-
 routers/web/repo/setting/protected_branch.go  |  2 +-
 routers/web/repo/view.go                      |  2 +-
 routers/web/repo/wiki.go                      |  4 +-
 routers/web/user/home.go                      |  2 +-
 routers/web/user/setting/profile.go           |  6 +-
 routers/web/user/task.go                      |  2 +-
 routers/web/web.go                            |  2 +-
 services/cron/setting.go                      |  6 +-
 services/cron/tasks.go                        |  2 +-
 services/forms/repo_form.go                   |  2 +-
 services/mailer/mail.go                       | 10 ++--
 services/mailer/mail_release.go               |  2 +-
 services/mailer/mail_repo.go                  |  6 +-
 services/mailer/mail_team_invite.go           |  2 +-
 templates/mail/issue/assigned.tmpl            |  4 +-
 templates/mail/issue/default.tmpl             |  2 +-
 templates/mail/notify/repo_transfer.tmpl      |  2 +-
 templates/mail/release.tmpl                   |  2 +-
 templates/repo/editor/cherry_pick.tmpl        |  4 +-
 .../repo/issue/view_content/comments.tmpl     |  6 +-
 templates/repo/issue/view_title.tmpl          |  8 +--
 tests/integration/auth_ldap_test.go           |  4 +-
 tests/integration/branches_test.go            |  4 +-
 tests/integration/pull_merge_test.go          |  2 +-
 tests/integration/release_test.go             | 10 ++--
 tests/integration/repo_branch_test.go         | 18 +++---
 tests/integration/signin_test.go              |  8 +--
 tests/integration/signup_test.go              |  6 +-
 tests/integration/user_test.go                |  4 +-
 77 files changed, 356 insertions(+), 284 deletions(-)

diff --git a/models/actions/runner.go b/models/actions/runner.go
index 4103ba4477..b646146ee6 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -97,7 +97,7 @@ func (r *ActionRunner) StatusName() string {
 }
 
 func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string {
-	return lang.Tr("actions.runners.status." + r.StatusName())
+	return lang.TrString("actions.runners.status." + r.StatusName())
 }
 
 func (r *ActionRunner) IsOnline() bool {
diff --git a/models/actions/status.go b/models/actions/status.go
index c97578f2ac..eda2234137 100644
--- a/models/actions/status.go
+++ b/models/actions/status.go
@@ -41,7 +41,7 @@ func (s Status) String() string {
 
 // LocaleString returns the locale string name of the Status
 func (s Status) LocaleString(lang translation.Locale) string {
-	return lang.Tr("actions.status." + s.String())
+	return lang.TrString("actions.status." + s.String())
 }
 
 // IsDone returns whether the Status is final
diff --git a/models/git/commit_status.go b/models/git/commit_status.go
index 1118b6cc8c..2d1d1bcb06 100644
--- a/models/git/commit_status.go
+++ b/models/git/commit_status.go
@@ -194,7 +194,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string {
 
 // LocaleString returns the locale string name of the Status
 func (status *CommitStatus) LocaleString(lang translation.Locale) string {
-	return lang.Tr("repo.commitstatus." + status.State.String())
+	return lang.TrString("repo.commitstatus." + status.State.String())
 }
 
 // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
diff --git a/models/issues/comment.go b/models/issues/comment.go
index a586caf1b5..fa0eb3cc0f 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -210,12 +210,12 @@ const (
 
 // LocaleString returns the locale string name of the role
 func (r RoleInRepo) LocaleString(lang translation.Locale) string {
-	return lang.Tr("repo.issues.role." + string(r))
+	return lang.TrString("repo.issues.role." + string(r))
 }
 
 // LocaleHelper returns the locale tooltip of the role
 func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
-	return lang.Tr("repo.issues.role." + string(r) + "_helper")
+	return lang.TrString("repo.issues.role." + string(r) + "_helper")
 }
 
 // Comment represents a comment in commit and issue page.
diff --git a/models/shared/types/ownertype.go b/models/shared/types/ownertype.go
index e6fe4e4cfd..a1d46c986f 100644
--- a/models/shared/types/ownertype.go
+++ b/models/shared/types/ownertype.go
@@ -17,13 +17,13 @@ const (
 func (o OwnerType) LocaleString(locale translation.Locale) string {
 	switch o {
 	case OwnerTypeSystemGlobal:
-		return locale.Tr("concept_system_global")
+		return locale.TrString("concept_system_global")
 	case OwnerTypeIndividual:
-		return locale.Tr("concept_user_individual")
+		return locale.TrString("concept_user_individual")
 	case OwnerTypeRepository:
-		return locale.Tr("concept_code_repository")
+		return locale.TrString("concept_code_repository")
 	case OwnerTypeOrganization:
-		return locale.Tr("concept_user_organization")
+		return locale.TrString("concept_user_organization")
 	}
-	return locale.Tr("unknown")
+	return locale.TrString("unknown")
 }
diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go
index 2c7205b708..27074358a9 100644
--- a/modules/auth/password/password.go
+++ b/modules/auth/password/password.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"crypto/rand"
 	"errors"
+	"html/template"
 	"math/big"
 	"strings"
 	"sync"
@@ -121,15 +122,15 @@ func Generate(n int) (string, error) {
 }
 
 // BuildComplexityError builds the error message when password complexity checks fail
-func BuildComplexityError(locale translation.Locale) string {
+func BuildComplexityError(locale translation.Locale) template.HTML {
 	var buffer bytes.Buffer
-	buffer.WriteString(locale.Tr("form.password_complexity"))
+	buffer.WriteString(locale.TrString("form.password_complexity"))
 	buffer.WriteString("<ul>")
 	for _, c := range requiredList {
 		buffer.WriteString("<li>")
-		buffer.WriteString(locale.Tr(c.TrNameOne))
+		buffer.WriteString(locale.TrString(c.TrNameOne))
 		buffer.WriteString("</li>")
 	}
 	buffer.WriteString("</ul>")
-	return buffer.String()
+	return template.HTML(buffer.String())
 }
diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go
index 3f08fd94a4..29943eb858 100644
--- a/modules/charset/escape_stream.go
+++ b/modules/charset/escape_stream.go
@@ -173,7 +173,7 @@ func (e *escapeStreamer) ambiguousRune(r, c rune) error {
 		Val: "ambiguous-code-point",
 	}, html.Attribute{
 		Key: "data-tooltip-content",
-		Val: e.locale.Tr("repo.ambiguous_character", r, c),
+		Val: e.locale.TrString("repo.ambiguous_character", r, c),
 	}); err != nil {
 		return err
 	}
diff --git a/modules/context/api.go b/modules/context/api.go
index e226264a87..f8bc682fed 100644
--- a/modules/context/api.go
+++ b/modules/context/api.go
@@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler {
 // NotFound handles 404s for APIContext
 // String will replace message, errors will be added to a slice
 func (ctx *APIContext) NotFound(objs ...any) {
-	message := ctx.Tr("error.not_found")
+	message := ctx.Locale.TrString("error.not_found")
 	var errors []string
 	for _, obj := range objs {
 		// Ignore nil
diff --git a/modules/context/base.go b/modules/context/base.go
index 8df1dde866..fa05850a16 100644
--- a/modules/context/base.go
+++ b/modules/context/base.go
@@ -6,6 +6,7 @@ package context
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"io"
 	"net/http"
 	"net/url"
@@ -286,11 +287,11 @@ func (b *Base) cleanUp() {
 	}
 }
 
-func (b *Base) Tr(msg string, args ...any) string {
+func (b *Base) Tr(msg string, args ...any) template.HTML {
 	return b.Locale.Tr(msg, args...)
 }
 
-func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string {
+func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
 	return b.Locale.TrN(cnt, key1, keyN, args...)
 }
 
diff --git a/modules/context/context.go b/modules/context/context.go
index d19c5d1198..4d367b3242 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -6,7 +6,7 @@ package context
 
 import (
 	"context"
-	"html"
+	"fmt"
 	"html/template"
 	"io"
 	"net/http"
@@ -71,16 +71,6 @@ func init() {
 	})
 }
 
-// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
-// This is useful if the locale message is intended to only produce HTML content.
-func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
-	trArgs := make([]any, len(args))
-	for i, arg := range args {
-		trArgs[i] = html.EscapeString(arg)
-	}
-	return ctx.Locale.Tr(msg, trArgs...)
-}
-
 type webContextKeyType struct{}
 
 var WebContextKey = webContextKeyType{}
@@ -253,6 +243,13 @@ func (ctx *Context) JSONOK() {
 	ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
 }
 
-func (ctx *Context) JSONError(msg string) {
-	ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg})
+func (ctx *Context) JSONError(msg any) {
+	switch v := msg.(type) {
+	case string:
+		ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
+	case template.HTML:
+		ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
+	default:
+		panic(fmt.Sprintf("unsupported type: %T", msg))
+	}
 }
diff --git a/modules/context/context_response.go b/modules/context/context_response.go
index 5729865561..d9102b77bd 100644
--- a/modules/context/context_response.go
+++ b/modules/context/context_response.go
@@ -98,12 +98,11 @@ func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (stri
 }
 
 // RenderWithErr used for page has form validation but need to prompt error to users.
-func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form any) {
+func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
 	if form != nil {
 		middleware.AssignForm(form, ctx.Data)
 	}
-	ctx.Flash.ErrorMsg = msg
-	ctx.Data["Flash"] = ctx.Flash
+	ctx.Flash.Error(msg, true)
 	ctx.HTML(http.StatusOK, tpl)
 }
 
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 75ebfec705..3ff7209c4c 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -6,6 +6,7 @@ package context
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"html"
 	"net/http"
@@ -85,7 +86,7 @@ func (r *Repository) CanCreateBranch() bool {
 func RepoMustNotBeArchived() func(ctx *Context) {
 	return func(ctx *Context) {
 		if ctx.Repo.Repository.IsArchived {
-			ctx.NotFound("IsArchived", fmt.Errorf(ctx.Tr("repo.archive.title")))
+			ctx.NotFound("IsArchived", errors.New(ctx.Locale.TrString("repo.archive.title")))
 		}
 	}
 }
diff --git a/modules/csv/csv.go b/modules/csv/csv.go
index c5497befe7..35c5d6ab67 100644
--- a/modules/csv/csv.go
+++ b/modules/csv/csv.go
@@ -123,9 +123,9 @@ func guessDelimiter(data []byte) rune {
 func FormatError(err error, locale translation.Locale) (string, error) {
 	if perr, ok := err.(*stdcsv.ParseError); ok {
 		if perr.Err == stdcsv.ErrFieldCount {
-			return locale.Tr("repo.error.csv.invalid_field_count", perr.Line), nil
+			return locale.TrString("repo.error.csv.invalid_field_count", perr.Line), nil
 		}
-		return locale.Tr("repo.error.csv.unexpected", perr.Line, perr.Column), nil
+		return locale.TrString("repo.error.csv.unexpected", perr.Line, perr.Column), nil
 	}
 
 	return "", err
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 33dc1e9086..b7291823b5 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -804,7 +804,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
 		// indicate that in the text by appending (comment)
 		if m[4] != -1 && m[5] != -1 {
 			if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
-				text += " " + locale.Tr("repo.from_comment")
+				text += " " + locale.TrString("repo.from_comment")
 			} else {
 				text += " (comment)"
 			}
diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go
index 9602040931..38f744a25f 100644
--- a/modules/markup/markdown/toc.go
+++ b/modules/markup/markdown/toc.go
@@ -21,7 +21,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str
 		details.SetAttributeString(k, []byte(v))
 	}
 
-	summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc"))))
+	summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc"))))
 	details.AppendChild(details, summary)
 	ul := ast.NewList('-')
 	details.AppendChild(details, ul)
diff --git a/modules/migration/messenger.go b/modules/migration/messenger.go
index 924aac9769..6f9cad3f10 100644
--- a/modules/migration/messenger.go
+++ b/modules/migration/messenger.go
@@ -3,7 +3,7 @@
 
 package migration
 
-// Messenger is a formatting function similar to i18n.Tr
+// Messenger is a formatting function similar to i18n.TrString
 type Messenger func(key string, args ...any)
 
 // NilMessenger represents an empty formatting function
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 96cdd9ca46..9ff5d8927f 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -36,7 +36,7 @@ func NewFuncMap() template.FuncMap {
 		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
 		"Eval":        Eval,
 		"Safe":        Safe,
-		"Escape":      html.EscapeString,
+		"Escape":      Escape,
 		"QueryEscape": url.QueryEscape,
 		"JSEscape":    template.JSEscapeString,
 		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
@@ -159,7 +159,7 @@ func NewFuncMap() template.FuncMap {
 		"RenderCodeBlock":  RenderCodeBlock,
 		"RenderIssueTitle": RenderIssueTitle,
 		"RenderEmoji":      RenderEmoji,
-		"RenderEmojiPlain": emoji.ReplaceAliases,
+		"RenderEmojiPlain": RenderEmojiPlain,
 		"ReactionToEmoji":  ReactionToEmoji,
 
 		"RenderMarkdownToHtml": RenderMarkdownToHtml,
@@ -180,13 +180,45 @@ func NewFuncMap() template.FuncMap {
 }
 
 // Safe render raw as HTML
-func Safe(raw string) template.HTML {
-	return template.HTML(raw)
+func Safe(s any) template.HTML {
+	switch v := s.(type) {
+	case string:
+		return template.HTML(v)
+	case template.HTML:
+		return v
+	}
+	panic(fmt.Sprintf("unexpected type %T", s))
 }
 
-// Str2html render Markdown text to HTML
-func Str2html(raw string) template.HTML {
-	return template.HTML(markup.Sanitize(raw))
+// Str2html sanitizes the input by pre-defined markdown rules
+func Str2html(s any) template.HTML {
+	switch v := s.(type) {
+	case string:
+		return template.HTML(markup.Sanitize(v))
+	case template.HTML:
+		return template.HTML(markup.Sanitize(string(v)))
+	}
+	panic(fmt.Sprintf("unexpected type %T", s))
+}
+
+func Escape(s any) template.HTML {
+	switch v := s.(type) {
+	case string:
+		return template.HTML(html.EscapeString(v))
+	case template.HTML:
+		return v
+	}
+	panic(fmt.Sprintf("unexpected type %T", s))
+}
+
+func RenderEmojiPlain(s any) any {
+	switch v := s.(type) {
+	case string:
+		return emoji.ReplaceAliases(v)
+	case template.HTML:
+		return template.HTML(emoji.ReplaceAliases(string(v)))
+	}
+	panic(fmt.Sprintf("unexpected type %T", s))
 }
 
 // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go
index 1cb3c4f288..dfaa0e3e3a 100644
--- a/modules/timeutil/since.go
+++ b/modules/timeutil/since.go
@@ -28,54 +28,54 @@ func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) {
 	switch {
 	case diff <= 0:
 		diff = 0
-		diffStr = lang.Tr("tool.now")
+		diffStr = lang.TrString("tool.now")
 	case diff < 2:
 		diff = 0
-		diffStr = lang.Tr("tool.1s")
+		diffStr = lang.TrString("tool.1s")
 	case diff < 1*Minute:
-		diffStr = lang.Tr("tool.seconds", diff)
+		diffStr = lang.TrString("tool.seconds", diff)
 		diff = 0
 
 	case diff < 2*Minute:
 		diff -= 1 * Minute
-		diffStr = lang.Tr("tool.1m")
+		diffStr = lang.TrString("tool.1m")
 	case diff < 1*Hour:
-		diffStr = lang.Tr("tool.minutes", diff/Minute)
+		diffStr = lang.TrString("tool.minutes", diff/Minute)
 		diff -= diff / Minute * Minute
 
 	case diff < 2*Hour:
 		diff -= 1 * Hour
-		diffStr = lang.Tr("tool.1h")
+		diffStr = lang.TrString("tool.1h")
 	case diff < 1*Day:
-		diffStr = lang.Tr("tool.hours", diff/Hour)
+		diffStr = lang.TrString("tool.hours", diff/Hour)
 		diff -= diff / Hour * Hour
 
 	case diff < 2*Day:
 		diff -= 1 * Day
-		diffStr = lang.Tr("tool.1d")
+		diffStr = lang.TrString("tool.1d")
 	case diff < 1*Week:
-		diffStr = lang.Tr("tool.days", diff/Day)
+		diffStr = lang.TrString("tool.days", diff/Day)
 		diff -= diff / Day * Day
 
 	case diff < 2*Week:
 		diff -= 1 * Week
-		diffStr = lang.Tr("tool.1w")
+		diffStr = lang.TrString("tool.1w")
 	case diff < 1*Month:
-		diffStr = lang.Tr("tool.weeks", diff/Week)
+		diffStr = lang.TrString("tool.weeks", diff/Week)
 		diff -= diff / Week * Week
 
 	case diff < 2*Month:
 		diff -= 1 * Month
-		diffStr = lang.Tr("tool.1mon")
+		diffStr = lang.TrString("tool.1mon")
 	case diff < 1*Year:
-		diffStr = lang.Tr("tool.months", diff/Month)
+		diffStr = lang.TrString("tool.months", diff/Month)
 		diff -= diff / Month * Month
 
 	case diff < 2*Year:
 		diff -= 1 * Year
-		diffStr = lang.Tr("tool.1y")
+		diffStr = lang.TrString("tool.1y")
 	default:
-		diffStr = lang.Tr("tool.years", diff/Year)
+		diffStr = lang.TrString("tool.years", diff/Year)
 		diff -= (diff / Year) * Year
 	}
 	return diff, diffStr
@@ -97,10 +97,10 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
 	diff := now.Unix() - then.Unix()
 
 	if then.After(now) {
-		return lang.Tr("tool.future")
+		return lang.TrString("tool.future")
 	}
 	if diff == 0 {
-		return lang.Tr("tool.now")
+		return lang.TrString("tool.now")
 	}
 
 	var timeStr, diffStr string
@@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
 	return strings.TrimPrefix(timeStr, ", ")
 }
 
-func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
+func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
 	friendlyText := then.Format("2006-01-02 15:04:05 -07:00")
 
 	// document: https://github.com/github/relative-time-element
diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go
index 42475545b3..1555cd961e 100644
--- a/modules/translation/i18n/i18n.go
+++ b/modules/translation/i18n/i18n.go
@@ -4,26 +4,25 @@
 package i18n
 
 import (
+	"html/template"
 	"io"
 )
 
 var DefaultLocales = NewLocaleStore()
 
 type Locale interface {
-	// Tr translates a given key and arguments for a language
-	Tr(trKey string, trArgs ...any) string
-	// Has reports if a locale has a translation for a given key
-	Has(trKey string) bool
+	// TrString translates a given key and arguments for a language
+	TrString(trKey string, trArgs ...any) string
+	// TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
+	TrHTML(trKey string, trArgs ...any) template.HTML
+	// HasKey reports if a locale has a translation for a given key
+	HasKey(trKey string) bool
 }
 
 // LocaleStore provides the functions common to all locale stores
 type LocaleStore interface {
 	io.Closer
 
-	// Tr translates a given key and arguments for a language
-	Tr(lang, trKey string, trArgs ...any) string
-	// Has reports if a locale has a translation for a given key
-	Has(lang, trKey string) bool
 	// SetDefaultLang sets the default language to fall back to
 	SetDefaultLang(lang string)
 	// ListLangNameDesc provides paired slices of language names to descriptors
@@ -45,7 +44,7 @@ func ResetDefaultLocales() {
 	DefaultLocales = NewLocaleStore()
 }
 
-// GetLocales returns the locale from the default locales
+// GetLocale returns the locale from the default locales
 func GetLocale(lang string) (Locale, bool) {
 	return DefaultLocales.Locale(lang)
 }
diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go
index 1d1be43318..ffe69a74df 100644
--- a/modules/translation/i18n/i18n_test.go
+++ b/modules/translation/i18n/i18n_test.go
@@ -17,7 +17,7 @@ fmt = %[1]s %[2]s
 
 [section]
 sub = Sub String
-mixed = test value; <span style="color: red\; background: none;">more text</span>
+mixed = test value; <span style="color: red\; background: none;">%s</span>
 `)
 
 	testData2 := []byte(`
@@ -32,29 +32,33 @@ sub = Changed Sub String
 	assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
 	ls.SetDefaultLang("lang1")
 
-	result := ls.Tr("lang1", "fmt", "a", "b")
+	lang1, _ := ls.Locale("lang1")
+	lang2, _ := ls.Locale("lang2")
+
+	result := lang1.TrString("fmt", "a", "b")
 	assert.Equal(t, "a b", result)
 
-	result = ls.Tr("lang2", "fmt", "a", "b")
+	result = lang2.TrString("fmt", "a", "b")
 	assert.Equal(t, "b a", result)
 
-	result = ls.Tr("lang1", "section.sub")
+	result = lang1.TrString("section.sub")
 	assert.Equal(t, "Sub String", result)
 
-	result = ls.Tr("lang2", "section.sub")
+	result = lang2.TrString("section.sub")
 	assert.Equal(t, "Changed Sub String", result)
 
-	result = ls.Tr("", ".dot.name")
+	langNone, _ := ls.Locale("none")
+	result = langNone.TrString(".dot.name")
 	assert.Equal(t, "Dot Name", result)
 
-	result = ls.Tr("lang2", "section.mixed")
-	assert.Equal(t, `test value; <span style="color: red; background: none;">more text</span>`, result)
+	result2 := lang2.TrHTML("section.mixed", "a&b")
+	assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
 
 	langs, descs := ls.ListLangNameDesc()
 	assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
 	assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
 
-	found := ls.Has("lang1", "no-such")
+	found := lang1.HasKey("no-such")
 	assert.False(t, found)
 	assert.NoError(t, ls.Close())
 }
@@ -72,9 +76,10 @@ c=22
 
 	ls := NewLocaleStore()
 	assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
-	assert.Equal(t, "11", ls.Tr("lang1", "a"))
-	assert.Equal(t, "21", ls.Tr("lang1", "b"))
-	assert.Equal(t, "22", ls.Tr("lang1", "c"))
+	lang1, _ := ls.Locale("lang1")
+	assert.Equal(t, "11", lang1.TrString("a"))
+	assert.Equal(t, "21", lang1.TrString("b"))
+	assert.Equal(t, "22", lang1.TrString("c"))
 }
 
 func TestLocaleStoreQuirks(t *testing.T) {
@@ -110,8 +115,9 @@ func TestLocaleStoreQuirks(t *testing.T) {
 	for _, testData := range testDataList {
 		ls := NewLocaleStore()
 		err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
+		lang1, _ := ls.Locale("lang1")
 		assert.NoError(t, err, testData.hint)
-		assert.Equal(t, testData.out, ls.Tr("lang1", "a"), testData.hint)
+		assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
 		assert.NoError(t, ls.Close())
 	}
 
diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go
index f5a951a79f..69cc9fd91d 100644
--- a/modules/translation/i18n/localestore.go
+++ b/modules/translation/i18n/localestore.go
@@ -5,6 +5,8 @@ package i18n
 
 import (
 	"fmt"
+	"html/template"
+	"slices"
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -18,6 +20,8 @@ type locale struct {
 	idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
 }
 
+var _ Locale = (*locale)(nil)
+
 type localeStore struct {
 	// After initializing has finished, these fields are read-only.
 	langNames []string
@@ -85,20 +89,6 @@ func (store *localeStore) SetDefaultLang(lang string) {
 	store.defaultLang = lang
 }
 
-// Tr translates content to target language. fall back to default language.
-func (store *localeStore) Tr(lang, trKey string, trArgs ...any) string {
-	l, _ := store.Locale(lang)
-
-	return l.Tr(trKey, trArgs...)
-}
-
-// Has returns whether the given language has a translation for the provided key
-func (store *localeStore) Has(lang, trKey string) bool {
-	l, _ := store.Locale(lang)
-
-	return l.Has(trKey)
-}
-
 // Locale returns the locale for the lang or the default language
 func (store *localeStore) Locale(lang string) (Locale, bool) {
 	l, found := store.localeMap[lang]
@@ -113,13 +103,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
 	return l, found
 }
 
-// Close implements io.Closer
 func (store *localeStore) Close() error {
 	return nil
 }
 
-// Tr translates content to locale language. fall back to default language.
-func (l *locale) Tr(trKey string, trArgs ...any) string {
+func (l *locale) TrString(trKey string, trArgs ...any) string {
 	format := trKey
 
 	idx, ok := l.store.trKeyToIdxMap[trKey]
@@ -141,8 +129,23 @@ func (l *locale) Tr(trKey string, trArgs ...any) string {
 	return msg
 }
 
-// Has returns whether a key is present in this locale or not
-func (l *locale) Has(trKey string) bool {
+func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
+	args := slices.Clone(trArgs)
+	for i, v := range args {
+		switch v := v.(type) {
+		case string:
+			args[i] = template.HTML(template.HTMLEscapeString(v))
+		case fmt.Stringer:
+			args[i] = template.HTMLEscapeString(v.String())
+		default: // int, float, include template.HTML
+			// do nothing, just use it
+		}
+	}
+	return template.HTML(l.TrString(trKey, args...))
+}
+
+// HasKey returns whether a key is present in this locale or not
+func (l *locale) HasKey(trKey string) bool {
 	idx, ok := l.store.trKeyToIdxMap[trKey]
 	if !ok {
 		return false
diff --git a/modules/translation/mock.go b/modules/translation/mock.go
index 2d0cb17324..1f0559f38d 100644
--- a/modules/translation/mock.go
+++ b/modules/translation/mock.go
@@ -3,7 +3,10 @@
 
 package translation
 
-import "fmt"
+import (
+	"fmt"
+	"html/template"
+)
 
 // MockLocale provides a mocked locale without any translations
 type MockLocale struct{}
@@ -14,12 +17,16 @@ func (l MockLocale) Language() string {
 	return "en"
 }
 
-func (l MockLocale) Tr(s string, _ ...any) string {
+func (l MockLocale) TrString(s string, _ ...any) string {
 	return s
 }
 
-func (l MockLocale) TrN(_cnt any, key1, _keyN string, _args ...any) string {
-	return key1
+func (l MockLocale) Tr(s string, a ...any) template.HTML {
+	return template.HTML(s)
+}
+
+func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
+	return template.HTML(key1)
 }
 
 func (l MockLocale) PrettyNumber(v any) string {
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index dba4de6607..b7c18f610a 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -5,6 +5,7 @@ package translation
 
 import (
 	"context"
+	"html/template"
 	"sort"
 	"strings"
 	"sync"
@@ -27,8 +28,11 @@ var ContextKey any = &contextKey{}
 // Locale represents an interface to translation
 type Locale interface {
 	Language() string
-	Tr(string, ...any) string
-	TrN(cnt any, key1, keyN string, args ...any) string
+	TrString(string, ...any) string
+
+	Tr(key string, args ...any) template.HTML
+	TrN(cnt any, key1, keyN string, args ...any) template.HTML
+
 	PrettyNumber(v any) string
 }
 
@@ -144,6 +148,8 @@ type locale struct {
 	msgPrinter     *message.Printer
 }
 
+var _ Locale = (*locale)(nil)
+
 // NewLocale return a locale
 func NewLocale(lang string) Locale {
 	if lock != nil {
@@ -216,8 +222,12 @@ var trNLangRules = map[string]func(int64) int{
 	},
 }
 
+func (l *locale) Tr(s string, args ...any) template.HTML {
+	return l.TrHTML(s, args...)
+}
+
 // TrN returns translated message for plural text translation
-func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string {
+func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
 	var c int64
 	if t, ok := cnt.(int); ok {
 		c = int64(t)
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go
index d9bcdf3b2a..43e1bbc70e 100644
--- a/modules/web/middleware/binding.go
+++ b/modules/web/middleware/binding.go
@@ -104,40 +104,40 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
 
 			trName := field.Tag.Get("locale")
 			if len(trName) == 0 {
-				trName = l.Tr("form." + field.Name)
+				trName = l.TrString("form." + field.Name)
 			} else {
-				trName = l.Tr(trName)
+				trName = l.TrString(trName)
 			}
 
 			switch errs[0].Classification {
 			case binding.ERR_REQUIRED:
-				data["ErrorMsg"] = trName + l.Tr("form.require_error")
+				data["ErrorMsg"] = trName + l.TrString("form.require_error")
 			case binding.ERR_ALPHA_DASH:
-				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
+				data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
 			case binding.ERR_ALPHA_DASH_DOT:
-				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
+				data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
 			case validation.ErrGitRefName:
-				data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error")
+				data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
 			case binding.ERR_SIZE:
-				data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field))
+				data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
 			case binding.ERR_MIN_SIZE:
-				data["ErrorMsg"] = trName + l.Tr("form.min_size_error", GetMinSize(field))
+				data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
 			case binding.ERR_MAX_SIZE:
-				data["ErrorMsg"] = trName + l.Tr("form.max_size_error", GetMaxSize(field))
+				data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
 			case binding.ERR_EMAIL:
-				data["ErrorMsg"] = trName + l.Tr("form.email_error")
+				data["ErrorMsg"] = trName + l.TrString("form.email_error")
 			case binding.ERR_URL:
-				data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
 			case binding.ERR_INCLUDE:
-				data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
+				data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
 			case validation.ErrGlobPattern:
-				data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
 			case validation.ErrRegexPattern:
-				data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
 			case validation.ErrUsername:
-				data["ErrorMsg"] = trName + l.Tr("form.username_error")
+				data["ErrorMsg"] = trName + l.TrString("form.username_error")
 			case validation.ErrInvalidGroupTeamMap:
-				data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
 			default:
 				msg := errs[0].Classification
 				if msg != "" && errs[0].Message != "" {
@@ -146,7 +146,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
 
 				msg += errs[0].Message
 				if msg == "" {
-					msg = l.Tr("form.unknown_error")
+					msg = l.TrString("form.unknown_error")
 				}
 				data["ErrorMsg"] = trName + ": " + msg
 			}
diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go
index 41f3aac27c..88da2049a4 100644
--- a/modules/web/middleware/flash.go
+++ b/modules/web/middleware/flash.go
@@ -3,7 +3,11 @@
 
 package middleware
 
-import "net/url"
+import (
+	"fmt"
+	"html/template"
+	"net/url"
+)
 
 // Flash represents a one time data transfer between two requests.
 type Flash struct {
@@ -26,26 +30,36 @@ func (f *Flash) set(name, msg string, current ...bool) {
 	}
 }
 
+func flashMsgStringOrHTML(msg any) string {
+	switch v := msg.(type) {
+	case string:
+		return v
+	case template.HTML:
+		return string(v)
+	}
+	panic(fmt.Sprintf("unknown type: %T", msg))
+}
+
 // Error sets error message
-func (f *Flash) Error(msg string, current ...bool) {
-	f.ErrorMsg = msg
-	f.set("error", msg, current...)
+func (f *Flash) Error(msg any, current ...bool) {
+	f.ErrorMsg = flashMsgStringOrHTML(msg)
+	f.set("error", f.ErrorMsg, current...)
 }
 
 // Warning sets warning message
-func (f *Flash) Warning(msg string, current ...bool) {
-	f.WarningMsg = msg
-	f.set("warning", msg, current...)
+func (f *Flash) Warning(msg any, current ...bool) {
+	f.WarningMsg = flashMsgStringOrHTML(msg)
+	f.set("warning", f.WarningMsg, current...)
 }
 
 // Info sets info message
-func (f *Flash) Info(msg string, current ...bool) {
-	f.InfoMsg = msg
-	f.set("info", msg, current...)
+func (f *Flash) Info(msg any, current ...bool) {
+	f.InfoMsg = flashMsgStringOrHTML(msg)
+	f.set("info", f.InfoMsg, current...)
 }
 
 // Success sets success message
-func (f *Flash) Success(msg string, current ...bool) {
-	f.SuccessMsg = msg
-	f.set("success", msg, current...)
+func (f *Flash) Success(msg any, current ...bool) {
+	f.SuccessMsg = flashMsgStringOrHTML(msg)
+	f.set("success", f.SuccessMsg, current...)
 }
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 065d6bf8b2..370e4753f3 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -762,13 +762,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch
 	}
 	message := ""
 	if len(createFiles) != 0 {
-		message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
+		message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
 	}
 	if len(updateFiles) != 0 {
-		message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
+		message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
 	}
 	if len(deleteFiles) != 0 {
-		message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
+		message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
 	}
 	return strings.Trim(message, "\n")
 }
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 4db2c68a79..2b7a8f7ba1 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -395,7 +395,7 @@ func CreateIssueComment(ctx *context.APIContext) {
 	}
 
 	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
-		ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
+		ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
 		return
 	}
 
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 2cf63c646d..7fdd18dfae 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -210,16 +210,16 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
 func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
 	if util.IsEmptyString(form.SSPISeparatorReplacement) {
 		ctx.Data["Err_SSPISeparatorReplacement"] = true
-		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
+		return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
 	}
 	if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
 		ctx.Data["Err_SSPISeparatorReplacement"] = true
-		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
+		return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
 	}
 
 	if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
 		ctx.Data["Err_SSPIDefaultLanguage"] = true
-		return nil, errors.New(ctx.Tr("form.lang_select_error"))
+		return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
 	}
 
 	return &sspi.Source{
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 5af1696a64..c23379b87a 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -37,7 +37,7 @@ func ForgotPasswd(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
 
 	if setting.MailService == nil {
-		log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin"))
+		log.Warn("no mail service configured")
 		ctx.Data["IsResetDisable"] = true
 		ctx.HTML(http.StatusOK, tplForgotPassword)
 		return
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 66b01d3680..1e040ed819 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -6,6 +6,7 @@ package feed
 import (
 	"fmt"
 	"html"
+	"html/template"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -79,119 +80,120 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 
 		// title
 		title = act.ActUser.DisplayName() + " "
+		var titleExtra template.HTML
 		switch act.OpType {
 		case activities_model.ActionCreateRepo:
-			title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
 			link.Href = act.GetRepoAbsoluteLink(ctx)
 		case activities_model.ActionRenameRepo:
-			title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
 			link.Href = act.GetRepoAbsoluteLink(ctx)
 		case activities_model.ActionCommitRepo:
 			link.Href = toBranchLink(ctx, act)
 			if len(act.Content) != 0 {
-				title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+				titleExtra = ctx.Locale.Tr("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
 			} else {
-				title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+				titleExtra = ctx.Locale.Tr("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
 			}
 		case activities_model.ActionCreateIssue:
 			link.Href = toIssueLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionCreatePullRequest:
 			link.Href = toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionTransferRepo:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
 		case activities_model.ActionPushTag:
 			link.Href = toTagLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
 		case activities_model.ActionCommentIssue:
 			issueLink := toIssueLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = issueLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionMergePullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionAutoMergePullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionCloseIssue:
 			issueLink := toIssueLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = issueLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionReopenIssue:
 			issueLink := toIssueLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = issueLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionClosePullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionReopenPullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionDeleteTag:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
 		case activities_model.ActionDeleteBranch:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
 		case activities_model.ActionMirrorSyncPush:
 			srcLink := toSrcLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = srcLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
 		case activities_model.ActionMirrorSyncCreate:
 			srcLink := toSrcLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = srcLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
 		case activities_model.ActionMirrorSyncDelete:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
 		case activities_model.ActionApprovePullRequest:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionRejectPullRequest:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionCommentPull:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionPublishRelease:
 			releaseLink := toReleaseLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = releaseLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
+			titleExtra = ctx.Locale.Tr("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
 		case activities_model.ActionPullReviewDismissed:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
+			titleExtra = ctx.Locale.Tr("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
 		case activities_model.ActionStarRepo:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
 		case activities_model.ActionWatchRepo:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
 		default:
 			return nil, fmt.Errorf("unknown action type: %v", act.OpType)
 		}
@@ -233,7 +235,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 			case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest:
 				desc = act.GetIssueTitle(ctx)
 			case activities_model.ActionPullReviewDismissed:
-				desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
+				desc = ctx.Locale.TrString("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
 			}
 		}
 		if len(content) == 0 {
@@ -241,7 +243,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 		}
 
 		items = append(items, &feeds.Item{
-			Title:       title,
+			Title:       template.HTMLEscapeString(title) + string(titleExtra),
 			Link:        link,
 			Description: desc,
 			IsPermaLink: "false",
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 04f84c0c8d..3feca68d61 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -56,7 +56,7 @@ func showUserFeed(ctx *context.Context, formatType string) {
 	}
 
 	feed := &feeds.Feed{
-		Title:       ctx.Tr("home.feed_of", ctx.ContextUser.DisplayName()),
+		Title:       ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()),
 		Link:        &feeds.Link{Href: ctx.ContextUser.HTMLURL()},
 		Description: ctxUserDescription,
 		Created:     time.Now(),
diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go
index 57b0c92766..558c03dba7 100644
--- a/routers/web/feed/release.go
+++ b/routers/web/feed/release.go
@@ -28,10 +28,10 @@ func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleas
 	var link *feeds.Link
 
 	if isReleasesOnly {
-		title = ctx.Tr("repo.release.releases_for", repo.FullName())
+		title = ctx.Locale.TrString("repo.release.releases_for", repo.FullName())
 		link = &feeds.Link{Href: repo.HTMLURL() + "/release"}
 	} else {
-		title = ctx.Tr("repo.release.tags_for", repo.FullName())
+		title = ctx.Locale.TrString("repo.release.tags_for", repo.FullName())
 		link = &feeds.Link{Href: repo.HTMLURL() + "/tags"}
 	}
 
diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go
index 5fcad26779..51c24510c7 100644
--- a/routers/web/feed/repo.go
+++ b/routers/web/feed/repo.go
@@ -27,7 +27,7 @@ func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType
 	}
 
 	feed := &feeds.Feed{
-		Title:       ctx.Tr("home.feed_of", repo.FullName()),
+		Title:       ctx.Locale.TrString("home.feed_of", repo.FullName()),
 		Link:        &feeds.Link{Href: repo.HTMLURL()},
 		Description: repo.Description,
 		Created:     time.Now(),
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
index 52f8df8a1c..1e4544730e 100644
--- a/routers/web/org/org.go
+++ b/routers/web/org/org.go
@@ -29,7 +29,7 @@ func Create(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("new_org")
 	ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
 	if !ctx.Doer.CanCreateOrganization() {
-		ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+		ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
 		return
 	}
 	ctx.HTML(http.StatusOK, tplCreateOrg)
@@ -41,7 +41,7 @@ func CreatePost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("new_org")
 
 	if !ctx.Doer.CanCreateOrganization() {
-		ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+		ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
 		return
 	}
 
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index f65cc6e679..f062127d24 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -353,7 +353,7 @@ func ViewProject(ctx *context.Context) {
 	}
 
 	if boards[0].ID == 0 {
-		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
+		boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
 	}
 
 	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@@ -679,7 +679,7 @@ func MoveIssues(ctx *context.Context) {
 		board = &project_model.Board{
 			ID:        0,
 			ProjectID: project.ID,
-			Title:     ctx.Tr("repo.projects.type.uncategorized"),
+			Title:     ctx.Locale.TrString("repo.projects.type.uncategorized"),
 		}
 	} else {
 		board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 5f6a1ec36a..19aca26711 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -100,7 +100,7 @@ func List(ctx *context.Context) {
 			}
 			wf, err := model.ReadWorkflow(bytes.NewReader(content))
 			if err != nil {
-				workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error())
+				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
 				workflows = append(workflows, workflow)
 				continue
 			}
@@ -115,7 +115,7 @@ func List(ctx *context.Context) {
 						continue
 					}
 					if !allRunnerLabels.Contains(ro) {
-						workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_online_runner_helper", ro)
+						workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
 						break
 					}
 				}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 9cda30d23d..59fb25b680 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -168,8 +168,8 @@ func ViewPost(ctx *context_module.Context) {
 		Link: run.RefLink(),
 	}
 	resp.State.Run.Commit = ViewCommit{
-		LocaleCommit:   ctx.Tr("actions.runs.commit"),
-		LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
+		LocaleCommit:   ctx.Locale.TrString("actions.runs.commit"),
+		LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"),
 		ShortSha:       base.ShortSha(run.CommitSHA),
 		Link:           fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
 		Pusher:         pusher,
@@ -194,7 +194,7 @@ func ViewPost(ctx *context_module.Context) {
 	resp.State.CurrentJob.Title = current.Name
 	resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
 	if run.NeedApproval {
-		resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
+		resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc")
 	}
 	resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
 	resp.Logs.StepsLog = make([]*ViewStepLog, 0)          // marshal to '[]' instead fo 'null' in json
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
index 25dd881219..8de54d569f 100644
--- a/routers/web/repo/cherry_pick.go
+++ b/routers/web/repo/cherry_pick.go
@@ -104,9 +104,9 @@ func CherryPickPost(ctx *context.Context) {
 	message := strings.TrimSpace(form.CommitSummary)
 	if message == "" {
 		if form.Revert {
-			message = ctx.Tr("repo.commit.revert-header", sha)
+			message = ctx.Locale.TrString("repo.commit.revert-header", sha)
 		} else {
-			message = ctx.Tr("repo.commit.cherry-pick-header", sha)
+			message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
 		}
 	}
 
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index a3593815b8..67d41cf807 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -126,7 +126,7 @@ func setCsvCompareContext(ctx *context.Context) {
 			return CsvDiffResult{nil, ""}
 		}
 
-		errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
+		errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large"))
 
 		csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
 			if blob == nil {
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 85d40e7820..bc3cb8801d 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -262,9 +262,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 	message := strings.TrimSpace(form.CommitSummary)
 	if len(message) == 0 {
 		if isNewFile {
-			message = ctx.Tr("repo.editor.add", form.TreePath)
+			message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
 		} else {
-			message = ctx.Tr("repo.editor.update", form.TreePath)
+			message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
 		}
 	}
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@@ -415,7 +415,7 @@ func DiffPreviewPost(ctx *context.Context) {
 	}
 
 	if diff.NumFiles == 0 {
-		ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show"))
+		ctx.PlainText(http.StatusOK, ctx.Locale.TrString("repo.editor.no_changes_to_show"))
 		return
 	}
 	ctx.Data["File"] = diff.Files[0]
@@ -482,7 +482,7 @@ func DeleteFilePost(ctx *context.Context) {
 
 	message := strings.TrimSpace(form.CommitSummary)
 	if len(message) == 0 {
-		message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
+		message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
 	}
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
 	if len(form.CommitMessage) > 0 {
@@ -691,7 +691,7 @@ func UploadFilePost(ctx *context.Context) {
 		if dir == "" {
 			dir = "/"
 		}
-		message = ctx.Tr("repo.editor.upload_files_to_dir", dir)
+		message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
 	}
 
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index a85f6e7666..d5e49960a1 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1036,7 +1036,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
 	})
 	if err != nil {
 		log.Debug("render flash error: %v", err)
-		flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
+		flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates")
 	}
 	return flashError
 }
@@ -1655,7 +1655,7 @@ func ViewIssue(ctx *context.Context) {
 			}
 			ghostMilestone := &issues_model.Milestone{
 				ID:   -1,
-				Name: ctx.Tr("repo.issues.deleted_milestone"),
+				Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
 			}
 			if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
 				comment.OldMilestone = ghostMilestone
@@ -1672,7 +1672,7 @@ func ViewIssue(ctx *context.Context) {
 
 			ghostProject := &project_model.Project{
 				ID:    -1,
-				Title: ctx.Tr("repo.issues.deleted_project"),
+				Title: ctx.Locale.TrString("repo.issues.deleted_project"),
 			}
 
 			if comment.OldProjectID > 0 && comment.OldProject == nil {
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 0f376db145..0939af487c 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -56,12 +56,12 @@ func GetContentHistoryList(ctx *context.Context) {
 	for _, item := range items {
 		var actionText string
 		if item.IsDeleted {
-			actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
+			actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
 			actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
 		} else if item.IsFirstCreated {
-			actionText = ctx.Locale.Tr("repo.issues.content_history.created")
+			actionText = ctx.Locale.TrString("repo.issues.content_history.created")
 		} else {
-			actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
+			actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
 		}
 
 		username := item.UserName
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index e0d49e44e1..742f12114d 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -123,7 +123,7 @@ func TestDeleteLabel(t *testing.T) {
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
 	unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
 	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
-	assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
+	assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
 }
 
 func TestUpdateIssueLabel_Clear(t *testing.T) {
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
index c04435cf1b..00bd45aaec 100644
--- a/routers/web/repo/patch.go
+++ b/routers/web/repo/patch.go
@@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) {
 	// `message` will be both the summary and message combined
 	message := strings.TrimSpace(form.CommitSummary)
 	if len(message) == 0 {
-		message = ctx.Tr("repo.editor.patch")
+		message = ctx.Locale.TrString("repo.editor.patch")
 	}
 
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 001f0752c3..cc0127e7e1 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -315,7 +315,7 @@ func ViewProject(ctx *context.Context) {
 	}
 
 	if boards[0].ID == 0 {
-		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
+		boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
 	}
 
 	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@@ -633,7 +633,7 @@ func MoveIssues(ctx *context.Context) {
 		board = &project_model.Board{
 			ID:        0,
 			ProjectID: project.ID,
-			Title:     ctx.Tr("repo.projects.type.uncategorized"),
+			Title:     ctx.Locale.TrString("repo.projects.type.uncategorized"),
 		}
 	} else {
 		board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index b265cf4754..365d9bf258 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -723,7 +723,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 type pullCommitList struct {
 	Commits             []pull_service.CommitInfo `json:"commits"`
 	LastReviewCommitSha string                    `json:"last_review_commit_sha"`
-	Locale              map[string]string         `json:"locale"`
+	Locale              map[string]any            `json:"locale"`
 }
 
 // GetPullCommits get all commits for given pull request
@@ -741,7 +741,7 @@ func GetPullCommits(ctx *context.Context) {
 	}
 
 	// Get the needed locale
-	resp.Locale = map[string]string{
+	resp.Locale = map[string]any{
 		"lang":                                ctx.Locale.Language(),
 		"show_all_commits":                    ctx.Tr("repo.pulls.show_all_commits"),
 		"stats_num_commits":                   ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)),
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index b93460d169..217f2dea6d 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -219,9 +219,9 @@ func SubmitReview(ctx *context.Context) {
 		if issue.IsPoster(ctx.Doer.ID) {
 			var translated string
 			if reviewType == issues_model.ReviewTypeApprove {
-				translated = ctx.Tr("repo.issues.review.self.approval")
+				translated = ctx.Locale.TrString("repo.issues.review.self.approval")
 			} else {
-				translated = ctx.Tr("repo.issues.review.self.rejection")
+				translated = ctx.Locale.TrString("repo.issues.review.self.rejection")
 			}
 
 			ctx.Flash.Error(translated)
diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go
index 02c807b775..44468d2666 100644
--- a/routers/web/repo/setting/avatar.go
+++ b/routers/web/repo/setting/avatar.go
@@ -38,7 +38,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
 	defer r.Close()
 
 	if form.Avatar.Size > setting.Avatar.MaxFileSize {
-		return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
+		return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
 	}
 
 	data, err := io.ReadAll(r)
@@ -47,7 +47,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
 	}
 	st := typesniffer.DetectContentType(data)
 	if !(st.IsImage() && !st.IsSvgImage()) {
-		return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+		return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
 	}
 	if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil {
 		return fmt.Errorf("UploadAvatar: %w", err)
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index 98d6977b81..85068f0ab2 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -68,7 +68,7 @@ func SettingsProtectedBranch(c *context.Context) {
 	}
 
 	c.Data["PageIsSettingsBranches"] = true
-	c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName
+	c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
 
 	users, err := access_model.GetRepoReaders(c, c.Repo.Repository)
 	if err != nil {
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 75051d1995..15f22237a8 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -739,7 +739,7 @@ func checkHomeCodeViewable(ctx *context.Context) {
 		}
 	}
 
-	ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
+	ctx.NotFound("Home", fmt.Errorf(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
 }
 
 func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 5e7b971e67..49e95faaba 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -714,7 +714,7 @@ func NewWikiPost(ctx *context.Context) {
 	wikiName := wiki_service.UserTitleToWebPath("", form.Title)
 
 	if len(form.Message) == 0 {
-		form.Message = ctx.Tr("repo.editor.add", form.Title)
+		form.Message = ctx.Locale.TrString("repo.editor.add", form.Title)
 	}
 
 	if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
@@ -766,7 +766,7 @@ func EditWikiPost(ctx *context.Context) {
 	newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
 
 	if len(form.Message) == 0 {
-		form.Message = ctx.Tr("repo.editor.update", form.Title)
+		form.Message = ctx.Locale.TrString("repo.editor.update", form.Title)
 	}
 
 	if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 83fc4d7162..b7abbcbc00 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -85,7 +85,7 @@ func Dashboard(ctx *context.Context) {
 		page = 1
 	}
 
-	ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
+	ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Locale.TrString("dashboard")
 	ctx.Data["PageIsDashboard"] = true
 	ctx.Data["PageIsNews"] = true
 	cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 95b350528c..24a807d518 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -126,7 +126,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
 		defer fr.Close()
 
 		if form.Avatar.Size > setting.Avatar.MaxFileSize {
-			return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
+			return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
 		}
 
 		data, err := io.ReadAll(fr)
@@ -136,7 +136,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
 
 		st := typesniffer.DetectContentType(data)
 		if !(st.IsImage() && !st.IsSvgImage()) {
-			return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+			return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
 		}
 		if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil {
 			return fmt.Errorf("UploadAvatar: %w", err)
@@ -389,7 +389,7 @@ func UpdateUserLang(ctx *context.Context) {
 	middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0)
 
 	log.Trace("User settings updated: %s", ctx.Doer.Name)
-	ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_language_success"))
+	ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).TrString("settings.update_language_success"))
 	ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
 }
 
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index f35f40e6a0..bec68c5f20 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -39,7 +39,7 @@ func TaskStatus(ctx *context.Context) {
 				Args:   []any{task.Message},
 			}
 		}
-		message = ctx.Tr(translatableMessage.Format, translatableMessage.Args...)
+		message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
 	}
 
 	ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/web.go b/routers/web/web.go
index 92cf5132b4..ba5c86cc7e 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -155,7 +155,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
 			if ctx.Doer.MustChangePassword {
 				if ctx.Req.URL.Path != "/user/settings/change_password" {
 					if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
-						ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password"))
+						ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
 						return
 					}
 					ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 0656307cba..6dad88830a 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -70,7 +70,7 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
 // Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
 func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string {
 	realArgs := make([]any, 0, len(args)+2)
-	realArgs = append(realArgs, locale.Tr("admin.dashboard."+name))
+	realArgs = append(realArgs, locale.TrString("admin.dashboard."+name))
 	if doer == "" {
 		realArgs = append(realArgs, "(Cron)")
 	} else {
@@ -80,7 +80,7 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer
 		realArgs = append(realArgs, args...)
 	}
 	if doer == "" {
-		return locale.Tr("admin.dashboard.cron."+status, realArgs...)
+		return locale.TrString("admin.dashboard.cron."+status, realArgs...)
 	}
-	return locale.Tr("admin.dashboard.task."+status, realArgs...)
+	return locale.TrString("admin.dashboard.task."+status, realArgs...)
 }
diff --git a/services/cron/tasks.go b/services/cron/tasks.go
index f0956a97d8..f8a7444c49 100644
--- a/services/cron/tasks.go
+++ b/services/cron/tasks.go
@@ -159,7 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
 	log.Debug("Registering task: %s", name)
 
 	i18nKey := "admin.dashboard." + name
-	if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey {
+	if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey {
 		return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
 	}
 
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 60fa0ab363..98d556b946 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -314,7 +314,7 @@ func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) bind
 		errs = append(errs, binding.Error{
 			FieldNames:     []string{"Channel"},
 			Classification: "",
-			Message:        ctx.Tr("repo.settings.add_webhook.invalid_channel_name"),
+			Message:        ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"),
 		})
 	}
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index ca27336f92..38973ea935 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -94,7 +94,7 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
 		// No mail service configured
 		return
 	}
-	sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.activate_account"), "activate account")
+	sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
 }
 
 // SendResetPasswordMail sends a password reset mail to the user
@@ -104,7 +104,7 @@ func SendResetPasswordMail(u *user_model.User) {
 		return
 	}
 	locale := translation.NewLocale(u.Language)
-	sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.reset_password"), "recover account")
+	sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
 }
 
 // SendActivateEmailMail sends confirmation email to confirm new email address
@@ -130,7 +130,7 @@ func SendActivateEmailMail(u *user_model.User, email string) {
 		return
 	}
 
-	msg := NewMessage(email, locale.Tr("mail.activate_email"), content.String())
+	msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String())
 	msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
 
 	SendAsync(msg)
@@ -158,7 +158,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
 		return
 	}
 
-	msg := NewMessage(u.Email, locale.Tr("mail.register_notify"), content.String())
+	msg := NewMessage(u.Email, locale.TrString("mail.register_notify"), content.String())
 	msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
 
 	SendAsync(msg)
@@ -173,7 +173,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
 	locale := translation.NewLocale(u.Language)
 	repoName := repo.FullName()
 
-	subject := locale.Tr("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
+	subject := locale.TrString("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
 	data := map[string]any{
 		"locale":   locale,
 		"Subject":  subject,
diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go
index 5e8e5b6af3..6682774a04 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -68,7 +68,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
 		return
 	}
 
-	subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
+	subject := locale.TrString("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
 	mailMeta := map[string]any{
 		"locale":   locale,
 		"Release":  rel,
diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go
index b89dcd43b5..e0d55bb120 100644
--- a/services/mailer/mail_repo.go
+++ b/services/mailer/mail_repo.go
@@ -56,11 +56,11 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
 		content bytes.Buffer
 	)
 
-	destination := locale.Tr("mail.repo.transfer.to_you")
-	subject := locale.Tr("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName())
+	destination := locale.TrString("mail.repo.transfer.to_you")
+	subject := locale.TrString("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName())
 	if newOwner.IsOrganization() {
 		destination = newOwner.DisplayName()
-		subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
+		subject = locale.TrString("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
 	}
 
 	data := map[string]any{
diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go
index ab32beefac..ceecefa50f 100644
--- a/services/mailer/mail_team_invite.go
+++ b/services/mailer/mail_team_invite.go
@@ -50,7 +50,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
 		inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect)
 	}
 
-	subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
+	subject := locale.TrString("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
 	mailMeta := map[string]any{
 		"locale":       locale,
 		"Inviter":      inviter,
diff --git a/templates/mail/issue/assigned.tmpl b/templates/mail/issue/assigned.tmpl
index d02ea39918..e80bd2fc31 100644
--- a/templates/mail/issue/assigned.tmpl
+++ b/templates/mail/issue/assigned.tmpl
@@ -13,9 +13,9 @@
 <body>
 	<p>
 		{{if .IsPull}}
-			{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url | Str2html}}
+			{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name ($link|Safe) ($repo_url|Safe)}}
 		{{else}}
-			{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url | Str2html}}
+			{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name ($link|Safe) ($repo_url|Safe)}}
 		{{end}}
 	</p>
 	<div class="footer">
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index 422a4f0461..b5a7ab95cf 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -28,7 +28,7 @@
 				{{$newShortSha := ShortSha .Comment.NewCommit}}
 				{{$newCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $newCommitUrl) (Escape $newShortSha)}}
 
-				{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink | Str2html}}
+				{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch ($oldCommitLink|Safe) ($newCommitLink|Safe)}}
 			{{else}}
 				{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}}
 			{{end}}
diff --git a/templates/mail/notify/repo_transfer.tmpl b/templates/mail/notify/repo_transfer.tmpl
index 43d95b3ff0..1b23593f6b 100644
--- a/templates/mail/notify/repo_transfer.tmpl
+++ b/templates/mail/notify/repo_transfer.tmpl
@@ -8,7 +8,7 @@
 {{$url := printf "<a href='%[1]s'>%[2]s</a>" (Escape .Link) (Escape .Repo)}}
 <body>
 	<p>{{.Subject}}.
-		{{.locale.Tr "mail.repo.transfer.body" $url | Str2html}}
+		{{.locale.Tr "mail.repo.transfer.body" ($url|Safe)}}
 	</p>
 	<p>
 		---
diff --git a/templates/mail/release.tmpl b/templates/mail/release.tmpl
index f588d8224f..96dc769993 100644
--- a/templates/mail/release.tmpl
+++ b/templates/mail/release.tmpl
@@ -15,7 +15,7 @@
 {{$repo_url := printf "<a href='%s'>%s</a>" (.Release.Repo.HTMLURL | Escape) (.Release.Repo.FullName | Escape)}}
 <body>
 	<p>
-		{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url | Str2html}}
+		{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name ($release_url|Safe) ($repo_url|Safe)}}
 	</p>
 	<h4>{{.locale.Tr "mail.release.title" .Release.Title}}</h4>
 	<p>
diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl
index ab2c3c3349..b65c3a3033 100644
--- a/templates/repo/editor/cherry_pick.tmpl
+++ b/templates/repo/editor/cherry_pick.tmpl
@@ -13,9 +13,9 @@
 					{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
 					{{$shalink := printf `<a class="ui primary sha label" href="%s">%s</a>` (Escape $shaurl) (ShortSha .SHA)}}
 					{{if eq .CherryPickType "revert"}}
-						{{ctx.Locale.Tr "repo.editor.revert" $shalink | Str2html}}
+						{{ctx.Locale.Tr "repo.editor.revert" ($shalink|Safe)}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.editor.cherry_pick" $shalink | Str2html}}
+						{{ctx.Locale.Tr "repo.editor.cherry_pick" ($shalink|Safe)}}
 					{{end}}
 					<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
 					<div class="breadcrumb-divider">:</div>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 3cb7f7d0cf..c1797ba77d 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -591,11 +591,11 @@
 						{{$newProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}}
 					{{end}}
 					{{if and (gt .OldProjectID 0) (gt .ProjectID 0)}}
-						{{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.change_project_at" ($oldProjectDisplayHtml|Safe) ($newProjectDisplayHtml|Safe) $createdStr}}
 					{{else if gt .OldProjectID 0}}
-						{{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.remove_project_at" ($oldProjectDisplayHtml|Safe) $createdStr}}
 					{{else if gt .ProjectID 0}}
-						{{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.add_project_at" ($newProjectDisplayHtml|Safe) $createdStr}}
 					{{end}}
 				</span>
 			</div>
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 7ec48c6734..582e9864fb 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -56,18 +56,18 @@
 					{{$mergedStr:= TimeSinceUnix .Issue.PullRequest.MergedUnix ctx.Locale}}
 					{{if .Issue.OriginalAuthor}}
 						{{.Issue.OriginalAuthor}}
-						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span>
+						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe) $mergedStr}}</span>
 					{{else}}
 						<a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a>
-						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span>
+						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe) $mergedStr}}</span>
 					{{end}}
 				{{else}}
 					{{if .Issue.OriginalAuthor}}
-						<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}}</span>
+						<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe)}}</span>
 					{{else}}
 						<span id="pull-desc" class="pull-desc">
 							<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
-							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}}
+							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe)}}
 						</span>
 					{{end}}
 					<span id="pull-desc-edit" class="gt-hidden flex-text-block">
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 1148b3ad39..2d69dfcfd7 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -309,7 +309,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
 	// all groups the user is a member of, the user filter is modified accordingly inside
 	// the addAuthSourceLDAP based on the value of the groupFilter
 	u := otherLDAPUsers[0]
-	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
+	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
 
 	auth.SyncExternalUsers(context.Background(), true)
 
@@ -362,7 +362,7 @@ func TestLDAPUserSigninFailed(t *testing.T) {
 	addAuthSourceLDAP(t, "", "")
 
 	u := otherLDAPUsers[0]
-	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
+	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
 }
 
 func TestLDAPUserSSHKeySync(t *testing.T) {
diff --git a/tests/integration/branches_test.go b/tests/integration/branches_test.go
index 99d7eef706..e148fe2d6f 100644
--- a/tests/integration/branches_test.go
+++ b/tests/integration/branches_test.go
@@ -37,7 +37,7 @@ func TestUndoDeleteBranch(t *testing.T) {
 		htmlDoc, name := branchAction(t, ".restore-branch-button")
 		assert.Contains(t,
 			htmlDoc.doc.Find(".ui.positive.message").Text(),
-			translation.NewLocale("en-US").Tr("repo.branch.restore_success", name),
+			translation.NewLocale("en-US").TrString("repo.branch.restore_success", name),
 		)
 	})
 }
@@ -46,7 +46,7 @@ func deleteBranch(t *testing.T) {
 	htmlDoc, name := branchAction(t, ".delete-branch-button")
 	assert.Contains(t,
 		htmlDoc.doc.Find(".ui.positive.message").Text(),
-		translation.NewLocale("en-US").Tr("repo.branch.deletion_success", name),
+		translation.NewLocale("en-US").TrString("repo.branch.deletion_success", name),
 	)
 }
 
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index 5205df2f8e..a04b4c98cd 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -219,7 +219,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
 		text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
 		assert.NotEmpty(t, text, "Can't find WIP text")
 
-		assert.Contains(t, text, translation.NewLocale("en-US").Tr("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
+		assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
 		assert.Contains(t, text, "[wip]", "Unable to find WIP text")
 	})
 }
diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go
index 42d0d00e78..04de0c123f 100644
--- a/tests/integration/release_test.go
+++ b/tests/integration/release_test.go
@@ -86,7 +86,7 @@ func TestCreateRelease(t *testing.T) {
 	session := loginUser(t, "user2")
 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.stable"), 4)
 }
 
 func TestCreateReleasePreRelease(t *testing.T) {
@@ -95,7 +95,7 @@ func TestCreateReleasePreRelease(t *testing.T) {
 	session := loginUser(t, "user2")
 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.prerelease"), 4)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.prerelease"), 4)
 }
 
 func TestCreateReleaseDraft(t *testing.T) {
@@ -104,7 +104,7 @@ func TestCreateReleaseDraft(t *testing.T) {
 	session := loginUser(t, "user2")
 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.draft"), 4)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.draft"), 4)
 }
 
 func TestCreateReleasePaging(t *testing.T) {
@@ -124,11 +124,11 @@ func TestCreateReleasePaging(t *testing.T) {
 	}
 	createNewRelease(t, session, "/user2/repo1", "v0.0.12", "v0.0.12", false, true)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").Tr("repo.release.draft"), 10)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").TrString("repo.release.draft"), 10)
 
 	// Check that user4 does not see draft and still see 10 latest releases
 	session2 := loginUser(t, "user4")
-	checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").Tr("repo.release.stable"), 10)
+	checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").TrString("repo.release.stable"), 10)
 }
 
 func TestViewReleaseListNoLogin(t *testing.T) {
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index 91674ddc82..baa8da4b75 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -52,37 +52,37 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "feature/test1",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test1"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.require_error"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.require_error"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "feature=test1",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature=test1"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      strings.Repeat("b", 101),
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.max_size_error", "100"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.max_size_error", "100"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "master",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.branch_already_exists", "master"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.branch_already_exists", "master"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "master/test",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.branch_name_conflict", "master/test", "master"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.branch_name_conflict", "master/test", "master"),
 		},
 		{
 			OldRefSubURL:   "commit/acd1d892867872cb47f3993468605b8aa59aa2e0",
@@ -93,21 +93,21 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
 			OldRefSubURL:   "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
 			NewBranch:      "feature/test3",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test3"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "v1.0.0",
 			CreateRelease:  "v1.0.0",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.tag_collision", "v1.0.0"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.tag_collision", "v1.0.0"),
 		},
 		{
 			OldRefSubURL:   "tag/v1.0.0",
 			NewBranch:      "feature/test4",
 			CreateRelease:  "v1.0.1",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test4"),
 		},
 	}
 	for _, test := range tests {
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
index 2584b88f65..77e19bba96 100644
--- a/tests/integration/signin_test.go
+++ b/tests/integration/signin_test.go
@@ -49,10 +49,10 @@ func TestSignin(t *testing.T) {
 		password string
 		message  string
 	}{
-		{username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
-		{username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
-		{username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
-		{username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
+		{username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
+		{username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
+		{username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
+		{username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
 	}
 
 	for _, s := range samples {
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go
index f983f98ad8..859f873f85 100644
--- a/tests/integration/signup_test.go
+++ b/tests/integration/signup_test.go
@@ -68,9 +68,9 @@ func TestSignupEmail(t *testing.T) {
 		wantStatus int
 		wantMsg    string
 	}{
-		{"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
-		{"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
-		{"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
+		{"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
+		{"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
+		{"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
 		{"exampleUser@example.com", http.StatusSeeOther, ""},
 	}
 
diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go
index d8e4c64e85..c30733b1b0 100644
--- a/tests/integration/user_test.go
+++ b/tests/integration/user_test.go
@@ -85,7 +85,7 @@ func TestRenameInvalidUsername(t *testing.T) {
 		htmlDoc := NewHTMLParser(t, resp.Body)
 		assert.Contains(t,
 			htmlDoc.doc.Find(".ui.negative.message").Text(),
-			translation.NewLocale("en-US").Tr("form.username_error"),
+			translation.NewLocale("en-US").TrString("form.username_error"),
 		)
 
 		unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
@@ -147,7 +147,7 @@ func TestRenameReservedUsername(t *testing.T) {
 		htmlDoc := NewHTMLParser(t, resp.Body)
 		assert.Contains(t,
 			htmlDoc.doc.Find(".ui.negative.message").Text(),
-			translation.NewLocale("en-US").Tr("user.form.name_reserved", reservedUsername),
+			translation.NewLocale("en-US").TrString("user.form.name_reserved", reservedUsername),
 		)
 
 		unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername})

From 1c14cd0c43d670fef984068e2666641ea5a062db Mon Sep 17 00:00:00 2001
From: Rafael Heard <rafael.heard@gmail.com>
Date: Thu, 15 Feb 2024 03:47:49 -0500
Subject: [PATCH 038/679] move sign in labels to be above inputs (#28753)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

There are a few inconsistencies within Gitea and this PR addresses one of them.
This PR updates the sign-in page layout, including the register and openID tabs,
to match the layout of the settings pages (`/user/settings`) for more consistency.

**Before**
<img width="968" alt="Screenshot 2024-02-05 at 8 27 24 AM"
src="https://github.com/go-gitea/gitea/assets/6152817/fb0cb517-57c0-4eed-be1d-56f36bd1960d">


**After**
<img width="968" alt="Screenshot 2024-02-05 at 8 26 39 AM"
src="https://github.com/go-gitea/gitea/assets/6152817/428d691d-0a42-4a67-a646-05527f2a7b41">

---------

Co-authored-by: rafh <rafaelheard@gmail.com>
---
 templates/user/auth/signin_inner.tmpl  | 11 ++++-------
 templates/user/auth/signin_openid.tmpl |  6 ++----
 templates/user/auth/signup_inner.tmpl  | 14 ++++++--------
 web_src/css/form.css                   |  2 --
 web_src/css/helpers.css                |  1 +
 5 files changed, 13 insertions(+), 21 deletions(-)

diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 40e54ec8fa..a0aea5cb9b 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -9,21 +9,20 @@
 	{{end}}
 </h4>
 <div class="ui attached segment">
-	<form class="ui form" action="{{.SignInLink}}" method="post">
+	<form class="ui form gt-max-width-36rem gt-m-auto" action="{{.SignInLink}}" method="post">
 	{{.CsrfTokenHtml}}
 	<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
-		<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
+		<input id="user_name" class="gt-w-full" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 	</div>
 	{{if or (not .DisablePassword) .LinkAccountMode}}
 	<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="password">{{ctx.Locale.Tr "password"}}</label>
-		<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required>
+		<input id="password" class="gt-w-full" name="password" type="password" value="{{.password}}" autocomplete="current-password" required>
 	</div>
 	{{end}}
 	{{if not .LinkAccountMode}}
 	<div class="inline field">
-		<label></label>
 		<div class="ui checkbox">
 			<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 			<input name="remember" type="checkbox">
@@ -34,7 +33,6 @@
 	{{template "user/auth/captcha" .}}
 
 	<div class="inline field">
-		<label></label>
 		<button class="ui primary button">
 			{{if .LinkAccountMode}}
 				{{ctx.Locale.Tr "auth.oauth_signin_submit"}}
@@ -47,7 +45,6 @@
 
 	{{if .ShowRegistrationButton}}
 		<div class="inline field">
-			<label></label>
 			<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a>
 		</div>
 	{{end}}
@@ -60,7 +57,7 @@
 		<div class="gt-df gt-fc gt-jc">
 			<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
 				{{range $provider := .OAuth2Providers}}
-					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 gt-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
 						{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 					</a>
diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl
index 0428026aa8..a138ea0b8d 100644
--- a/templates/user/auth/signin_openid.tmpl
+++ b/templates/user/auth/signin_openid.tmpl
@@ -8,7 +8,7 @@
 			OpenID
 		</h4>
 		<div class="ui attached segment">
-			<form class="ui form" action="{{.Link}}" method="post">
+			<form class="ui form gt-m-auto" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="inline field">
 				{{ctx.Locale.Tr "auth.openid_signin_desc"}}
@@ -18,17 +18,15 @@
 				{{svg "fontawesome-openid"}}
 				OpenID URI
 				</label>
-				<input id="openid" name="openid" value="{{.openid}}" autofocus required>
+				<input id="openid" class="gt-w-full" name="openid" value="{{.openid}}" autofocus required>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<div class="ui checkbox">
 					<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 					<input name="remember" type="checkbox">
 				</div>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<button class="ui primary button">{{ctx.Locale.Tr "sign_in"}}</button>
 			</div>
 			</form>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index e930bd3d15..65ce98c31a 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -7,7 +7,7 @@
 		{{end}}
 	</h4>
 	<div class="ui attached segment">
-		<form class="ui form" action="{{.SignUpLink}}" method="post">
+		<form class="ui form gt-max-width-36rem gt-m-auto" action="{{.SignUpLink}}" method="post">
 			{{.CsrfTokenHtml}}
 			{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
 			{{template "base/alert" .}}
@@ -17,28 +17,27 @@
 			{{else}}
 				<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 					<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
-					<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
+					<input id="user_name" class="gt-w-full" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 				</div>
 				<div class="required inline field {{if .Err_Email}}error{{end}}">
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
-					<input id="email" name="email" type="email" value="{{.email}}" required>
+					<input id="email" class="gt-w-full" name="email" type="email" value="{{.email}}" required>
 				</div>
 
 				{{if not .DisablePassword}}
 					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="password">{{ctx.Locale.Tr "password"}}</label>
-						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
+						<input id="password" class="gt-w-full" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
 					</div>
 					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="retype">{{ctx.Locale.Tr "re_type"}}</label>
-						<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required>
+						<input id="retype" class="gt-w-full" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required>
 					</div>
 				{{end}}
 
 				{{template "user/auth/captcha" .}}
 
 				<div class="inline field">
-					<label></label>
 					<button class="ui primary button">
 						{{if .LinkAccountMode}}
 							{{ctx.Locale.Tr "auth.oauth_signup_submit"}}
@@ -50,7 +49,6 @@
 
 				{{if not .LinkAccountMode}}
 				<div class="inline field">
-					<label></label>
 					<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.register_helper_msg"}}</a>
 				</div>
 				{{end}}
@@ -64,7 +62,7 @@
 				<div class="gt-df gt-fc gt-jc">
 					<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
 						{{range $provider := .OAuth2Providers}}
-							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 gt-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
 								{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 							</a>
diff --git a/web_src/css/form.css b/web_src/css/form.css
index e4efa34948..c0de4978dd 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -243,7 +243,6 @@ textarea:focus,
 .user.forgot.password form,
 .user.reset.password form,
 .user.link-account form,
-.user.signin form,
 .user.signup form {
   margin: auto;
   width: 700px !important;
@@ -279,7 +278,6 @@ textarea:focus,
   .user.forgot.password form .inline.field > label,
   .user.reset.password form .inline.field > label,
   .user.link-account form .inline.field > label,
-  .user.signin form .inline.field > label,
   .user.signup form .inline.field > label {
     text-align: right;
     width: 250px !important;
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index da94ebb486..c7d8abb1d4 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -48,6 +48,7 @@ Gitea's private styles use `g-` prefix.
 
 .gt-max-width-12rem { max-width: 12rem !important; }
 .gt-max-width-24rem { max-width: 24rem !important; }
+.gt-max-width-36rem { max-width: 36rem !important; }
 
 /* below class names match Tailwind CSS */
 .gt-break-all { word-break: break-all !important; }

From 78c48d8fdde70a2874a7ed42b7762f797f432b03 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Thu, 15 Feb 2024 20:30:11 +0900
Subject: [PATCH 039/679] Fix can not select team reviewers when reviewers is
 empty (#29174)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Before:

![image](https://github.com/go-gitea/gitea/assets/18380374/b29e9c0c-f0fc-454f-b82d-ff9688d9e871)

After:

![image](https://github.com/go-gitea/gitea/assets/18380374/a982f7c6-4911-4951-91a5-4bb347e866f9)

Is this a bug? Maybe we don't need to fix this, as it only occurs when
there's only one user in the organization. 🤔
---
 templates/repo/issue/view_content/sidebar.tmpl | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 6c13eef023..22f67ade7b 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -2,7 +2,7 @@
 	{{template "repo/issue/branch_selector_field" .}}
 	{{if .Issue.IsPull}}
 		<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
-		<div class="ui {{if or (not .Reviewers) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
+		<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
 			<a class="text gt-df gt-ac muted">
 				<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
 				{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
@@ -29,7 +29,9 @@
 					{{end}}
 				{{end}}
 				{{if .TeamReviewers}}
-					<div class="divider"></div>
+					{{if .Reviewers}}
+						<div class="divider"></div>
+					{{end}}
 					{{range .TeamReviewers}}
 						{{if .Team}}
 							<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>

From 542480a9b0d5cdb497dbfa99752d59fd016df0d6 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Thu, 15 Feb 2024 15:27:07 +0200
Subject: [PATCH 040/679] Remove jQuery from the comment task list (#29170)

- Switched to plain JavaScript
- Tested the task list functionality and it works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/markup/tasklist.js | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js
index ad1c6964a7..00076bce58 100644
--- a/web_src/js/markup/tasklist.js
+++ b/web_src/js/markup/tasklist.js
@@ -1,4 +1,4 @@
-import $ from 'jquery';
+import {POST} from '../modules/fetch.js';
 
 const preventListener = (e) => e.preventDefault();
 
@@ -55,12 +55,11 @@ export function initMarkupTasklist() {
           const updateUrl = editContentZone.getAttribute('data-update-url');
           const context = editContentZone.getAttribute('data-context');
 
-          await $.post(updateUrl, {
-            ignore_attachments: true,
-            _csrf: window.config.csrfToken,
-            content: newContent,
-            context
-          });
+          const requestBody = new FormData();
+          requestBody.append('ignore_attachments', 'true');
+          requestBody.append('content', newContent);
+          requestBody.append('context', context);
+          await POST(updateUrl, {data: requestBody});
 
           rawContent.textContent = newContent;
         } catch (err) {

From 374e886f5113a996e1e927a60d1775e77262c364 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Nicas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Thu, 15 Feb 2024 14:59:48 +0100
Subject: [PATCH 041/679] Change webhook-type in create-view (#29114)

It's now possible to change webhook-type in create-view.

before:

![image](https://github.com/go-gitea/gitea/assets/72873130/9ee1b9fb-843b-4f28-b8d6-6361e5d184f1)

after:

![image](https://github.com/go-gitea/gitea/assets/72873130/9dbf058f-5912-43af-9acd-487271212f2d)

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 routers/web/repo/setting/webhook.go           |  1 +
 .../repo/settings/webhook/base_list.tmpl      | 47 +----------------
 .../repo/settings/webhook/link_menu.tmpl      | 50 +++++++++++++++++++
 templates/shared/webhook/icon.tmpl            |  2 +-
 templates/webhook/new.tmpl                    |  9 +++-
 web_src/css/base.css                          |  7 +++
 6 files changed, 67 insertions(+), 49 deletions(-)
 create mode 100644 templates/repo/settings/webhook/link_menu.tmpl

diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index ab3c70006f..c12d7e82a6 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -151,6 +151,7 @@ func WebhooksNew(ctx *context.Context) {
 		}
 	}
 	ctx.Data["BaseLink"] = orCtx.LinkNew
+	ctx.Data["BaseLinkNew"] = orCtx.LinkNew
 
 	ctx.HTML(http.StatusOK, orCtx.NewTemplate)
 }
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index ed6e670d60..5a3fc0e7b8 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -3,52 +3,7 @@
 	<div class="ui right">
 		<div class="ui jump dropdown">
 			<div class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</div>
-			<div class="menu">
-				<a class="item" href="{{.BaseLinkNew}}/gitea/new">
-					{{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/gogs/new">
-					{{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/slack/new">
-					{{template "shared/webhook/icon" (dict "HookType" "slack" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/discord/new">
-					{{template "shared/webhook/icon" (dict "HookType" "discord" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/dingtalk/new">
-					{{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/telegram/new">
-					{{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/msteams/new">
-					{{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/feishu/new">
-					{{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/matrix/new">
-					{{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/wechatwork/new">
-					{{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/packagist/new">
-					{{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}}
-				</a>
-			</div>
+			{{template "repo/settings/webhook/link_menu" .}}
 		</div>
 	</div>
 </h4>
diff --git a/templates/repo/settings/webhook/link_menu.tmpl b/templates/repo/settings/webhook/link_menu.tmpl
new file mode 100644
index 0000000000..e2c86dcc3c
--- /dev/null
+++ b/templates/repo/settings/webhook/link_menu.tmpl
@@ -0,0 +1,50 @@
+{{$size := 20}}
+{{if .Size}}
+	{{$size = .Size}}
+{{end}}
+<div class="menu">
+	<a class="item" href="{{.BaseLinkNew}}/gitea/new">
+		{{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/gogs/new">
+		{{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/slack/new">
+		{{template "shared/webhook/icon" (dict "HookType" "slack" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/discord/new">
+		{{template "shared/webhook/icon" (dict "HookType" "discord" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/dingtalk/new">
+		{{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/telegram/new">
+		{{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/msteams/new">
+		{{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/feishu/new">
+		{{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/matrix/new">
+		{{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/wechatwork/new">
+		{{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/packagist/new">
+		{{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}}
+	</a>
+</div>
diff --git a/templates/shared/webhook/icon.tmpl b/templates/shared/webhook/icon.tmpl
index 84f9de266f..0f80787c57 100644
--- a/templates/shared/webhook/icon.tmpl
+++ b/templates/shared/webhook/icon.tmpl
@@ -3,7 +3,7 @@
 	{{$size = .Size}}
 {{end}}
 {{if eq .HookType "gitea"}}
-	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gitea.svg">
+	{{svg "gitea-gitea" $size "img"}}
 {{else if eq .HookType "gogs"}}
 	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gogs.ico">
 {{else if eq .HookType "slack"}}
diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl
index a185c42b51..305133c03a 100644
--- a/templates/webhook/new.tmpl
+++ b/templates/webhook/new.tmpl
@@ -1,7 +1,12 @@
 <h4 class="ui top attached header">
 	{{.CustomHeaderTitle}}
-	<div class="ui right">
-		{{template "shared/webhook/icon" .ctxData}}
+	<div class="ui right type dropdown">
+		<div class="text gt-df gt-ac">
+			{{template "shared/webhook/icon" (dict "Size" 20 "HookType" .ctxData.HookType)}}
+			{{ctx.Locale.Tr (print "repo.settings.web_hook_name_" .ctxData.HookType)}}
+		</div>
+		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+		{{template "repo/settings/webhook/link_menu" .ctxData}}
 	</div>
 </h4>
 <div class="ui attached segment">
diff --git a/web_src/css/base.css b/web_src/css/base.css
index ea32aac6f7..0d547f16ff 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -413,6 +413,13 @@ ol.ui.list li,
   color: var(--color-text-light-2);
 }
 
+/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */
+.ui.dropdown > .text > .img {
+  margin-left: 0;
+  float: none;
+  margin-right: 0.78571429rem;
+}
+
 .ui.dropdown > .text > .description,
 .ui.dropdown .menu > .item > .description {
   color: var(--color-text-light-2);

From 363b5f0b595df4c703d80878d2f2a1bafd647291 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 15 Feb 2024 17:52:21 +0100
Subject: [PATCH 042/679] Tweak repo header (#29134)

- Tweak colors, remove link color from repo name and make text use
inherited color
- Downsize repo icon from 32px to 24px

Before:
<img width="255" alt="Screenshot 2024-02-11 at 15 31 00"
src="https://github.com/go-gitea/gitea/assets/115237/f65c1d02-d8a3-4171-ad3d-4c95871fb2ba">

After:
<img width="260" alt="Screenshot 2024-02-11 at 15 30 48"
src="https://github.com/go-gitea/gitea/assets/115237/a9b25b56-8d3f-4910-af60-2513d44f6d81">

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/header.tmpl | 6 +++---
 templates/repo/icon.tmpl   | 8 ++++----
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 93102467cc..3e27d963bb 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -5,9 +5,9 @@
 			<div class="flex-item gt-ac">
 				<div class="flex-item-leading">{{template "repo/icon" .}}</div>
 				<div class="flex-item-main">
-					<div class="flex-item-title">
-						<a class="text light thin" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/
-						<a class="text primary" href="{{$.RepoLink}}">{{.Name}}</a></div>
+					<div class="flex-item-title gt-font-18">
+						<a class="muted gt-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/
+						<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a></div>
 				</div>
 				<div class="flex-item-trailing">
 					{{if .IsArchived}}
diff --git a/templates/repo/icon.tmpl b/templates/repo/icon.tmpl
index 5a80b959d0..a001f81891 100644
--- a/templates/repo/icon.tmpl
+++ b/templates/repo/icon.tmpl
@@ -1,10 +1,10 @@
 {{$avatarLink := (.RelAvatarLink ctx)}}
 {{if $avatarLink}}
-	<img class="ui avatar gt-vm" src="{{$avatarLink}}" width="32" height="32" alt="{{.FullName}}">
+	<img class="ui avatar gt-vm" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}">
 {{else if $.IsMirror}}
-	{{svg "octicon-mirror" 32}}
+	{{svg "octicon-mirror" 24}}
 {{else if $.IsFork}}
-	{{svg "octicon-repo-forked" 32}}
+	{{svg "octicon-repo-forked" 24}}
 {{else}}
-	{{svg "octicon-repo" 32}}
+	{{svg "octicon-repo" 24}}
 {{end}}

From 702a876453a8906103e95a62f6cfa25fb08ea8e4 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Thu, 15 Feb 2024 18:49:13 +0100
Subject: [PATCH 043/679] Advertise WebAuthn support (#29176)

This well-known indicates for password manager, that passkeys are supported.

source:
https://android-developers.googleblog.com/2023/10/make-passkey-endpoints-well-known-url-part-of-your-passkey-implementation.html

spec:
https://github.com/ms-id-standards/MSIdentityStandardsExplainers/blob/main/PasskeyEndpointsWellKnownUrl/explainer.md
---
 routers/web/passkey.go | 24 ++++++++++++++++++++++++
 routers/web/web.go     |  1 +
 2 files changed, 25 insertions(+)
 create mode 100644 routers/web/passkey.go

diff --git a/routers/web/passkey.go b/routers/web/passkey.go
new file mode 100644
index 0000000000..95874dfc48
--- /dev/null
+++ b/routers/web/passkey.go
@@ -0,0 +1,24 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package web
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+type passkeyEndpointsType struct {
+	Enroll string `json:"enroll"`
+	Manage string `json:"manage"`
+}
+
+func passkeyEndpoints(ctx *context.Context) {
+	url := setting.AppURL + "user/settings/security"
+	ctx.JSON(http.StatusOK, passkeyEndpointsType{
+		Enroll: url,
+		Manage: url,
+	})
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index ba5c86cc7e..7aa9bb0795 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -473,6 +473,7 @@ func registerRoutes(m *web.Route) {
 		m.Get("/change-password", func(ctx *context.Context) {
 			ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 		})
+		m.Get("/passkey-endpoints", passkeyEndpoints)
 		m.Methods("GET, HEAD", "/*", public.FileHandlerFunc())
 	}, optionsCorsHandler())
 

From 07597c71a4b6642beae7589c678603f4846f7920 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Thu, 15 Feb 2024 21:39:50 +0100
Subject: [PATCH 044/679] Add support for action artifact serve direct (#29120)

Fixes #29093
---
 routers/api/actions/artifacts.go | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
index 3363c4c0e8..9fbd3f045d 100644
--- a/routers/api/actions/artifacts.go
+++ b/routers/api/actions/artifacts.go
@@ -63,6 +63,7 @@ package actions
 
 import (
 	"crypto/md5"
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -426,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
 
 	var items []downloadArtifactResponseItem
 	for _, artifact := range artifacts {
-		downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
+		var downloadURL string
+		if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+			u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
+			if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
+				log.Error("Error getting serve direct url: %v", err)
+			}
+			if u != nil {
+				downloadURL = u.String()
+			}
+		}
+		if downloadURL == "" {
+			downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
+		}
 		item := downloadArtifactResponseItem{
 			Path:            util.PathJoinRel(itemPath, artifact.ArtifactPath),
 			ItemType:        "file",

From 21331be30cb8f6c2d8b9dd99f1061623900632b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= <sahin@sahinakkaya.dev>
Date: Fri, 16 Feb 2024 01:21:13 +0300
Subject: [PATCH 045/679] Implement contributors graph (#27882)

Continuation of https://github.com/go-gitea/gitea/pull/25439. Fixes #847

Before:
<img width="1296" alt="image"
src="https://github.com/go-gitea/gitea/assets/32161460/24571ac8-b254-43c9-b178-97340f0dc8a9">

----
After:
<img width="1296" alt="image"
src="https://github.com/go-gitea/gitea/assets/32161460/c60b2459-9d10-4d42-8d83-d5ef0f45bf94">

---
#### Overview
This is the implementation of a requested feature: Contributors graph
(#847)

It makes Activity page a multi-tab page and adds a new tab called
Contributors. Contributors tab shows the contribution graphs over time
since the repository existed. It also shows per user contribution graphs
for top 100 contributors. Top 100 is calculated based on the selected
contribution type (commits, additions or deletions).

---
#### Demo
(The demo is a bit old but still a good example to show off the main
features)

<video src="https://github.com/go-gitea/gitea/assets/32161460/9f68103f-8145-4cc2-94bc-5546daae7014" controls width="320" height="240">
  <a href="https://github.com/go-gitea/gitea/assets/32161460/9f68103f-8145-4cc2-94bc-5546daae7014">Download</a>
</video>


#### Features:

- Select contribution type (commits, additions or deletions)
- See overall and per user contribution graphs for the selected
contribution type
- Zoom and pan on graphs to see them in detail
- See top 100 contributors based on the selected contribution type and
selected time range
- Go directly to users' profile by clicking their name if they are
registered gitea users
- Cache the results so that when the same repository is visited again
fetching data will be faster


---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: hiifong <i@hiif.ong>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: yp05327 <576951401@qq.com>
---
 options/locale/locale_en-US.ini               |  12 +
 package-lock.json                             |  67 ++-
 package.json                                  |   5 +
 routers/web/repo/activity.go                  |   2 +
 routers/web/repo/contributors.go              |  44 ++
 routers/web/web.go                            |   4 +
 services/repository/contributors_graph.go     | 319 +++++++++++++
 .../repository/contributors_graph_test.go     |  87 ++++
 templates/repo/activity.tmpl                  | 234 +--------
 templates/repo/contributors.tmpl              |  13 +
 templates/repo/navbar.tmpl                    |   8 +
 templates/repo/pulse.tmpl                     | 227 +++++++++
 web_src/js/components/.eslintrc.yaml          |   4 +
 web_src/js/components/RepoContributors.vue    | 443 ++++++++++++++++++
 web_src/js/features/contributors.js           |  28 ++
 web_src/js/index.js                           |   2 +
 web_src/js/utils/time.js                      |  46 ++
 web_src/js/utils/time.test.js                 |  15 +
 18 files changed, 1330 insertions(+), 230 deletions(-)
 create mode 100644 routers/web/repo/contributors.go
 create mode 100644 services/repository/contributors_graph.go
 create mode 100644 services/repository/contributors_graph_test.go
 create mode 100644 templates/repo/contributors.tmpl
 create mode 100644 templates/repo/navbar.tmpl
 create mode 100644 templates/repo/pulse.tmpl
 create mode 100644 web_src/js/components/RepoContributors.vue
 create mode 100644 web_src/js/features/contributors.js
 create mode 100644 web_src/js/utils/time.js
 create mode 100644 web_src/js/utils/time.test.js

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 96345f51f8..5f34bc4c1d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1912,6 +1912,8 @@ wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: '
 wiki.original_git_entry_tooltip = View original Git file instead of using friendly link.
 
 activity = Activity
+activity.navbar.pulse = Pulse
+activity.navbar.contributors = Contributors
 activity.period.filter_label = Period:
 activity.period.daily = 1 day
 activity.period.halfweekly = 3 days
@@ -1977,6 +1979,16 @@ activity.git_stats_and_deletions = and
 activity.git_stats_deletion_1 = %d deletion
 activity.git_stats_deletion_n = %d deletions
 
+contributors = Contributors
+contributors.contribution_type.filter_label = Contribution type:
+contributors.contribution_type.commits = Commits
+contributors.contribution_type.additions = Additions
+contributors.contribution_type.deletions = Deletions
+contributors.loading_title = Loading contributions...
+contributors.loading_title_failed = Could not load contributions
+contributors.loading_info = This might take a bit…
+contributors.component_failed_to_load = An unexpected error happened.
+
 search = Search
 search.search_repo = Search repository
 search.type.tooltip = Search type
diff --git a/package-lock.json b/package-lock.json
index 62bf36e7b7..764ae51f9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,8 +19,12 @@
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
         "asciinema-player": "3.6.3",
+        "chart.js": "4.3.0",
+        "chartjs-adapter-dayjs-4": "1.0.4",
+        "chartjs-plugin-zoom": "2.0.1",
         "clippie": "4.0.6",
         "css-loader": "6.10.0",
+        "dayjs": "1.11.10",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
         "esbuild-loader": "4.0.3",
@@ -47,6 +51,7 @@
         "uint8-to-base64": "0.2.0",
         "vue": "3.4.18",
         "vue-bar-graph": "2.0.0",
+        "vue-chartjs": "5.3.0",
         "vue-loader": "17.4.2",
         "vue3-calendar-heatmap": "2.0.5",
         "webpack": "5.90.1",
@@ -1278,6 +1283,11 @@
         "jsep": "^0.4.0||^1.0.0"
       }
     },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+      "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+    },
     "node_modules/@mcaptcha/core-glue": {
       "version": "0.1.0-alpha-5",
       "resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz",
@@ -3329,6 +3339,40 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/chart.js": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz",
+      "integrity": "sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=7"
+      }
+    },
+    "node_modules/chartjs-adapter-dayjs-4": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs-4/-/chartjs-adapter-dayjs-4-1.0.4.tgz",
+      "integrity": "sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==",
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "chart.js": ">=4.0.1",
+        "dayjs": "^1.9.7"
+      }
+    },
+    "node_modules/chartjs-plugin-zoom": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz",
+      "integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==",
+      "dependencies": {
+        "hammerjs": "^2.0.8"
+      },
+      "peerDependencies": {
+        "chart.js": ">=3.2.0"
+      }
+    },
     "node_modules/check-error": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -5868,9 +5912,17 @@
       "dev": true
     },
     "node_modules/gsap": {
-      "version": "3.12.5",
-      "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
-      "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ=="
+      "version": "3.12.2",
+      "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.2.tgz",
+      "integrity": "sha512-EkYnpG8qHgYBFAwsgsGEqvT1WUidX0tt/ijepx7z8EUJHElykg91RvW1XbkT59T0gZzzszOpjQv7SE41XuIXyQ=="
+    },
+    "node_modules/hammerjs": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
+      "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
+      "engines": {
+        "node": ">=0.8.0"
+      }
     },
     "node_modules/has-bigints": {
       "version": "1.0.2",
@@ -10934,6 +10986,15 @@
         "vue": "^3.2.37"
       }
     },
+    "node_modules/vue-chartjs": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
+      "integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==",
+      "peerDependencies": {
+        "chart.js": "^4.1.1",
+        "vue": "^3.0.0-0 || ^2.7.0"
+      }
+    },
     "node_modules/vue-eslint-parser": {
       "version": "9.4.2",
       "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz",
diff --git a/package.json b/package.json
index 46dfdd1055..dbb57b1624 100644
--- a/package.json
+++ b/package.json
@@ -18,8 +18,12 @@
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
     "asciinema-player": "3.6.3",
+    "chart.js": "4.3.0",
+    "chartjs-adapter-dayjs-4": "1.0.4",
+    "chartjs-plugin-zoom": "2.0.1",
     "clippie": "4.0.6",
     "css-loader": "6.10.0",
+    "dayjs": "1.11.10",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
     "esbuild-loader": "4.0.3",
@@ -46,6 +50,7 @@
     "uint8-to-base64": "0.2.0",
     "vue": "3.4.18",
     "vue-bar-graph": "2.0.0",
+    "vue-chartjs": "5.3.0",
     "vue-loader": "17.4.2",
     "vue3-calendar-heatmap": "2.0.5",
     "webpack": "5.90.1",
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 3d030edaca..af99c4ed98 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -22,6 +22,8 @@ func Activity(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.activity")
 	ctx.Data["PageIsActivity"] = true
 
+	ctx.Data["PageIsPulse"] = true
+
 	ctx.Data["Period"] = ctx.Params("period")
 
 	timeUntil := time.Now()
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
new file mode 100644
index 0000000000..f7dedc0b34
--- /dev/null
+++ b/routers/web/repo/contributors.go
@@ -0,0 +1,44 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplContributors base.TplName = "repo/activity"
+)
+
+// Contributors render the page to show repository contributors graph
+func Contributors(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.contributors")
+
+	ctx.Data["PageIsActivity"] = true
+	ctx.Data["PageIsContributors"] = true
+
+	ctx.PageData["contributionType"] = "commits"
+
+	ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+	ctx.HTML(http.StatusOK, tplContributors)
+}
+
+// ContributorsData renders JSON of contributors along with their weekly commit statistics
+func ContributorsData(ctx *context.Context) {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+			ctx.Status(http.StatusAccepted)
+			return
+		}
+		ctx.ServerError("GetContributorStats", err)
+	} else {
+		ctx.JSON(http.StatusOK, contributorStats)
+	}
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 7aa9bb0795..a6288caaf6 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1392,6 +1392,10 @@ func registerRoutes(m *web.Route) {
 		m.Group("/activity", func() {
 			m.Get("", repo.Activity)
 			m.Get("/{period}", repo.Activity)
+			m.Group("/contributors", func() {
+				m.Get("", repo.Contributors)
+				m.Get("/data", repo.ContributorsData)
+			})
 		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
 
 		m.Group("/activity_author_data", func() {
diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
new file mode 100644
index 0000000000..8421df8e3a
--- /dev/null
+++ b/services/repository/contributors_graph.go
@@ -0,0 +1,319 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"bufio"
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"code.gitea.io/gitea/models/avatars"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/graceful"
+	"code.gitea.io/gitea/modules/log"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"gitea.com/go-chi/cache"
+)
+
+const (
+	contributorStatsCacheKey           = "GetContributorStats/%s/%s"
+	contributorStatsCacheTimeout int64 = 60 * 10
+)
+
+var (
+	ErrAwaitGeneration  = errors.New("generation took longer than ")
+	awaitGenerationTime = time.Second * 5
+	generateLock        = sync.Map{}
+)
+
+type WeekData struct {
+	Week      int64 `json:"week"`      // Starting day of the week as Unix timestamp
+	Additions int   `json:"additions"` // Number of additions in that week
+	Deletions int   `json:"deletions"` // Number of deletions in that week
+	Commits   int   `json:"commits"`   // Number of commits in that week
+}
+
+// ContributorData represents statistical git commit count data
+type ContributorData struct {
+	Name         string              `json:"name"`  // Display name of the contributor
+	Login        string              `json:"login"` // Login name of the contributor in case it exists
+	AvatarLink   string              `json:"avatar_link"`
+	HomeLink     string              `json:"home_link"`
+	TotalCommits int64               `json:"total_commits"`
+	Weeks        map[int64]*WeekData `json:"weeks"`
+}
+
+// ExtendedCommitStats contains information for commit stats with author data
+type ExtendedCommitStats struct {
+	Author *api.CommitUser  `json:"author"`
+	Stats  *api.CommitStats `json:"stats"`
+}
+
+const layout = time.DateOnly
+
+func findLastSundayBeforeDate(dateStr string) (string, error) {
+	date, err := time.Parse(layout, dateStr)
+	if err != nil {
+		return "", err
+	}
+
+	weekday := date.Weekday()
+	daysToSubtract := int(weekday) - int(time.Sunday)
+	if daysToSubtract < 0 {
+		daysToSubtract += 7
+	}
+
+	lastSunday := date.AddDate(0, 0, -daysToSubtract)
+	return lastSunday.Format(layout), nil
+}
+
+// GetContributorStats returns contributors stats for git commits for given revision or default branch
+func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) {
+	// as GetContributorStats is resource intensive we cache the result
+	cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision)
+	if !cache.IsExist(cacheKey) {
+		genReady := make(chan struct{})
+
+		// dont start multible async generations
+		_, run := generateLock.Load(cacheKey)
+		if run {
+			return nil, ErrAwaitGeneration
+		}
+
+		generateLock.Store(cacheKey, struct{}{})
+		// run generation async
+		go generateContributorStats(genReady, cache, cacheKey, repo, revision)
+
+		select {
+		case <-time.After(awaitGenerationTime):
+			return nil, ErrAwaitGeneration
+		case <-genReady:
+			// we got generation ready before timeout
+			break
+		}
+	}
+	// TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout)
+
+	switch v := cache.Get(cacheKey).(type) {
+	case error:
+		return nil, v
+	case map[string]*ContributorData:
+		return v, nil
+	default:
+		return nil, fmt.Errorf("unexpected type in cache detected")
+	}
+}
+
+// getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision
+func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) {
+	baseCommit, err := repo.GetCommit(revision)
+	if err != nil {
+		return nil, err
+	}
+	stdoutReader, stdoutWriter, err := os.Pipe()
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		_ = stdoutReader.Close()
+		_ = stdoutWriter.Close()
+	}()
+
+	gitCmd := git.NewCommand(repo.Ctx, "log", "--shortstat", "--no-merges", "--pretty=format:---%n%aN%n%aE%n%as", "--reverse")
+	// AddOptionFormat("--max-count=%d", limit)
+	gitCmd.AddDynamicArguments(baseCommit.ID.String())
+
+	var extendedCommitStats []*ExtendedCommitStats
+	stderr := new(strings.Builder)
+	err = gitCmd.Run(&git.RunOpts{
+		Dir:    repo.Path,
+		Stdout: stdoutWriter,
+		Stderr: stderr,
+		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+			_ = stdoutWriter.Close()
+			scanner := bufio.NewScanner(stdoutReader)
+			scanner.Split(bufio.ScanLines)
+
+			for scanner.Scan() {
+				line := strings.TrimSpace(scanner.Text())
+				if line != "---" {
+					continue
+				}
+				scanner.Scan()
+				authorName := strings.TrimSpace(scanner.Text())
+				scanner.Scan()
+				authorEmail := strings.TrimSpace(scanner.Text())
+				scanner.Scan()
+				date := strings.TrimSpace(scanner.Text())
+				scanner.Scan()
+				stats := strings.TrimSpace(scanner.Text())
+				if authorName == "" || authorEmail == "" || date == "" || stats == "" {
+					// FIXME: find a better way to parse the output so that we will handle this properly
+					log.Warn("Something is wrong with git log output, skipping...")
+					log.Warn("authorName: %s,  authorEmail: %s,  date: %s,  stats: %s", authorName, authorEmail, date, stats)
+					continue
+				}
+				//  1 file changed, 1 insertion(+), 1 deletion(-)
+				fields := strings.Split(stats, ",")
+
+				commitStats := api.CommitStats{}
+				for _, field := range fields[1:] {
+					parts := strings.Split(strings.TrimSpace(field), " ")
+					value, contributionType := parts[0], parts[1]
+					amount, _ := strconv.Atoi(value)
+
+					if strings.HasPrefix(contributionType, "insertion") {
+						commitStats.Additions = amount
+					} else {
+						commitStats.Deletions = amount
+					}
+				}
+				commitStats.Total = commitStats.Additions + commitStats.Deletions
+				scanner.Scan()
+				scanner.Text() // empty line at the end
+
+				res := &ExtendedCommitStats{
+					Author: &api.CommitUser{
+						Identity: api.Identity{
+							Name:  authorName,
+							Email: authorEmail,
+						},
+						Date: date,
+					},
+					Stats: &commitStats,
+				}
+				extendedCommitStats = append(extendedCommitStats, res)
+
+			}
+			_ = stdoutReader.Close()
+			return nil
+		},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get ContributorsCommitStats for repository.\nError: %w\nStderr: %s", err, stderr)
+	}
+
+	return extendedCommitStats, nil
+}
+
+func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey string, repo *repo_model.Repository, revision string) {
+	ctx := graceful.GetManager().HammerContext()
+
+	gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+	if err != nil {
+		err := fmt.Errorf("OpenRepository: %w", err)
+		_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+		return
+	}
+	defer closer.Close()
+
+	if len(revision) == 0 {
+		revision = repo.DefaultBranch
+	}
+	extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision)
+	if err != nil {
+		err := fmt.Errorf("ExtendedCommitStats: %w", err)
+		_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+		return
+	}
+	if len(extendedCommitStats) == 0 {
+		err := fmt.Errorf("no commit stats returned for revision '%s'", revision)
+		_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+		return
+	}
+
+	layout := time.DateOnly
+
+	unknownUserAvatarLink := user_model.NewGhostUser().AvatarLinkWithSize(ctx, 0)
+	contributorsCommitStats := make(map[string]*ContributorData)
+	contributorsCommitStats["total"] = &ContributorData{
+		Name:  "Total",
+		Weeks: make(map[int64]*WeekData),
+	}
+	total := contributorsCommitStats["total"]
+
+	for _, v := range extendedCommitStats {
+		userEmail := v.Author.Email
+		if len(userEmail) == 0 {
+			continue
+		}
+		u, _ := user_model.GetUserByEmail(ctx, userEmail)
+		if u != nil {
+			// update userEmail with user's primary email address so
+			// that different mail addresses will linked to same account
+			userEmail = u.GetEmail()
+		}
+		// duplicated logic
+		if _, ok := contributorsCommitStats[userEmail]; !ok {
+			if u == nil {
+				avatarLink := avatars.GenerateEmailAvatarFastLink(ctx, userEmail, 0)
+				if avatarLink == "" {
+					avatarLink = unknownUserAvatarLink
+				}
+				contributorsCommitStats[userEmail] = &ContributorData{
+					Name:       v.Author.Name,
+					AvatarLink: avatarLink,
+					Weeks:      make(map[int64]*WeekData),
+				}
+			} else {
+				contributorsCommitStats[userEmail] = &ContributorData{
+					Name:       u.DisplayName(),
+					Login:      u.LowerName,
+					AvatarLink: u.AvatarLinkWithSize(ctx, 0),
+					HomeLink:   u.HomeLink(),
+					Weeks:      make(map[int64]*WeekData),
+				}
+			}
+		}
+		// Update user statistics
+		user := contributorsCommitStats[userEmail]
+		startingOfWeek, _ := findLastSundayBeforeDate(v.Author.Date)
+
+		val, _ := time.Parse(layout, startingOfWeek)
+		week := val.UnixMilli()
+
+		if user.Weeks[week] == nil {
+			user.Weeks[week] = &WeekData{
+				Additions: 0,
+				Deletions: 0,
+				Commits:   0,
+				Week:      week,
+			}
+		}
+		if total.Weeks[week] == nil {
+			total.Weeks[week] = &WeekData{
+				Additions: 0,
+				Deletions: 0,
+				Commits:   0,
+				Week:      week,
+			}
+		}
+		user.Weeks[week].Additions += v.Stats.Additions
+		user.Weeks[week].Deletions += v.Stats.Deletions
+		user.Weeks[week].Commits++
+		user.TotalCommits++
+
+		// Update overall statistics
+		total.Weeks[week].Additions += v.Stats.Additions
+		total.Weeks[week].Deletions += v.Stats.Deletions
+		total.Weeks[week].Commits++
+		total.TotalCommits++
+	}
+
+	_ = cache.Put(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout)
+	generateLock.Delete(cacheKey)
+	if genDone != nil {
+		genDone <- struct{}{}
+	}
+}
diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go
new file mode 100644
index 0000000000..3801a5eee4
--- /dev/null
+++ b/services/repository/contributors_graph_test.go
@@ -0,0 +1,87 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"slices"
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/git"
+
+	"gitea.com/go-chi/cache"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRepository_ContributorsGraph(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+	assert.NoError(t, repo.LoadOwner(db.DefaultContext))
+	mockCache, err := cache.NewCacher(cache.Options{
+		Adapter:  "memory",
+		Interval: 24 * 60,
+	})
+	assert.NoError(t, err)
+
+	generateContributorStats(nil, mockCache, "key", repo, "404ref")
+	err, isErr := mockCache.Get("key").(error)
+	assert.True(t, isErr)
+	assert.ErrorAs(t, err, &git.ErrNotExist{})
+
+	generateContributorStats(nil, mockCache, "key2", repo, "master")
+	data, isData := mockCache.Get("key2").(map[string]*ContributorData)
+	assert.True(t, isData)
+	var keys []string
+	for k := range data {
+		keys = append(keys, k)
+	}
+	slices.Sort(keys)
+	assert.EqualValues(t, []string{
+		"ethantkoenig@gmail.com",
+		"jimmy.praet@telenet.be",
+		"jon@allspice.io",
+		"total", // generated summary
+	}, keys)
+
+	assert.EqualValues(t, &ContributorData{
+		Name:         "Ethan Koenig",
+		AvatarLink:   "https://secure.gravatar.com/avatar/b42fb195faa8c61b8d88abfefe30e9e3?d=identicon",
+		TotalCommits: 1,
+		Weeks: map[int64]*WeekData{
+			1511654400000: {
+				Week:      1511654400000, // sunday 2017-11-26
+				Additions: 3,
+				Deletions: 0,
+				Commits:   1,
+			},
+		},
+	}, data["ethantkoenig@gmail.com"])
+	assert.EqualValues(t, &ContributorData{
+		Name:         "Total",
+		AvatarLink:   "",
+		TotalCommits: 3,
+		Weeks: map[int64]*WeekData{
+			1511654400000: {
+				Week:      1511654400000, // sunday 2017-11-26 (2017-11-26 20:31:18 -0800)
+				Additions: 3,
+				Deletions: 0,
+				Commits:   1,
+			},
+			1607817600000: {
+				Week:      1607817600000, // sunday 2020-12-13 (2020-12-15 15:23:11 -0500)
+				Additions: 10,
+				Deletions: 0,
+				Commits:   1,
+			},
+			1624752000000: {
+				Week:      1624752000000, // sunday 2021-06-27 (2021-06-29 21:54:09 +0200)
+				Additions: 2,
+				Deletions: 0,
+				Commits:   1,
+			},
+		},
+	}, data["total"])
+}
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index 3149f20670..960083d2fb 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -1,235 +1,15 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content repository commits">
 	{{template "repo/header" .}}
-	<div class="ui container">
-		<h2 class="ui header activity-header">
-			<span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span>
-			<!-- Period -->
-			<div class="ui floating dropdown jump filter">
-				<div class="ui basic compact button">
-					{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
-					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-				</div>
-				<div class="menu">
-					<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a>
-					<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a>
-					<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a>
-					<a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a>
-					<a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a>
-					<a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a>
-					<a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a>
-				</div>
-			</div>
-		</h2>
-		<div class="divider"></div>
-
-		{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
-		<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4>
-		<div class="ui attached segment two column grid">
-			{{if .Permission.CanRead $.UnitTypePullRequests}}
-				<div class="column">
-					{{if gt .Activity.ActivePRCount 0}}
-					<div class="stats-table">
-						<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a>
-						<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
-					</div>
-					{{else}}
-					<div class="stats-table">
-						<a class="table-cell tiny background light grey"></a>
-					</div>
-					{{end}}
-					{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
-				</div>
-			{{end}}
-			{{if .Permission.CanRead $.UnitTypeIssues}}
-				<div class="column">
-					{{if gt .Activity.ActiveIssueCount 0}}
-					<div class="stats-table">
-						<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a>
-						<a href="#new-issues" class="table-cell tiny background green"></a>
-					</div>
-					{{else}}
-					<div class="stats-table">
-						<a class="table-cell tiny background light grey"></a>
-					</div>
-					{{end}}
-					{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
-				</div>
-			{{end}}
+	<div class="ui container flex-container">
+		<div class="flex-container-nav">
+			{{template "repo/navbar" .}}
 		</div>
-		<div class="ui attached segment horizontal segments">
-			{{if .Permission.CanRead $.UnitTypePullRequests}}
-				<a href="#merged-pull-requests" class="ui attached segment text center">
-					<span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
-				</a>
-				<a href="#proposed-pull-requests" class="ui attached segment text center">
-					<span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
-				</a>
-			{{end}}
-			{{if .Permission.CanRead $.UnitTypeIssues}}
-				<a href="#closed-issues" class="ui attached segment text center">
-					<span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
-				</a>
-				<a href="#new-issues" class="ui attached segment text center">
-					<span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
-				</a>
-			{{end}}
+		<div class="flex-container-main">
+			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
+			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
 		</div>
-		{{end}}
-
-		{{if .Permission.CanRead $.UnitTypeCode}}
-			{{if eq .Activity.Code.CommitCountInAllBranches 0}}
-				<div class="ui center aligned segment">
-				<h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4>
-				</div>
-			{{end}}
-			{{if gt .Activity.Code.CommitCountInAllBranches 0}}
-				<div class="ui attached segment horizontal segments">
-					<div class="ui attached segment text">
-						{{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong>
-						{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong>
-						{{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong>
-						{{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
-						{{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong>
-						{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
-						{{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
-						<strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong>
-						{{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
-						<strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>.
-					</div>
-					<div class="ui attached segment">
-						<div id="repo-activity-top-authors-chart"></div>
-					</div>
-				</div>
-			{{end}}
-		{{end}}
-
-		{{if gt .Activity.PublishedReleaseCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="published-releases">
-				{{svg "octicon-tag" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
-					(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
-					(ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.PublishedReleases}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
-						{{.TagName}}
-						{{if not .IsTag}}
-							<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{end}}
-						{{TimeSinceUnix .CreatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.MergedPRCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="merged-pull-requests">
-				{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
-					(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
-					(ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.MergedPRs}}
-					<p class="desc">
-						<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .MergedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.OpenedPRCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="proposed-pull-requests">
-				{{svg "octicon-git-branch" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
-					(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
-					(ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.OpenedPRs}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.ClosedIssueCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="closed-issues">
-				{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
-					(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
-					(ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.ClosedIssues}}
-					<p class="desc">
-						<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .ClosedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.OpenedIssueCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="new-issues">
-				{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
-					(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
-					(ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.OpenedIssues}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .CreatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.UnresolvedIssueCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
-				{{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
-				{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
-			</h4>
-			<div class="list">
-				{{range .Activity.UnresolvedIssues}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
-						#{{.Index}}
-						{{if .IsPull}}
-						<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{else}}
-						<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{end}}
-						{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
 	</div>
 </div>
 {{template "base/footer" .}}
+
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl
new file mode 100644
index 0000000000..49a251c1f9
--- /dev/null
+++ b/templates/repo/contributors.tmpl
@@ -0,0 +1,13 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+	<div id="repo-contributors-chart"
+		data-locale-filter-label="{{ctx.Locale.Tr "repo.contributors.contribution_type.filter_label"}}"
+		data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}"
+		data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}"
+		data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}"
+		data-locale-loading-title="{{ctx.Locale.Tr "repo.contributors.loading_title"}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "repo.contributors.loading_title_failed"}}"
+		data-locale-loading-info="{{ctx.Locale.Tr "repo.contributors.loading_info"}}"
+		data-locale-component-failed-to-load="{{ctx.Locale.Tr "repo.contributors.component_failed_to_load"}}"
+	>
+	</div>
+{{end}}
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
new file mode 100644
index 0000000000..a9042ee30d
--- /dev/null
+++ b/templates/repo/navbar.tmpl
@@ -0,0 +1,8 @@
+<div class="ui fluid vertical menu">
+	<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
+		{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
+	</a>
+	<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
+		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
+	</a>
+</div>
diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl
new file mode 100644
index 0000000000..ccd7ebf6b5
--- /dev/null
+++ b/templates/repo/pulse.tmpl
@@ -0,0 +1,227 @@
+<h2 class="ui header activity-header">
+	<span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span>
+	<!-- Period -->
+	<div class="ui floating dropdown jump filter">
+		<div class="ui basic compact button">
+			{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+		</div>
+		<div class="menu">
+			<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a>
+			<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a>
+			<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a>
+			<a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a>
+			<a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a>
+			<a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a>
+			<a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a>
+		</div>
+	</div>
+</h2>
+
+{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
+<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4>
+<div class="ui attached segment two column grid">
+	{{if .Permission.CanRead $.UnitTypePullRequests}}
+		<div class="column">
+			{{if gt .Activity.ActivePRCount 0}}
+			<div class="stats-table">
+				<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a>
+				<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
+			</div>
+			{{else}}
+			<div class="stats-table">
+				<a class="table-cell tiny background light grey"></a>
+			</div>
+			{{end}}
+			{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
+		</div>
+	{{end}}
+	{{if .Permission.CanRead $.UnitTypeIssues}}
+		<div class="column">
+			{{if gt .Activity.ActiveIssueCount 0}}
+			<div class="stats-table">
+				<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a>
+				<a href="#new-issues" class="table-cell tiny background green"></a>
+			</div>
+			{{else}}
+			<div class="stats-table">
+				<a class="table-cell tiny background light grey"></a>
+			</div>
+			{{end}}
+			{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
+		</div>
+	{{end}}
+</div>
+<div class="ui attached segment horizontal segments">
+	{{if .Permission.CanRead $.UnitTypePullRequests}}
+		<a href="#merged-pull-requests" class="ui attached segment text center">
+			<span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
+		</a>
+		<a href="#proposed-pull-requests" class="ui attached segment text center">
+			<span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
+		</a>
+	{{end}}
+	{{if .Permission.CanRead $.UnitTypeIssues}}
+		<a href="#closed-issues" class="ui attached segment text center">
+			<span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
+		</a>
+		<a href="#new-issues" class="ui attached segment text center">
+			<span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
+		</a>
+	{{end}}
+</div>
+{{end}}
+
+{{if .Permission.CanRead $.UnitTypeCode}}
+	{{if eq .Activity.Code.CommitCountInAllBranches 0}}
+		<div class="ui center aligned segment">
+		<h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4>
+		</div>
+	{{end}}
+	{{if gt .Activity.Code.CommitCountInAllBranches 0}}
+		<div class="ui attached segment horizontal segments">
+			<div class="ui attached segment text">
+				{{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong>
+				{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong>
+				{{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong>
+				{{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
+				{{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong>
+				{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
+				{{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
+				<strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong>
+				{{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
+				<strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>.
+			</div>
+			<div class="ui attached segment">
+				<div id="repo-activity-top-authors-chart"></div>
+			</div>
+		</div>
+	{{end}}
+{{end}}
+
+{{if gt .Activity.PublishedReleaseCount 0}}
+	<h4 class="divider divider-text gt-normal-case" id="published-releases">
+		{{svg "octicon-tag" 16 "gt-mr-3"}}
+		{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
+			(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
+			(ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.PublishedReleases}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
+				{{.TagName}}
+				{{if not .IsTag}}
+					<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{end}}
+				{{TimeSinceUnix .CreatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.MergedPRCount 0}}
+	<h4 class="divider divider-text gt-normal-case" id="merged-pull-requests">
+		{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
+		{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
+			(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
+			(ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.MergedPRs}}
+			<p class="desc">
+				<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .MergedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.OpenedPRCount 0}}
+	<h4 class="divider divider-text gt-normal-case" id="proposed-pull-requests">
+		{{svg "octicon-git-branch" 16 "gt-mr-3"}}
+		{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
+			(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
+			(ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.OpenedPRs}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.ClosedIssueCount 0}}
+	<h4 class="divider divider-text gt-normal-case" id="closed-issues">
+		{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
+		{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
+			(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
+			(ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.ClosedIssues}}
+			<p class="desc">
+				<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .ClosedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.OpenedIssueCount 0}}
+	<h4 class="divider divider-text gt-normal-case" id="new-issues">
+		{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+		{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
+			(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
+			(ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.OpenedIssues}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .CreatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.UnresolvedIssueCount 0}}
+	<h4 class="divider divider-text gt-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
+		{{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
+		{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
+	</h4>
+	<div class="list">
+		{{range .Activity.UnresolvedIssues}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
+				#{{.Index}}
+				{{if .IsPull}}
+				<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{else}}
+				<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{end}}
+				{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
diff --git a/web_src/js/components/.eslintrc.yaml b/web_src/js/components/.eslintrc.yaml
index 0cab470f6b..0d233442bc 100644
--- a/web_src/js/components/.eslintrc.yaml
+++ b/web_src/js/components/.eslintrc.yaml
@@ -7,6 +7,10 @@ extends:
   - plugin:vue/vue3-recommended
   - plugin:vue-scoped-css/vue3-recommended
 
+parserOptions:
+  sourceType: module
+  ecmaVersion: latest
+
 env:
   browser: true
 
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
new file mode 100644
index 0000000000..fa1545b3df
--- /dev/null
+++ b/web_src/js/components/RepoContributors.vue
@@ -0,0 +1,443 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+  Chart,
+  Title,
+  Tooltip,
+  Legend,
+  BarElement,
+  CategoryScale,
+  LinearScale,
+  TimeScale,
+  PointElement,
+  LineElement,
+  Filler,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import zoomPlugin from 'chartjs-plugin-zoom';
+import {Line as ChartLine} from 'vue-chartjs';
+import {
+  startDaysBetween,
+  firstStartDateAfterDate,
+  fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+import $ from 'jquery';
+
+const {pageData} = window.config;
+
+const colors = {
+  text: '--color-text',
+  border: '--color-secondary-alpha-60',
+  commits: '--color-primary-alpha-60',
+  additions: '--color-green',
+  deletions: '--color-red',
+  title: '--color-secondary-dark-4',
+};
+
+const styles = window.getComputedStyle(document.documentElement);
+const getColor = (name) => styles.getPropertyValue(name).trim();
+
+for (const [key, value] of Object.entries(colors)) {
+  colors[key] = getColor(value);
+}
+
+const customEventListener = {
+  id: 'customEventListener',
+  afterEvent: (chart, args, opts) => {
+    // event will be replayed from chart.update when reset zoom,
+    // so we need to check whether args.replay is true to avoid call loops
+    if (args.event.type === 'dblclick' && opts.chartType === 'main' && !args.replay) {
+      chart.resetZoom();
+      opts.instance.updateOtherCharts(args.event, true);
+    }
+  }
+};
+
+Chart.defaults.color = colors.text;
+Chart.defaults.borderColor = colors.border;
+
+Chart.register(
+  TimeScale,
+  CategoryScale,
+  LinearScale,
+  BarElement,
+  Title,
+  Tooltip,
+  Legend,
+  PointElement,
+  LineElement,
+  Filler,
+  zoomPlugin,
+  customEventListener,
+);
+
+export default {
+  components: {ChartLine, SvgIcon},
+  props: {
+    locale: {
+      type: Object,
+      required: true,
+    },
+  },
+  data: () => ({
+    isLoading: false,
+    errorText: '',
+    totalStats: {},
+    sortedContributors: {},
+    repoLink: pageData.repoLink || [],
+    type: pageData.contributionType,
+    contributorsStats: [],
+    xAxisStart: null,
+    xAxisEnd: null,
+    xAxisMin: null,
+    xAxisMax: null,
+  }),
+  mounted() {
+    this.fetchGraphData();
+
+    $('#repo-contributors').dropdown({
+      onChange: (val) => {
+        this.xAxisMin = this.xAxisStart;
+        this.xAxisMax = this.xAxisEnd;
+        this.type = val;
+        this.sortContributors();
+      }
+    });
+  },
+  methods: {
+    sortContributors() {
+      const contributors = this.filterContributorWeeksByDateRange();
+      const criteria = `total_${this.type}`;
+      this.sortedContributors = Object.values(contributors)
+        .filter((contributor) => contributor[criteria] !== 0)
+        .sort((a, b) => a[criteria] > b[criteria] ? -1 : a[criteria] === b[criteria] ? 0 : 1)
+        .slice(0, 100);
+    },
+
+    async fetchGraphData() {
+      this.isLoading = true;
+      try {
+        let response;
+        do {
+          response = await GET(`${this.repoLink}/activity/contributors/data`);
+          if (response.status === 202) {
+            await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second before retrying
+          }
+        } while (response.status === 202);
+        if (response.ok) {
+          const data = await response.json();
+          const {total, ...rest} = data;
+          // below line might be deleted if we are sure go produces map always sorted by keys
+          total.weeks = Object.fromEntries(Object.entries(total.weeks).sort());
+
+          const weekValues = Object.values(total.weeks);
+          this.xAxisStart = weekValues[0].week;
+          this.xAxisEnd = firstStartDateAfterDate(new Date());
+          const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
+          total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
+          this.xAxisMin = this.xAxisStart;
+          this.xAxisMax = this.xAxisEnd;
+          this.contributorsStats = {};
+          for (const [email, user] of Object.entries(rest)) {
+            user.weeks = fillEmptyStartDaysWithZeroes(startDays, user.weeks);
+            this.contributorsStats[email] = user;
+          }
+          this.sortContributors();
+          this.totalStats = total;
+          this.errorText = '';
+        } else {
+          this.errorText = response.statusText;
+        }
+      } catch (err) {
+        this.errorText = err.message;
+      } finally {
+        this.isLoading = false;
+      }
+    },
+
+    filterContributorWeeksByDateRange() {
+      const filteredData = {};
+      const data = this.contributorsStats;
+      for (const key of Object.keys(data)) {
+        const user = data[key];
+        user.total_commits = 0;
+        user.total_additions = 0;
+        user.total_deletions = 0;
+        user.max_contribution_type = 0;
+        const filteredWeeks = user.weeks.filter((week) => {
+          const oneWeek = 7 * 24 * 60 * 60 * 1000;
+          if (week.week >= this.xAxisMin - oneWeek && week.week <= this.xAxisMax + oneWeek) {
+            user.total_commits += week.commits;
+            user.total_additions += week.additions;
+            user.total_deletions += week.deletions;
+            if (week[this.type] > user.max_contribution_type) {
+              user.max_contribution_type = week[this.type];
+            }
+            return true;
+          }
+          return false;
+        });
+        // this line is required. See https://github.com/sahinakkaya/gitea/pull/3#discussion_r1396495722
+        // for details.
+        user.max_contribution_type += 1;
+
+        filteredData[key] = {...user, weeks: filteredWeeks};
+      }
+
+      return filteredData;
+    },
+
+    maxMainGraph() {
+      // This method calculates maximum value for Y value of the main graph. If the number
+      // of maximum contributions for selected contribution type is 15.955 it is probably
+      // better to round it up to 20.000.This method is responsible for doing that.
+      // Normally, chartjs handles this automatically, but it will resize the graph when you
+      // zoom, pan etc. I think resizing the graph makes it harder to compare things visually.
+      const maxValue = Math.max(
+        ...this.totalStats.weeks.map((o) => o[this.type])
+      );
+      const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
+      if (coefficient % 1 === 0) return maxValue;
+      return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
+    },
+
+    maxContributorGraph() {
+      // Similar to maxMainGraph method this method calculates maximum value for Y value
+      // for contributors' graph. If I let chartjs do this for me, it will choose different
+      // maxY value for each contributors' graph which again makes it harder to compare.
+      const maxValue = Math.max(
+        ...this.sortedContributors.map((c) => c.max_contribution_type)
+      );
+      const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
+      if (coefficient % 1 === 0) return maxValue;
+      return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
+    },
+
+    toGraphData(data) {
+      return {
+        datasets: [
+          {
+            data: data.map((i) => ({x: i.week, y: i[this.type]})),
+            pointRadius: 0,
+            pointHitRadius: 0,
+            fill: 'start',
+            backgroundColor: colors[this.type],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+        ],
+      };
+    },
+
+    updateOtherCharts(event, reset) {
+      const minVal = event.chart.options.scales.x.min;
+      const maxVal = event.chart.options.scales.x.max;
+      if (reset) {
+        this.xAxisMin = this.xAxisStart;
+        this.xAxisMax = this.xAxisEnd;
+        this.sortContributors();
+      } else if (minVal) {
+        this.xAxisMin = minVal;
+        this.xAxisMax = maxVal;
+        this.sortContributors();
+      }
+    },
+
+    getOptions(type) {
+      return {
+        responsive: true,
+        maintainAspectRatio: false,
+        animation: false,
+        events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove', 'dblclick'],
+        plugins: {
+          title: {
+            display: type === 'main',
+            text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
+            color: colors.title,
+            position: 'top',
+            align: 'center',
+          },
+          customEventListener: {
+            chartType: type,
+            instance: this,
+          },
+          legend: {
+            display: false,
+          },
+          zoom: {
+            pan: {
+              enabled: true,
+              modifierKey: 'shift',
+              mode: 'x',
+              threshold: 20,
+              onPanComplete: this.updateOtherCharts,
+            },
+            limits: {
+              x: {
+                // Check https://www.chartjs.org/chartjs-plugin-zoom/latest/guide/options.html#scale-limits
+                // to know what each option means
+                min: 'original',
+                max: 'original',
+
+                // number of milliseconds in 2 weeks. Minimum x range will be 2 weeks when you zoom on the graph
+                minRange: 2 * 7 * 24 * 60 * 60 * 1000,
+              },
+            },
+            zoom: {
+              drag: {
+                enabled: type === 'main',
+              },
+              pinch: {
+                enabled: type === 'main',
+              },
+              mode: 'x',
+              onZoomComplete: this.updateOtherCharts,
+            },
+          },
+        },
+        scales: {
+          x: {
+            min: this.xAxisMin,
+            max: this.xAxisMax,
+            type: 'time',
+            grid: {
+              display: false,
+            },
+            time: {
+              minUnit: 'month',
+            },
+            ticks: {
+              maxRotation: 0,
+              maxTicksLimit: type === 'main' ? 12 : 6,
+            },
+          },
+          y: {
+            min: 0,
+            max: type === 'main' ? this.maxMainGraph() : this.maxContributorGraph(),
+            ticks: {
+              maxTicksLimit: type === 'main' ? 6 : 4,
+            },
+          },
+        },
+      };
+    },
+  },
+};
+</script>
+<template>
+  <div>
+    <h2 class="ui header gt-df gt-ac gt-sb">
+      <div>
+        <relative-time
+          v-if="xAxisMin > 0"
+          format="datetime"
+          year="numeric"
+          month="short"
+          day="numeric"
+          weekday=""
+          :datetime="new Date(xAxisMin)"
+        >
+          {{ new Date(xAxisMin) }}
+        </relative-time>
+        {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "-" }}
+        <relative-time
+          v-if="xAxisMax > 0"
+          format="datetime"
+          year="numeric"
+          month="short"
+          day="numeric"
+          weekday=""
+          :datetime="new Date(xAxisMax)"
+        >
+          {{ new Date(xAxisMax) }}
+        </relative-time>
+      </div>
+      <div>
+        <!-- Contribution type -->
+        <div class="ui dropdown jump" id="repo-contributors">
+          <div class="ui basic compact button">
+            <span class="text">
+              {{ locale.filterLabel }} <strong>{{ locale.contributionType[type] }}</strong>
+              <svg-icon name="octicon-triangle-down" :size="14"/>
+            </span>
+          </div>
+          <div class="menu">
+            <div :class="['item', {'active': type === 'commits'}]">
+              {{ locale.contributionType.commits }}
+            </div>
+            <div :class="['item', {'active': type === 'additions'}]">
+              {{ locale.contributionType.additions }}
+            </div>
+            <div :class="['item', {'active': type === 'deletions'}]">
+              {{ locale.contributionType.deletions }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </h2>
+    <div class="gt-df ui segment main-graph">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
+        <div v-if="isLoading">
+          <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
+          {{ locale.loadingInfo }}
+        </div>
+        <div v-else class="text red">
+          <SvgIcon name="octicon-x-circle-fill"/>
+          {{ errorText }}
+        </div>
+      </div>
+      <ChartLine
+        v-memo="[totalStats.weeks, type]" v-if="Object.keys(totalStats).length !== 0"
+        :data="toGraphData(totalStats.weeks)" :options="getOptions('main')"
+      />
+    </div>
+    <div class="contributor-grid">
+      <div
+        v-for="(contributor, index) in sortedContributors" :key="index" class="stats-table"
+        v-memo="[sortedContributors, type]"
+      >
+        <div class="ui top attached header gt-df gt-f1">
+          <b class="ui right">#{{ index + 1 }}</b>
+          <a :href="contributor.home_link">
+            <img class="ui avatar gt-vm" height="40" width="40" :src="contributor.avatar_link">
+          </a>
+          <div class="gt-ml-3">
+            <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
+            <h4 v-else class="contributor-name">
+              {{ contributor.name }}
+            </h4>
+            <p class="gt-font-12 gt-df gt-gap-2">
+              <strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong>
+              <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
+              <strong v-if="contributor.total_deletions" class="text red">
+                {{ contributor.total_deletions.toLocaleString() }}--</strong>
+            </p>
+          </div>
+        </div>
+        <div class="ui attached segment">
+          <div>
+            <ChartLine
+              :data="toGraphData(contributor.weeks)"
+              :options="getOptions('contributor')"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped>
+.main-graph {
+  height: 260px;
+}
+.contributor-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 1rem;
+}
+
+.contributor-name {
+  margin-bottom: 0;
+}
+</style>
diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js
new file mode 100644
index 0000000000..66185ac315
--- /dev/null
+++ b/web_src/js/features/contributors.js
@@ -0,0 +1,28 @@
+import {createApp} from 'vue';
+
+export async function initRepoContributors() {
+  const el = document.getElementById('repo-contributors-chart');
+  if (!el) return;
+
+  const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
+  try {
+    const View = createApp(RepoContributors, {
+      locale: {
+        filterLabel: el.getAttribute('data-locale-filter-label'),
+        contributionType: {
+          commits: el.getAttribute('data-locale-contribution-type-commits'),
+          additions: el.getAttribute('data-locale-contribution-type-additions'),
+          deletions: el.getAttribute('data-locale-contribution-type-deletions'),
+        },
+
+        loadingTitle: el.getAttribute('data-locale-loading-title'),
+        loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+        loadingInfo: el.getAttribute('data-locale-loading-info'),
+      }
+    });
+    View.mount(el);
+  } catch (err) {
+    console.error('RepoContributors failed to load', err);
+    el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+  }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 4713618506..078f9fc9df 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -83,6 +83,7 @@ import {initGiteaFomantic} from './modules/fomantic.js';
 import {onDomReady} from './utils/dom.js';
 import {initRepoIssueList} from './features/repo-issue-list.js';
 import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
+import {initRepoContributors} from './features/contributors.js';
 import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 import {initDirAuto} from './modules/dirauto.js';
 
@@ -172,6 +173,7 @@ onDomReady(() => {
   initRepoWikiForm();
   initRepository();
   initRepositoryActionView();
+  initRepoContributors();
 
   initCommitStatuses();
   initCaptcha();
diff --git a/web_src/js/utils/time.js b/web_src/js/utils/time.js
new file mode 100644
index 0000000000..3284e893e1
--- /dev/null
+++ b/web_src/js/utils/time.js
@@ -0,0 +1,46 @@
+import dayjs from 'dayjs';
+
+// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
+export function startDaysBetween(startDate, endDate) {
+  // Ensure the start date is a Sunday
+  while (startDate.getDay() !== 0) {
+    startDate.setDate(startDate.getDate() + 1);
+  }
+
+  const start = dayjs(startDate);
+  const end = dayjs(endDate);
+  const startDays = [];
+
+  let current = start;
+  while (current.isBefore(end)) {
+    startDays.push(current.valueOf());
+    // we are adding 7 * 24 hours instead of 1 week because we don't want
+    // date library to use local time zone to calculate 1 week from now.
+    // local time zone is problematic because of daylight saving time (dst)
+    // used on some countries
+    current = current.add(7 * 24, 'hour');
+  }
+
+  return startDays;
+}
+
+export function firstStartDateAfterDate(inputDate) {
+  if (!(inputDate instanceof Date)) {
+    throw new Error('Invalid date');
+  }
+  const dayOfWeek = inputDate.getDay();
+  const daysUntilSunday = 7 - dayOfWeek;
+  const resultDate = new Date(inputDate.getTime());
+  resultDate.setDate(resultDate.getDate() + daysUntilSunday);
+  return resultDate.valueOf();
+}
+
+export function fillEmptyStartDaysWithZeroes(startDays, data) {
+  const result = {};
+
+  for (const startDay of startDays) {
+    result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0};
+  }
+
+  return Object.values(result);
+}
diff --git a/web_src/js/utils/time.test.js b/web_src/js/utils/time.test.js
new file mode 100644
index 0000000000..dd1114ce7f
--- /dev/null
+++ b/web_src/js/utils/time.test.js
@@ -0,0 +1,15 @@
+import {startDaysBetween} from './time.js';
+
+test('startDaysBetween', () => {
+  expect(startDaysBetween(new Date('2024-02-15'), new Date('2024-04-18'))).toEqual([
+    1708214400000,
+    1708819200000,
+    1709424000000,
+    1710028800000,
+    1710633600000,
+    1711238400000,
+    1711843200000,
+    1712448000000,
+    1713052800000,
+  ]);
+});

From 6d4dc16c726dd0be8d0f56405ba396d44dfd04ac Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Fri, 16 Feb 2024 00:23:19 +0000
Subject: [PATCH 046/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_cs-CZ.ini | 384 ++++++++++++++++++++++++++++++--
 1 file changed, 362 insertions(+), 22 deletions(-)

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index e9428ebcd4..8d1a46c6b6 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -4,6 +4,7 @@ explore=Procházet
 help=Nápověda
 logo=Logo
 sign_in=Přihlásit se
+sign_in_with_provider=Přihlásit se pomocí %s
 sign_in_or=nebo
 sign_out=Odhlásit se
 sign_up=Registrovat se
@@ -16,6 +17,7 @@ template=Šablona
 language=Jazyk
 notifications=Oznámení
 active_stopwatch=Aktivní sledování času
+tracked_time_summary=Shrnutí sledovaného času na základě filtrů v seznamu úkolů
 create_new=Vytvořit…
 user_profile_and_more=Profily a nastavení…
 signed_in_as=Přihlášen jako
@@ -79,6 +81,7 @@ milestones=Milníky
 
 ok=OK
 cancel=Zrušit
+retry=Znovu
 rerun=Znovu spustit
 rerun_all=Znovu spustit všechny úlohy
 save=Uložit
@@ -86,14 +89,17 @@ add=Přidat
 add_all=Přidat vše
 remove=Odstranit
 remove_all=Odstranit vše
-remove_label_str=`Odstranit položku "%s"`
+remove_label_str=Odstranit položku „%s“
 edit=Upravit
+view=Zobrazit
 
 enabled=Povolený
 disabled=Zakázané
+locked=Uzamčeno
 
 copy=Kopírovat
 copy_url=Kopírovat URL
+copy_hash=Kopírovat hash
 copy_content=Kopírovat obsah
 copy_branch=Kopírovat jméno větve
 copy_success=Zkopírováno!
@@ -106,6 +112,7 @@ loading=Načítá se…
 
 error=Chyba
 error404=Stránka, kterou se snažíte zobrazit, buď <strong>neexistuje</strong>, nebo <strong>nemáte oprávnění</strong> ji zobrazit.
+go_back=Zpět
 
 never=Nikdy
 unknown=Neznámý
@@ -127,7 +134,9 @@ concept_user_organization=Organizace
 show_timestamps=Zobrazit časové značky
 show_log_seconds=Zobrazit sekundy
 show_full_screen=Zobrazit celou obrazovku
+download_logs=Stáhnout logy
 
+confirm_delete_selected=Potvrdit odstranění všech vybraných položek?
 
 name=Název
 value=Hodnota
@@ -153,7 +162,7 @@ buttons.code.tooltip=Přidat kód
 buttons.link.tooltip=Přidat odkaz
 buttons.list.unordered.tooltip=Přidat seznam odrážek
 buttons.list.ordered.tooltip=Přidat číslovaný seznam
-buttons.list.task.tooltip=Přidat seznam úkolů
+buttons.list.task.tooltip=Přidat seznam úloh
 buttons.mention.tooltip=Uveďte uživatele nebo tým
 buttons.ref.tooltip=Odkaz na issue nebo pull request
 buttons.switch_to_legacy.tooltip=Místo toho použít starší editor
@@ -166,6 +175,7 @@ string.desc=Z – A
 
 [error]
 occurred=Došlo k chybě
+report_message=Pokud jste si jisti, že se jedná o chybu Gitea, prosím vyhledejte problém na <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> a v případě potřeby založte nový problém.
 missing_csrf=Špatný požadavek: Neexistuje CSRF token
 invalid_csrf=Špatný požadavek: Neplatný CSRF token
 not_found=Cíl nebyl nalezen.
@@ -174,6 +184,7 @@ network_error=Chyba sítě
 [startpage]
 app_desc=Snadno přístupný vlastní Git
 install=Jednoduchá na instalaci
+install_desc=Jednoduše <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">spusťte jako binární program</a> pro vaši platformu, nasaďte jej pomocí <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, nebo jej stáhněte jako <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">balíček</a>.
 platform=Multiplatformní
 platform_desc=Gitea běží všude, kde <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> může kompilovat: Windows, macOS, Linux, ARM, atd. Vyberte si ten, který milujete!
 lightweight=Lehká
@@ -218,6 +229,7 @@ repo_path_helper=Všechny vzdálené repozitáře Gitu budou uloženy do tohoto
 lfs_path=Kořenový adresář Git LFS
 lfs_path_helper=V tomto adresáři budou uloženy soubory, které jsou sledovány Git LFS. Pokud ponecháte prázdné, LFS zakážete.
 run_user=Spustit jako uživatel
+run_user_helper=Zadejte uživatelské jméno, pod kterým Gitea běží v operačním systému. Pozor: tento uživatel musí mít přístup ke kořenovému adresáři repozitářů.
 domain=Doména serveru
 domain_helper=Adresa domény, nebo hostitele serveru.
 ssh_port=Port SSH serveru
@@ -267,7 +279,7 @@ install_btn_confirm=Nainstalovat Gitea
 test_git_failed=Chyba při testu příkazu 'git': %v
 sqlite3_not_available=Tato verze Gitea nepodporuje SQLite3. Stáhněte si oficiální binární verzi od %s (nikoli verzi „gobuild“).
 invalid_db_setting=Nastavení databáze je neplatné: %v
-invalid_db_table=Databázová tabulka "%s" je neplatná: %v
+invalid_db_table=Databázová tabulka „%s“ je neplatná: %v
 invalid_repo_path=Kořenový adresář repozitářů není správný: %v
 invalid_app_data_path=Cesta k datům aplikace je neplatná: %v
 run_user_not_match=`"Run as" uživatelské jméno není aktuální uživatelské jméno: %s -> %s`
@@ -289,6 +301,8 @@ invalid_password_algorithm=Neplatný algoritmus hash hesla
 password_algorithm_helper=Nastavte algoritmus hashování hesla. Algoritmy mají odlišné požadavky a sílu. Algoritmus argon2 je poměrně bezpečný, ale používá spoustu paměti a může být nevhodný pro malé systémy.
 enable_update_checker=Povolit kontrolu aktualizací
 enable_update_checker_helper=Kontroluje vydání nových verzí pravidelně připojením ke gitea.io.
+env_config_keys=Konfigurace prostředí
+env_config_keys_prompt=Následující proměnné prostředí budou také použity pro váš konfigurační soubor:
 
 [home]
 uname_holder=Uživatelské jméno nebo e-mailová adresa
@@ -334,7 +348,7 @@ repo_no_results=Nebyly nalezeny žádné odpovídající repozitáře.
 user_no_results=Nebyly nalezeni žádní odpovídající uživatelé.
 org_no_results=Nebyly nalezeny žádné odpovídající organizace.
 code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu.
-code_search_results=`Výsledky hledání pro "%s"`
+code_search_results=Výsledky hledání pro „%s“
 code_last_indexed_at=Naposledy indexováno %s
 relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty.
 relevant_repositories=Zobrazují se pouze relevantní repositáře, <a href="%s">zobrazit nefiltrované výsledky</a>.
@@ -347,9 +361,11 @@ disable_register_prompt=Registrace jsou vypnuty. Prosíme, kontaktujte správce
 disable_register_mail=E-mailové potvrzení o registraci je zakázané.
 manual_activation_only=Pro dokončení aktivace kontaktujte správce webu.
 remember_me=Pamatovat si toto zařízení
+remember_me.compromised=Přihlašovací token již není platný, což může znamenat napadení účtu. Zkontrolujte prosím svůj účet pro neobvyklé aktivity.
 forgot_password_title=Zapomenuté heslo
 forgot_password=Zapomenuté heslo?
 sign_up_now=Potřebujete účet? Zaregistrujte se.
+sign_up_successful=Účet byl úspěšně vytvořen. Vítejte!
 confirmation_mail_sent_prompt=Na adresu <b>%s</b> byl zaslán nový potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces registrace.
 must_change_password=Aktualizujte své heslo
 allow_password_change=Vyžádat od uživatele změnu hesla (doporučeno)
@@ -357,6 +373,7 @@ reset_password_mail_sent_prompt=Na adresu <b>%s</b> byl zaslán potvrzovací e-m
 active_your_account=Aktivujte si váš účet
 account_activated=Účet byl aktivován
 prohibit_login=Přihlášení zakázáno
+prohibit_login_desc=Vašemu účtu je zakázáno se přihlásit, kontaktujte prosím správce webu.
 resent_limit_prompt=Omlouváme se, ale před chvílí jste požádal o zaslání aktivačního e-mailu. Počkejte prosím 3 minuty a pak to zkuste znovu.
 has_unconfirmed_mail=Zdravím, %s, máte nepotvrzenou e-mailovou adresu (<b>%s</b>). Pokud jste nedostali e-mail pro potvrzení nebo potřebujete zaslat nový, klikněte prosím na tlačítku níže.
 resend_mail=Klikněte zde pro odeslání aktivačního e-mailu
@@ -364,8 +381,10 @@ email_not_associate=Tato e-mailová adresa není spojena s žádným účtem.
 send_reset_mail=Zaslat e-mail pro obnovení účtu
 reset_password=Obnovení účtu
 invalid_code=Tento potvrzující kód je neplatný nebo mu vypršela platnost.
+invalid_code_forgot_password=Váš potvrzovací kód je neplatný nebo mu vypršela platnost. <a href="%s">Klikněte zde</a> pro vytvoření nového kódu.
 invalid_password=Vaše heslo se neshoduje s heslem, které bylo použito k vytvoření účtu.
 reset_password_helper=Obnovit účet
+reset_password_wrong_user=Jste přihlášen/a jako %s, ale odkaz pro obnovení účtu je pro %s
 password_too_short=Délka hesla musí být minimálně %d znaků.
 non_local_account=Externě ověřovaní uživatelé nemohou aktualizovat své heslo prostřednictvím webového rozhraní Gitea.
 verify=Ověřit
@@ -390,6 +409,7 @@ openid_connect_title=Připojení k existujícímu účtu
 openid_connect_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde.
 openid_register_title=Vytvořit nový účet
 openid_register_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde.
+openid_signin_desc=Zadejte vaši OpenID URI. Například: alice.openid.example.org nebo https://openid.example.org/alice.
 disable_forgot_password_mail=Obnovení účtu je zakázáno, protože není nastaven žádný e-mail. Obraťte se na správce webu.
 disable_forgot_password_mail_admin=Obnovení účtu je dostupné pouze po nastavení e-mailu. Pro povolení obnovy účtu nastavte prosím e-mail.
 email_domain_blacklisted=Nemůžete se registrovat s vaší e-mailovou adresou.
@@ -399,7 +419,9 @@ authorize_application_created_by=Tuto aplikaci vytvořil %s.
 authorize_application_description=Pokud povolíte přístup, bude moci přistupovat a zapisovat do všech vašich informací o účtu včetně soukromých repozitářů a organizací.
 authorize_title=Autorizovat „%s“ pro přístup k vašemu účtu?
 authorization_failed=Autorizace selhala
+authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný požadavek. Kontaktujte prosím správce aplikace, kterou jste se pokoušeli autorizovat.
 sspi_auth_failed=SSPI autentizace selhala
+password_pwned=Heslo, které jste zvolili, je na <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">seznamu odcizených hesel</a>, která byla dříve odhalena při narušení veřejných dat. Zkuste to prosím znovu s jiným heslem.
 password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned
 
 [mail]
@@ -414,6 +436,7 @@ activate_account.text_1=Ahoj <b>%[1]s</b>, děkujeme za registraci na %[2]s!
 activate_account.text_2=Pro aktivaci vašeho účtu do <b>%s</b> klikněte na následující odkaz:
 
 activate_email=Ověřte vaši e-mailovou adresu
+activate_email.title=%s, prosím ověřte vaši e-mailovou adresu
 activate_email.text=Pro aktivaci vašeho účtu do <b>%s</b> klikněte na následující odkaz:
 
 register_notify=Vítejte v Gitea
@@ -509,6 +532,7 @@ url_error=`„%s“ není platná adresa URL.`
 include_error=` musí obsahovat substring „%s“.`
 glob_pattern_error=`zástupný vzor je neplatný: %s.`
 regex_pattern_error=` regex vzor je neplatný: %s.`
+username_error=` může obsahovat pouze alfanumerické znaky („0-9“, „a-z“, „A-Z“), pomlčku („-“), podtržítka („_“) a tečka („.“). Nemůže začínat nebo končit nealfanumerickými znaky a po sobě jdoucí nealfanumerické znaky jsou také zakázány.`
 invalid_group_team_map_error=` mapování je neplatné: %s`
 unknown_error=Neznámá chyba:
 captcha_incorrect=CAPTCHA kód není správný.
@@ -553,13 +577,20 @@ invalid_ssh_key=Nelze ověřit váš SSH klíč: %s
 invalid_gpg_key=Nelze ověřit váš GPG klíč: %s
 invalid_ssh_principal=Neplatný SSH Principal certifikát: %s
 must_use_public_key=Zadaný klíč je soukromý klíč. Nenahrávejte svůj soukromý klíč nikde. Místo toho použijte váš veřejný klíč.
+unable_verify_ssh_key=Nelze ověřit váš SSH klíč.
 auth_failed=Ověření selhalo: %v
 
+still_own_repo=Váš účet vlastní jeden nebo více repozitářů. Nejprve je smažte nebo převeďte.
+still_has_org=Váš účet je členem jedné nebo více organizací. Nejdříve je musíte opustit.
+still_own_packages=Váš účet vlastní jeden nebo více balíčků. Nejprve je musíte odstranit.
+org_still_own_repo=Organizace stále vlastní jeden nebo více repozitářů. Nejdříve je smažte nebo převeďte.
+org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. Nejdříve je smažte.
 
 target_branch_not_exist=Cílová větev neexistuje.
 
 [user]
 change_avatar=Změnit váš avatar…
+joined_on=Přidal/a se %s
 repositories=Repozitáře
 activity=Veřejná aktivita
 followers=Sledující
@@ -575,10 +606,12 @@ user_bio=Životopis
 disabled_public_activity=Tento uživatel zakázal veřejnou viditelnost aktivity.
 email_visibility.limited=Vaše e-mailová adresa je viditelná pro všechny ověřené uživatele
 email_visibility.private=Vaše e-mailová adresa je viditelná pouze pro vás a administrátory
+show_on_map=Zobrazit toto místo na mapě
+settings=Uživatelská nastavení
 
-form.name_reserved=Uživatelské jméno "%s" je rezervováno.
-form.name_pattern_not_allowed=Vzor "%s" není povolen v uživatelském jméně.
-form.name_chars_not_allowed=Uživatelské jméno "%s" obsahuje neplatné znaky.
+form.name_reserved=Uživatelské jméno „%s“ je rezervováno.
+form.name_pattern_not_allowed=Vzor „%s“ není povolen v uživatelském jméně.
+form.name_chars_not_allowed=Uživatelské jméno „%s“ obsahuje neplatné znaky.
 
 [settings]
 profile=Profil
@@ -596,9 +629,13 @@ delete=Smazat účet
 twofa=Dvoufaktorové ověřování
 account_link=Propojené účty
 organization=Organizace
+uid=UID
 webauthn=Bezpečnostní klíče
 
 public_profile=Veřejný profil
+biography_placeholder=Řekněte nám něco o sobě! (Můžete použít Markdown)
+location_placeholder=Sdílejte svou přibližnou polohu s ostatními
+profile_desc=Nastavte, jak bude váš profil zobrazen ostatním uživatelům. Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla a operace Git.
 password_username_disabled=Externí uživatelé nemohou měnit svoje uživatelské jméno. Kontaktujte prosím svého administrátora pro více detailů.
 full_name=Celé jméno
 website=Web
@@ -606,15 +643,20 @@ location=Místo
 update_theme=Aktualizovat motiv vzhledu
 update_profile=Aktualizovat profil
 update_language=Aktualizovat jazyk
-update_language_not_found=Jazyk "%s" není k dispozici.
+update_language_not_found=Jazyk „%s“ není k dispozici.
 update_language_success=Jazyk byl aktualizován.
 update_profile_success=Váš profil byl aktualizován.
 change_username=Vaše uživatelské jméno bylo změněno.
+change_username_prompt=Poznámka: Změna uživatelského jména také změní URL vašeho účtu.
+change_username_redirect_prompt=Staré uživatelské jméno bude přesměrováváno, dokud nebude znovu obsazeno.
 continue=Pokračovat
 cancel=Zrušit
 language=Jazyk
 ui=Motiv vzhledu
 hidden_comment_types=Skryté typy komentářů
+hidden_comment_types_description=Zde zkontrolované typy komentářů nebudou zobrazeny na stránkách problémů. Zaškrtnutí „Štítek“ například odstraní všechny komentáře „<user> přidal/odstranil <label>“.
+hidden_comment_types.ref_tooltip=Komentáře, na které se odkazovalo z jiného úkolu/commitu/…
+hidden_comment_types.issue_ref_tooltip=Komentáře, kde uživatel změní větev/značku spojenou s problémem
 comment_type_group_reference=Reference
 comment_type_group_label=Štítek
 comment_type_group_milestone=Milník
@@ -631,6 +673,7 @@ comment_type_group_project=Projekt
 comment_type_group_issue_ref=Referenční číslo úkolu
 saved_successfully=Vaše nastavení bylo úspěšně uloženo.
 privacy=Soukromí
+keep_activity_private=Skrýt aktivitu z profilové stránky
 keep_activity_private_popup=Učinit aktivitu viditelnou pouze pro vás a administrátory
 
 lookup_avatar_by_mail=Vyhledat avatar pomocí e-mailové adresy
@@ -640,12 +683,14 @@ choose_new_avatar=Vybrat nový avatar
 update_avatar=Aktualizovat avatar
 delete_current_avatar=Smazat aktuální avatar
 uploaded_avatar_not_a_image=Nahraný soubor není obrázek.
+uploaded_avatar_is_too_big=Nahraný soubor (%d KiB) přesahuje maximální velikost (%d KiB).
 update_avatar_success=Vaše avatar byl aktualizován.
 update_user_avatar_success=Uživatelův avatar byl aktualizován.
 
 change_password=Aktualizovat heslo
 old_password=Stávající heslo
 new_password=Nové heslo
+retype_new_password=Potvrdit nové heslo
 password_incorrect=Zadané heslo není správné.
 change_password_success=Vaše heslo bylo aktualizováno. Od teď se přihlašujte novým heslem.
 password_change_disabled=Externě ověřovaní uživatelé nemohou aktualizovat své heslo prostřednictvím webového rozhraní Gitea.
@@ -654,6 +699,7 @@ emails=E-mailová adresa
 manage_emails=Správa e-mailových adres
 manage_themes=Vyberte výchozí motiv vzhledu
 manage_openid=Správa OpenID adres
+email_desc=Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla, a pokud není skrytá, pro operace Gitu.
 theme_desc=Toto bude váš výchozí motiv vzhledu napříč stránkou.
 primary=Hlavní
 activated=Aktivován
@@ -661,6 +707,7 @@ requires_activation=Vyžaduje aktivaci
 primary_email=Nastavit jako hlavní
 activate_email=Odeslat aktivaci
 activations_pending=Čekající aktivace
+can_not_add_email_activations_pending=Existuje čekající aktivace, zkuste to znovu za pár minut, pokud chcete přidat nový e-mail.
 delete_email=Smazat
 email_deletion=Odstranit e-mailovou adresu
 email_deletion_desc=E-mailová adresa a přidružené informace budou z vašeho účtu odstraněny. Commity Gitu s touto e-mailovou adresou zůstanou nezměněny. Pokračovat?
@@ -674,10 +721,12 @@ add_new_email=Přidat novou e-mailovou adresu
 add_new_openid=Přidat novou OpenID URI
 add_email=Přidat e-mailovou adresu
 add_openid=Přidat OpenID URI
+add_email_confirmation_sent=Potvrzovací e-mail byl odeslán na „%s“. Prosím zkontrolujte příchozí poštu během následujících %s pro potvrzení vaší e-mailové adresy.
 add_email_success=Nová e-mailová adresa byla přidána.
 email_preference_set_success=Nastavení e-mailu bylo úspěšně nastaveno.
 add_openid_success=Nová OpenID adresa byla přidána.
 keep_email_private=Schovat e-mailovou adresu
+keep_email_private_popup=Toto skryje vaši e-mailovou adresu z vašeho profilu, stejně jako při vytvoření pull requestu nebo úpravě souboru pomocí webového rozhraní. Odeslané commity nebudou změněny. Použijte %s v commitech pro jejich přiřazení k vašemu účtu.
 openid_desc=OpenID vám umožní delegovat ověřování na externího poskytovatele.
 
 manage_ssh_keys=Správa klíčů SSH
@@ -711,7 +760,7 @@ gpg_token_help=Podpis můžete vygenerovat pomocí:
 gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Zakódovaný podpis GPG
 key_signature_gpg_placeholder=Začíná s „-----BEGIN PGP SIGNATURE-----“
-verify_gpg_key_success=GPG klíč "%s" byl ověřen.
+verify_gpg_key_success=GPG klíč „%s“ byl ověřen.
 ssh_key_verified=Ověřený klíč
 ssh_key_verified_long=Klíč byl ověřen pomocí tokenu a může být použit k ověření commitů shodujících se s libovolnou vaší aktivovanou e-mailovou adresou pro tohoto uživatele.
 ssh_key_verify=Ověřit
@@ -721,14 +770,15 @@ ssh_token=Token
 ssh_token_help=Podpis můžete vygenerovat pomocí:
 ssh_token_signature=Zakódovaný podpis SSH
 key_signature_ssh_placeholder=Začíná s „-----BEGIN SSH SIGNATURE-----“
-verify_ssh_key_success=SSH klíč "%s" byl ověřen.
+verify_ssh_key_success=SSH klíč „%s“ byl ověřen.
 subkeys=Podklíče
 key_id=ID klíče
 key_name=Název klíče
 key_content=Obsah
 principal_content=Obsah
-add_key_success=SSH klíč "%s" byl přidán.
-add_gpg_key_success=GPG klíč "%s" byl přidán.
+add_key_success=SSH klíč „%s“ byl přidán.
+add_gpg_key_success=GPG klíč „%s“ byl přidán.
+add_principal_success=Byl přidán SSH Principal certifikát „%s“.
 delete_key=Odstranit
 ssh_key_deletion=Odstraňte SSH klíč
 gpg_key_deletion=Odstraňte GPG klíč
@@ -755,7 +805,9 @@ ssh_disabled=SSH zakázáno
 ssh_signonly=SSH je v současné době zakázáno, proto jsou tyto klíče použity pouze pro ověření podpisu.
 ssh_externally_managed=Tento SSH klíč je spravován externě pro tohoto uživatele
 manage_social=Správa propojených účtů sociálních sítí
+social_desc=Tyto účty sociálních sítí lze použít k přihlášení k vašemu účtu. Ujistěte se, že jsou všechny vaše.
 unbind=Odpojit
+unbind_success=Účet sociální sítě byl úspěšně odstraněn.
 
 manage_access_token=Spravovat přístupové tokeny
 generate_new_token=Vygenerovat nový token
@@ -776,6 +828,7 @@ permissions_access_all=Vše (veřejné, soukromé a omezené)
 select_permissions=Vyberte oprávnění
 permission_no_access=Bez přístupu
 permission_read=Přečtené
+permission_write=čtení i zápis
 at_least_one_permission=Musíte vybrat alespoň jedno oprávnění pro vytvoření tokenu
 permissions_list=Oprávnění:
 
@@ -787,6 +840,8 @@ remove_oauth2_application_desc=Odstraněním OAuth2 aplikace odeberete přístup
 remove_oauth2_application_success=Aplikace byla odstraněna.
 create_oauth2_application=Vytvořit novou OAuth2 aplikaci
 create_oauth2_application_button=Vytvořit aplikaci
+create_oauth2_application_success=Úspěšně jste vytvořili novou OAuth2 aplikaci.
+update_oauth2_application_success=Úspěšně jste aktualizovali OAuth2 aplikaci.
 oauth2_application_name=Název aplikace
 oauth2_confidential_client=Důvěrný klient. Vyberte aplikace, které zachovávají důvěrnosti v utajení, jako jsou webové aplikace. Nevybírejte pro nativní aplikace včetně stolních a mobilních aplikací.
 oauth2_redirect_uris=Přesměrování URI. Použijte nový řádek pro každou URI.
@@ -795,19 +850,26 @@ oauth2_client_id=ID klienta
 oauth2_client_secret=Tajný klíč klienta
 oauth2_regenerate_secret=Obnovit tajný klíč
 oauth2_regenerate_secret_hint=Ztratili jste svůj tajný klíč?
+oauth2_client_secret_hint=Tajný klíč se znovu nezobrazí po opuštění nebo obnovení této stránky. Ujistěte se, že jste si jej uložili.
 oauth2_application_edit=Upravit
 oauth2_application_create_description=OAuth2 aplikace poskytuje přístup aplikacím třetích stran k uživatelským účtům na této instanci.
+oauth2_application_remove_description=Odebráním OAuth2 aplikace zabrání přístupu ověřeným uživatelům na této instanci. Pokračovat?
+oauth2_application_locked=Gitea předregistruje některé OAuth2 aplikace při spuštění, pokud je to povoleno v konfiguraci. Aby se zabránilo neočekávanému chování, nelze je upravovat ani odstranit. Více informací naleznete v dokumentaci OAuth2.
 
 authorized_oauth2_applications=Autorizovat OAuth2 aplikaci
+authorized_oauth2_applications_description=Úspěšně jste povolili přístup k vašemu osobnímu účtu této aplikaci třetí strany. Zrušte prosím přístup aplikacím, které již nadále nepotřebujete.
 revoke_key=Zrušit
 revoke_oauth2_grant=Zrušit přístup
 revoke_oauth2_grant_description=Zrušením přístupu této aplikaci třetí strany ji zabráníte v přístupu k vašim datům. Jste si jisti?
+revoke_oauth2_grant_success=Přístup byl úspěšně zrušen.
 
 twofa_desc=Dvoufaktorový způsob ověřování zvýší zabezpečení vašeho účtu.
+twofa_recovery_tip=Pokud ztratíte své zařízení, budete moci použít jednorázový obnovovací klíč k získání přístupu k vašemu účtu.
 twofa_is_enrolled=Váš účet aktuálně <strong>používá</strong> dvoufaktorové ověřování.
 twofa_not_enrolled=Váš účet aktuálně nepoužívá dvoufaktorové ověřování.
 twofa_disable=Zakázat dvoufaktorové ověřování
 twofa_scratch_token_regenerate=Obnovit pomocný token
+twofa_scratch_token_regenerated=Váš jednorázový obnovovací klíč je nyní %s. Uložte jej na bezpečném místě, protože se znovu nezobrazí.
 twofa_enroll=Povolit dvoufaktorové ověřování
 twofa_disable_note=Dvoufaktorové ověřování můžete zakázat, když bude potřeba.
 twofa_disable_desc=Zakážete-li dvoufaktorové ověřování, bude váš účet méně zabezpečený. Pokračovat?
@@ -825,6 +887,8 @@ webauthn_register_key=Přidat bezpečnostní klíč
 webauthn_nickname=Přezdívka
 webauthn_delete_key=Odstranit bezpečnostní klíč
 webauthn_delete_key_desc=Pokud odstraníte bezpečnostní klíč, již se s ním nebudete moci přihlásit. Pokračovat?
+webauthn_key_loss_warning=Pokud ztratíte své bezpečnostní klíče, ztratíte přístup k vašemu účtu.
+webauthn_alternative_tip=Možná budete chtít nakonfigurovat další metodu ověřování.
 
 manage_account_links=Správa propojených účtů
 manage_account_links_desc=Tyto externí účty jsou propojeny s vaším Gitea účtem.
@@ -834,8 +898,10 @@ remove_account_link=Odstranit propojený účet
 remove_account_link_desc=Odstraněním propojeného účtu zrušíte jeho přístup k vašemu Gitea účtu. Pokračovat?
 remove_account_link_success=Propojený účet byl odstraněn.
 
+hooks.desc=Přidat webhooky, které budou spouštěny pro <strong>všechny repozitáře</strong> vve vašem vlastnictví.
 
 orgs_none=Nejste členem žádné organizace.
+repos_none=Nevlastníte žádné repozitáře.
 
 delete_account=Smazat váš účet
 delete_prompt=Tato operace natrvalo odstraní váš uživatelský účet. <strong>NELZE</strong> ji vrátit zpět.
@@ -854,9 +920,12 @@ visibility=Viditelnost uživatele
 visibility.public=Veřejný
 visibility.public_tooltip=Viditelné pro všechny
 visibility.limited=Omezený
+visibility.limited_tooltip=Viditelné pouze pro ověřené uživatele
 visibility.private=Soukromý
+visibility.private_tooltip=Viditelné pouze pro členy organizací, ke kterým jste se připojili
 
 [repo]
+new_repo_helper=Repozitář obsahuje všechny projektové soubory, včetně historie revizí. Už jej hostujete jinde? <a href="%s">Migrovat repozitář.</a>
 owner=Vlastník
 owner_helper=Některé organizace se nemusejí v seznamu zobrazit kvůli maximálnímu dosaženému počtu repozitářů.
 repo_name=Název repozitáře
@@ -868,6 +937,7 @@ template_helper=Z repozitáře vytvořit šablonu
 template_description=Šablony repozitářů umožňují uživatelům generovat nové repositáře se stejnou strukturou, soubory a volitelnými nastaveními.
 visibility=Viditelnost
 visibility_description=Pouze majitelé nebo členové organizace to budou moci vidět, pokud mají práva.
+visibility_helper=Nastavit repozitář jako soukromý
 visibility_helper_forced=Váš administrátor vynutil, že nové repozitáře budou soukromé.
 visibility_fork_helper=(Změna tohoto ovlivní všechny rozštěpení repozitáře.)
 clone_helper=Potřebujete pomoci s klonováním? Navštivte <a target="_blank" rel="noopener noreferrer" href="%s">nápovědu</a>.
@@ -876,6 +946,9 @@ fork_from=Rozštěpit z
 already_forked=Již jsi rozštěpil %s
 fork_to_different_account=Rozštěpit na jiný účet
 fork_visibility_helper=Viditelnost rozštěpeného repozitáře nemůže být změněna.
+fork_branch=Větev, která má být klonována pro fork
+all_branches=Všechny větve
+fork_no_valid_owners=Tento repozitář nemůže být rozštěpen, protože neexistují žádní platní vlastníci.
 use_template=Použít tuto šablonu
 clone_in_vsc=Klonovat ve VS Code
 download_zip=Stáhnout ZIP
@@ -904,6 +977,7 @@ trust_model_helper_collaborator_committer=Spolupracovník+Přispěvatel: Důvě
 trust_model_helper_default=Výchozí: Použít výchozí model důvěry pro tuto instalaci
 create_repo=Vytvořit repozitář
 default_branch=Výchozí větev
+default_branch_label=výchozí
 default_branch_helper=Výchozí větev je základní větev pro požadavky na natažení a commity kódu.
 mirror_prune=Vyčistit
 mirror_prune_desc=Odstranit zastaralé reference na vzdálené sledování
@@ -912,6 +986,8 @@ mirror_interval_invalid=Interval zrcadlení není platný.
 mirror_sync_on_commit=Synchronizovat při nahrávání revizí
 mirror_address=Klonovat z URL
 mirror_address_desc=Zadejte požadované přístupové údaje do sekce Ověření.
+mirror_address_url_invalid=Poskytnutá URL je neplatná. Všechny části musíte správně nahradit escape sekvencí.
+mirror_address_protocol_invalid=Zadaná URL je neplatná. Mohou být zrcadleny pouze umístění http(s):// nebo git://.
 mirror_lfs=Úložiště velkých souborů (LFS)
 mirror_lfs_desc=Aktivovat zrcadlení dat LFS.
 mirror_lfs_endpoint=Koncový bod LFS
@@ -937,13 +1013,19 @@ delete_preexisting=Odstranit již existující soubory
 delete_preexisting_content=Odstranit soubory v %s
 delete_preexisting_success=Smazány nepřijaté soubory v %s
 blame_prior=Zobrazit blame před touto změnou
+blame.ignore_revs.failed=Nepodařilo se ignorovat revize v <a href="%s">.git-blame-ignore-revs</a>.
 author_search_tooltip=Zobrazí maximálně 30 uživatelů
 
+tree_path_not_found_commit=Cesta %[1]s v commitu %[2]s neexistuje
+tree_path_not_found_branch=Cesta %[1]s ve větvi %[2]s neexistuje
+tree_path_not_found_tag=Cesta %[1]s ve značce %[2]s neexistuje
 
 transfer.accept=Přijmout převod
 transfer.accept_desc=Převést do „%s“
 transfer.reject=Odmítnout převod
 transfer.reject_desc=Zrušit převod do „%s“
+transfer.no_permission_to_accept=Nemáte oprávnění k přijetí tohoto převodu.
+transfer.no_permission_to_reject=Nemáte oprávnění k odmítnutí tohoto převodu.
 
 desc.private=Soukromý
 desc.public=Veřejný
@@ -962,12 +1044,15 @@ template.issue_labels=Štítky úkolů
 template.one_item=Musíte vybrat alespoň jednu položku šablony
 template.invalid=Musíte vybrat repositář šablony
 
+archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo požadavky na natažení.
+archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo požadavky na natažení.
 archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly.
 archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat požadavky na natažení.
 
 form.reach_limit_of_creation_1=Již jste dosáhli svůj limit %d repozitář.
 form.reach_limit_of_creation_n=Již jste dosáhli svůj limit %d repozitářů.
 form.name_reserved=Název repozitáře „%s“ je rezervován.
+form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu repozitáře.
 
 need_auth=Ověření
 migrate_options=Možnosti migrace
@@ -977,6 +1062,7 @@ migrate_options_lfs=Migrovat LFS soubory
 migrate_options_lfs_endpoint.label=Koncový bod LFS
 migrate_options_lfs_endpoint.description=Migrace se pokusí použít váš vzdálený Git pro <a target="_blank" rel="noopener noreferrer" href="%s">určení LFS serveru</a>. Můžete také zadat vlastní koncový bod, pokud jsou data LFS repozitáře uložena někde jinde.
 migrate_options_lfs_endpoint.description.local=Podporována je také cesta k lokálnímu serveru.
+migrate_options_lfs_endpoint.placeholder=Ponecháte-li prázdné, koncový bod bude odvozen z adresy URL klonu
 migrate_items=Položky pro migrování
 migrate_items_wiki=Wiki
 migrate_items_milestones=Milníky
@@ -992,6 +1078,7 @@ migrate.github_token_desc=Můžete sem vložit jeden nebo více tokenů oddělen
 migrate.clone_local_path=nebo místní cesta serveru
 migrate.permission_denied=Není dovoleno importovat místní repozitáře.
 migrate.permission_denied_blocked=Nelze importovat z nepovolených hostitelů, prosím požádejte správce, aby zkontroloval nastavení ALLOWED_DOMAINS/ALLOW_LOCALETWORKS/BLOCKED_DOMAINS.
+migrate.invalid_local_path=Místní cesta je neplatná, buď neexistuje nebo není adresářem.
 migrate.invalid_lfs_endpoint=Koncový bod LFS není platný.
 migrate.failed=Přenesení selhalo: %v
 migrate.migrate_items_options=Pro migraci dalších položek je vyžadován přístupový token
@@ -1069,6 +1156,7 @@ release=Vydání
 releases=Vydání
 tag=Značka
 released_this=vydal/a toto
+tagged_this=označil/a
 file.title=%s v %s
 file_raw=Surový
 file_history=Historie
@@ -1077,6 +1165,10 @@ file_view_rendered=Zobrazit vykreslené
 file_view_raw=Zobrazit v surovém stavu
 file_permalink=Trvalý odkaz
 file_too_large=Soubor je příliš velký pro zobrazení.
+invisible_runes_header=`Tento soubor obsahuje neviditelné znaky Unicode`
+invisible_runes_description=`Tento soubor obsahuje neviditelné Unicode znaky, které jsou pro člověka nerozeznatelné, ale mohou být zpracovány jiným způsobem. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.`
+ambiguous_runes_header=`Tento soubor obsahuje nejednoznačné znaky Unicode`
+ambiguous_runes_description=`Tento soubor obsahuje znaky Unicode, které mohou být zaměněny s jinými znaky. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.`
 invisible_runes_line=`Tento řádek má neviditelné znaky Unicode`
 ambiguous_runes_line=`Tento řádek má nejednoznačné znaky Unicode`
 ambiguous_character=`%[1]c [U+%04[1]X] je zaměnitelný s %[2]c [U+%04[2]X]`
@@ -1089,11 +1181,15 @@ video_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5
 audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5 audio.
 stored_lfs=Uloženo pomocí Git LFS
 symbolic_link=Symbolický odkaz
+executable_file=Spustitelný soubor
 commit_graph=Graf commitů
 commit_graph.select=Vybrat větve
 commit_graph.hide_pr_refs=Skrýt požadavky na natažení
 commit_graph.monochrome=Černobílé
 commit_graph.color=Barva
+commit.contained_in=Tento commit je obsažen v:
+commit.contained_in_default_branch=Tento commit je součástí výchozí větve
+commit.load_referencing_branches_and_tags=Načíst větve a značky odkazující na tento commit
 blame=Blame
 download_file=Stáhnout soubor
 normal_view=Normální zobrazení
@@ -1127,6 +1223,7 @@ editor.update=Aktualizovat %s
 editor.delete=Odstranit %s
 editor.patch=Použít záplatu
 editor.patching=Záplatování:
+editor.fail_to_apply_patch=Nelze použít záplatu „%s“
 editor.new_patch=Nová záplata
 editor.commit_message_desc=Přidat volitelný rozšířený popis…
 editor.signoff_desc=Přidat Signed-off-by podpis přispěvatele na konec zprávy o commitu.
@@ -1141,7 +1238,13 @@ editor.filename_cannot_be_empty=Jméno nemůže být prázdné.
 editor.filename_is_invalid=Název souboru je neplatný: „%s“.
 editor.branch_does_not_exist=Větev „%s“ v tomto repozitáři neexistuje.
 editor.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři.
+editor.directory_is_a_file=Jméno adresáře „%s“ je již použito jako jméno souboru v tomto repozitáři.
+editor.file_is_a_symlink=`„%s“ je symbolický odkaz. Symbolické odkazy nemohou být upravovány ve webovém editoru`
+editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři.
+editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již není součástí tohoto repozitáře.
+editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není součástí tohoto repozitáře.
 editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte zde</a>, abyste je zobrazili, nebo <strong>potvrďte změny ještě jednou</strong> pro jejich přepsání.
+editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři.
 editor.commit_empty_file_header=Odevzdat prázdný soubor
 editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat?
 editor.no_changes_to_show=Žádné změny k zobrazení.
@@ -1163,8 +1266,10 @@ editor.revert=Vrátit %s na:
 
 commits.desc=Procházet historii změn zdrojového kódu.
 commits.commits=Commity
+commits.no_commits=Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii.
 commits.nothing_to_compare=Tyto větve jsou stejné.
 commits.search=Hledání commitů…
+commits.search.tooltip=Můžete předřadit klíčová slova s „author:“, „committer:“, „after:“ nebo „before:“, např. „revert author:Alice before:2019-01-03“.
 commits.find=Vyhledat
 commits.search_all=Všechny větve
 commits.author=Autor
@@ -1177,6 +1282,7 @@ commits.signed_by_untrusted_user=Podepsáno nedůvěryhodným uživatelem
 commits.signed_by_untrusted_user_unmatched=Podepsáno nedůvěryhodným uživatelem, který nesouhlasí s přispěvatelem
 commits.gpg_key_id=ID GPG klíče
 commits.ssh_key_fingerprint=Otisk klíče SSH
+commits.view_path=Zobrazit v tomto bodě v historii
 
 commit.operations=Operace
 commit.revert=Vrátit
@@ -1202,12 +1308,14 @@ projects.create=Vytvořit projekt
 projects.title=Název
 projects.new=Nový projekt
 projects.new_subheader=Koordinujte, sledujte a aktualizujte svou práci na jednom místě, aby projekty zůstaly transparentní a v plánu.
+projects.create_success=Projekt „%s“ byl vytvořen.
 projects.deletion=Odstranit projekt
 projects.deletion_desc=Odstranění projektu jej odstraní ze všech souvisejících úkolů. Pokračovat?
 projects.deletion_success=Projekt byl odstraněn.
 projects.edit=Upravit projekty
 projects.edit_subheader=Projekty organizují úkoly a sledují pokrok.
 projects.modify=Aktualizovat projekt
+projects.edit_success=Projekt „%s“ byl aktualizován.
 projects.type.none=Žádný
 projects.type.basic_kanban=Základní Kanban
 projects.type.bug_triage=Třídění chyb
@@ -1233,7 +1341,7 @@ projects.card_type.desc=Náhledy karet
 projects.card_type.images_and_text=Obrázky a text
 projects.card_type.text_only=Pouze text
 
-issues.desc=Organizování hlášení chyb, úkolů a milníků.
+issues.desc=Organizování hlášení chyb, úloh a milníků.
 issues.filter_assignees=Filtrovat zpracovatele
 issues.filter_milestones=Filtrovat milník
 issues.filter_projects=Filtrovat projekt
@@ -1265,6 +1373,7 @@ issues.choose.blank=Výchozí
 issues.choose.blank_about=Vytvořit úkol z výchozí šablony.
 issues.choose.ignore_invalid_templates=Neplatné šablony byly ignorovány
 issues.choose.invalid_templates=%v nalezených neplatných šablon
+issues.choose.invalid_config=Nastavení problému obsahuje chyby:
 issues.no_ref=Není určena žádná větev/značka
 issues.create=Vytvořit úkol
 issues.new_label=Nový štítek
@@ -1301,6 +1410,7 @@ issues.delete_branch_at=`odstranil/a větev <b>%s</b> %s`
 issues.filter_label=Štítek
 issues.filter_label_exclude=`Chcete-li vyloučit štítky, použijte <code>alt</code> + <code>click/enter</code>`
 issues.filter_label_no_select=Všechny štítky
+issues.filter_label_select_no_label=Bez štítku
 issues.filter_milestone=Milník
 issues.filter_milestone_all=Všechny milníky
 issues.filter_milestone_none=Žádné milníky
@@ -1334,6 +1444,7 @@ issues.filter_sort.moststars=Nejvíce hvězdiček
 issues.filter_sort.feweststars=Nejméně hvězdiček
 issues.filter_sort.mostforks=Nejvíce rozštěpení
 issues.filter_sort.fewestforks=Nejméně rozštěpení
+issues.keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu.
 issues.action_open=Otevřít
 issues.action_close=Zavřít
 issues.action_label=Štítek
@@ -1354,6 +1465,7 @@ issues.next=Další
 issues.open_title=otevřený
 issues.closed_title=zavřený
 issues.draft_title=Koncept
+issues.num_comments_1=%d komentář
 issues.num_comments=%d komentářů
 issues.commented_at=`okomentoval <a href="#%s">%s</a>`
 issues.delete_comment_confirm=Jste si jist, že chcete smazat tento komentář?
@@ -1362,7 +1474,9 @@ issues.context.quote_reply=Citovat odpověď
 issues.context.reference_issue=Odkázat v novém úkolu
 issues.context.edit=Upravit
 issues.context.delete=Smazat
+issues.no_content=K dispozici není žádný popis.
 issues.close=Zavřít problém
+issues.comment_pull_merged_at=sloučený commit %[1]s do %[2]s %[3]s
 issues.comment_manually_pull_merged_at=ručně sloučený commit %[1]s do %[2]s %[3]s
 issues.close_comment_issue=Okomentovat a zavřít
 issues.reopen_issue=Znovuotevřít
@@ -1379,8 +1493,16 @@ issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento úkol %[4]s</a> <a id="
 issues.ref_reopened_from=`<a href="%[3]s">znovu otevřel/a tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`z %[1]s`
 issues.author=Autor
+issues.author_helper=Tento uživatel je autor.
 issues.role.owner=Vlastník
+issues.role.owner_helper=Tento uživatel je vlastníkem tohoto repozitáře.
 issues.role.member=Člen
+issues.role.member_helper=Tento uživatel je členem organizace vlastnící tento repositář.
+issues.role.collaborator=Spolupracovník
+issues.role.collaborator_helper=Tento uživatel byl pozván ke spolupráci v repozitáři.
+issues.role.first_time_contributor_helper=Toto je první příspěvek tohoto uživatele do repozitáře.
+issues.role.contributor=Přispěvatel
+issues.role.contributor_helper=Tento uživatel již dříve přispíval do repozitáře.
 issues.re_request_review=Znovu požádat o posouzení
 issues.is_stale=Od tohoto posouzení došlo ke změnám v tomto požadavku na natažení
 issues.remove_request_review=Odstranit žádost o posouzení
@@ -1395,6 +1517,7 @@ issues.label_title=Název štítku
 issues.label_description=Popis štítku
 issues.label_color=Barva štítku
 issues.label_exclusive=Exkluzivní
+issues.label_archived_filter=Zobrazit archivované popisky
 issues.label_count=%d štítků
 issues.label_open_issues=%d otevřených úkolů
 issues.label_edit=Upravit
@@ -1447,6 +1570,7 @@ issues.tracking_already_started=`Již jste spustili sledování času na <a href
 issues.stop_tracking=Zastavit časovač
 issues.stop_tracking_history=`ukončil/a práci %s`
 issues.cancel_tracking=Zahodit
+issues.cancel_tracking_history=`zrušil/a sledování času %s`
 issues.add_time=Přidat čas ručně
 issues.del_time=Odstranit tento časový záznam
 issues.add_time_short=Přidat čas
@@ -1470,6 +1594,7 @@ issues.due_date_form=rrrr-mm-dd
 issues.due_date_form_add=Přidat termín dokončení
 issues.due_date_form_edit=Upravit
 issues.due_date_form_remove=Odstranit
+issues.due_date_not_writer=Potřebujete přístup k zápisu do tohoto repozitáře, abyste mohli aktualizovat datum dokončení problému.
 issues.due_date_not_set=Žádný termín dokončení.
 issues.due_date_added=přidal/a termín dokončení %s %s
 issues.due_date_modified=upravil/a termín termínu z %[2]s na %[1]s %[3]s
@@ -1524,6 +1649,9 @@ issues.review.pending.tooltip=Tento komentář není momentálně viditelný pro
 issues.review.review=Posouzení
 issues.review.reviewers=Posuzovatelé
 issues.review.outdated=Zastaralé
+issues.review.outdated_description=Obsah se změnil od chvíle, kdy byl tento komentář vytvořen
+issues.review.option.show_outdated_comments=Zobrazit zastaralé komentáře
+issues.review.option.hide_outdated_comments=Skrýt zastaralé komentáře
 issues.review.show_outdated=Zobrazit zastaralé
 issues.review.hide_outdated=Skrýt zastaralé
 issues.review.show_resolved=Zobrazit vyřešené
@@ -1563,6 +1691,13 @@ pulls.switch_comparison_type=Přepnout typ porovnání
 pulls.switch_head_and_base=Prohodit hlavní a základní větev
 pulls.filter_branch=Filtrovat větev
 pulls.no_results=Nebyly nalezeny žádné výsledky.
+pulls.show_all_commits=Zobrazit všechny commity
+pulls.show_changes_since_your_last_review=Zobrazit změny od vašeho posledního posouzení
+pulls.showing_only_single_commit=Zobrazuji pouze změny commitu %[1]s
+pulls.showing_specified_commit_range=Zobrazují se pouze změny mezi %[1]s..%[2]s
+pulls.select_commit_hold_shift_for_range=Vyberte commit. Podržte klávesu shift + klepněte pro výběr rozsahu
+pulls.review_only_possible_for_full_diff=Posouzení je možné pouze při zobrazení plného rozlišení
+pulls.filter_changes_by_commit=Filtrovat podle commitu
 pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení.
 pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný.
 pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1576,6 +1711,8 @@ pulls.tab_files=Změněné soubory
 pulls.reopen_to_merge=Prosíme, otevřete znovu tento požadavek na natažení, aby se provedlo sloučení.
 pulls.cant_reopen_deleted_branch=Tento požadavek na natažení nemůže být znovu otevřen protože větev byla smazána.
 pulls.merged=Sloučený
+pulls.merged_success=Požadavek na natažení byl úspěšně sloučen a uzavřen
+pulls.closed=Požadavek na natažení uzavřen
 pulls.manually_merged=Sloučeno ručně
 pulls.merged_info_text=Větev %s může být nyní odstraněna.
 pulls.is_closed=Požadavek na natažení byl uzavřen.
@@ -1592,6 +1729,12 @@ pulls.is_empty=Změny na této větvi jsou již na cílové větvi. Toto bude pr
 pulls.required_status_check_failed=Některé požadované kontroly nebyly úspěšné.
 pulls.required_status_check_missing=Některé požadované kontroly chybí.
 pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento požadavek na natažení.
+pulls.blocked_by_approvals=Tento požadavek na natažení ještě nemá dostatek schválení. Uděleno %d z %d schválení.
+pulls.blocked_by_rejection=Tento požadavek na natažení obsahuje změny požadované oficiálním posuzovatelem.
+pulls.blocked_by_official_review_requests=Tento požadavek na natažení obsahuje oficiální žádosti o posouzení.
+pulls.blocked_by_outdated_branch=Tento požadavek na natažení je zablokován, protože je zastaralý.
+pulls.blocked_by_changed_protected_files_1=Tento požadavek na natažení je zablokován, protože mění chráněný soubor:
+pulls.blocked_by_changed_protected_files_n=Tento požadavek na natažení je zablokován, protože mění chráněné soubory:
 pulls.can_auto_merge_desc=Tento požadavek na natažení může být automaticky sloučen.
 pulls.cannot_auto_merge_desc=Tento požadavek na natažení nemůže být automaticky sloučen, neboť se v něm nachází konflikty.
 pulls.cannot_auto_merge_helper=Pro vyřešení konfliktů proveďte ruční sloučení.
@@ -1626,6 +1769,7 @@ pulls.rebase_conflict_summary=Chybové hlášení
 pulls.unrelated_histories=Sloučení selhalo: Hlavní a základní revize nesdílí společnou historii. Tip: Zkuste jinou strategii
 pulls.merge_out_of_date=Sloučení selhalo: Základ byl aktualizován při generování sloučení. Tip: Zkuste to znovu.
 pulls.head_out_of_date=Sloučení selhalo: Hlavní revize byla aktualizován při generování sloučení. Tip: Zkuste to znovu.
+pulls.has_merged=Chyba: Požadavek na natažení byl sloučen, nelze znovu sloučit nebo změnit cílovou větev.
 pulls.push_rejected=Sloučení selhalo: Nahrání bylo zamítnuto. Zkontrolujte háčky Gitu pro tento repozitář.
 pulls.push_rejected_summary=Úplná zpráva o odmítnutí
 pulls.push_rejected_no_message=Sloučení se nezdařilo: Nahrání bylo odmítnuto, ale nebyla nalezena žádná vzdálená zpráva.<br>Zkontrolujte háčky gitu pro tento repozitář
@@ -1637,13 +1781,21 @@ pulls.status_checks_failure=Některé kontroly se nezdařily
 pulls.status_checks_error=Některé kontroly nahlásily chyby
 pulls.status_checks_requested=Požadováno
 pulls.status_checks_details=Podrobnosti
+pulls.status_checks_hide_all=Skrýt všechny kontroly
+pulls.status_checks_show_all=Zobrazit všechny kontroly
 pulls.update_branch=Aktualizovat větev sloučením
 pulls.update_branch_rebase=Aktualizovat větev pomocí rebase
 pulls.update_branch_success=Aktualizace větve byla úspěšná
 pulls.update_not_allowed=Nemáte oprávnění aktualizovat větev
 pulls.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi
+pulls.close=Zavřít požadavek na natažení
 pulls.closed_at=`uzavřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`znovuotevřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_hint=`Zobrazit <a class="show-instruction">instrukce příkazové řádky</a>.`
+pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny.
+pulls.cmd_instruction_merge_title=Sloučit
+pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea.
+pulls.clear_merge_message=Vymazat zprávu o sloučení
 
 pulls.auto_merge_button_when_succeed=(Když kontroly uspějí)
 pulls.auto_merge_when_succeed=Automaticky sloučit, když všechny kontroly uspějí
@@ -1661,9 +1813,11 @@ pulls.delete.title=Odstranit tento požadavek na natažení?
 pulls.delete.text=Opravdu chcete tento požadavek na natažení smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.)
 
 
+pull.deleted_branch=(odstraněno):%s
 
 milestones.new=Nový milník
 milestones.closed=Zavřen dne %s
+milestones.update_ago=Aktualizováno %s
 milestones.no_due_date=Bez lhůty dokončení
 milestones.open=Otevřít
 milestones.close=Zavřít
@@ -1674,18 +1828,35 @@ milestones.desc=Popis
 milestones.due_date=Termín (volitelný)
 milestones.clear=Zrušit
 milestones.invalid_due_date_format=Termín dokončení musí být ve formátu 'rrrr-mm-dd'.
+milestones.create_success=Milník „%s“ byl vytvořen.
 milestones.edit=Upravit milník
 milestones.edit_subheader=Milník organizuje úkoly a sledují pokrok.
 milestones.cancel=Zrušit
 milestones.modify=Aktualizovat milník
+milestones.edit_success=Milník „%s“ byl aktualizován.
 milestones.deletion=Smazat milník
 milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat?
 milestones.deletion_success=Milník byl odstraněn.
+milestones.filter_sort.earliest_due_data=Nejstarší datum dokončení
+milestones.filter_sort.latest_due_date=Nejnovější datum dokončení
 milestones.filter_sort.least_complete=Nejméně dokončené
 milestones.filter_sort.most_complete=Nejvíce dokončené
 milestones.filter_sort.most_issues=Nejvíce úkolů
 milestones.filter_sort.least_issues=Nejméně úkolů
 
+signing.will_sign=Tento commit bude podepsána klíčem „%s“.
+signing.wont_sign.error=Došlo k chybě při kontrole, zda může být commit podepsán.
+signing.wont_sign.nokey=K podpisu tohoto commitu není k dispozici žádný klíč.
+signing.wont_sign.never=Commity nejsou nikdy podepsány.
+signing.wont_sign.always=Commity jsou vždy podepsány.
+signing.wont_sign.pubkey=Commit nebude podepsán, protože nemáte veřejný klíč spojený s vaším účtem.
+signing.wont_sign.twofa=Pro podepsání commitů musíte mít povoleno dvoufaktorové ověření.
+signing.wont_sign.parentsigned=Commit nebude podepsán, protože nadřazený commit není podepsán.
+signing.wont_sign.basesigned=Sloučení nebude podepsáno, protože základní commit není podepsaný.
+signing.wont_sign.headsigned=Sloučení nebude podepsáno, protože hlavní revize není podepsána.
+signing.wont_sign.commitssigned=Sloučení nebude podepsáno, protože všechny přidružené revize nejsou podepsány.
+signing.wont_sign.approved=Sloučení nebude podepsáno, protože požadavek na natažení není schválen.
+signing.wont_sign.not_signed_in=Nejste přihlášeni.
 
 ext_wiki=Přístup k externí Wiki
 ext_wiki.desc=Odkaz do externí Wiki.
@@ -1709,10 +1880,13 @@ wiki.file_revision=Revize stránky
 wiki.wiki_page_revisions=Revize Wiki stránky
 wiki.back_to_wiki=Zpět na wiki stránku
 wiki.delete_page_button=Smazat stránku
+wiki.delete_page_notice_1=Odstranění Wiki stránky „%s“ nemůže být vráceno zpět. Pokračovat?
 wiki.page_already_exists=Stránka Wiki se stejným názvem již existuje.
+wiki.reserved_page=Jméno Wiki stránky „%s“ je rezervováno.
 wiki.pages=Stránky
 wiki.last_updated=Naposledy aktualizováno: %s
 wiki.page_name_desc=Zadejte název této Wiki stránky. Některé speciální názvy jsou: „Home“, „_Sidebar“ a „_Footer“.
+wiki.original_git_entry_tooltip=Zobrazit originální Git soubor namísto použití přátelského odkazu.
 
 activity=Aktivita
 activity.period.filter_label=Období:
@@ -1804,6 +1978,12 @@ settings.hooks=Webové háčky
 settings.githooks=Háčky Gitu
 settings.basic_settings=Základní nastavení
 settings.mirror_settings=Nastavení zrcadla
+settings.mirror_settings.docs=Nastavte repozitář pro automatickou synchronizaci commitů, značek a větví s jiným repozitářem.
+settings.mirror_settings.docs.disabled_pull_mirror.instructions=Nastavte váš projekt pro automatické nahrávání commitů, značek a větví do jiného repozitáře. Správce webu zakázal zrcadla pro natažení.
+settings.mirror_settings.docs.disabled_push_mirror.instructions=Nastavte svůj projekt pro automatické natažení commitů, značek a větví z jiného repozitáře.
+settings.mirror_settings.docs.no_new_mirrors=Váš repozitář zrcadlí změny do nebo z jiného repozitáře. Mějte prosím na paměti, že v tuto chvíli nemůžete vytvořit žádná nová zrcadla.
+settings.mirror_settings.docs.can_still_use=I když nemůžete upravit stávající zrcadla nebo vytvořit nová, stále můžete použít své stávající zrcadlo.
+settings.mirror_settings.docs.more_information_if_disabled=Více informací o zrcadlech pro nahrání a natažení naleznete zde:
 settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře?
 settings.mirror_settings.docs.pulling_remote_title=Stažení ze vzdáleného úložiště
 settings.mirror_settings.mirrored_repository=Zrcadlený repozitář
@@ -1814,6 +1994,7 @@ settings.mirror_settings.last_update=Poslední aktualizace
 settings.mirror_settings.push_mirror.none=Nenastavena žádná zrcadla pro nahrání
 settings.mirror_settings.push_mirror.remote_url=URL vzdáleného Git repozitáře
 settings.mirror_settings.push_mirror.add=Přidat zrcadlo pro nahrání
+settings.mirror_settings.push_mirror.edit_sync_time=Upravit interval synchronizace zrcadla
 
 settings.sync_mirror=Synchronizovat nyní
 settings.site=Webová stránka
@@ -1851,8 +2032,11 @@ settings.pulls.ignore_whitespace=Ignorovat bílé znaky při konfliktech
 settings.pulls.enable_autodetect_manual_merge=Povolit autodetekci ručních sloučení (Poznámka: V některých zvláštních případech může dojít k nesprávnému rozhodnutí)
 settings.pulls.allow_rebase_update=Povolit aktualizaci větve požadavku na natažení pomocí rebase
 settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat větev požadavku na natažení po jeho sloučení
+settings.pulls.default_allow_edits_from_maintainers=Ve výchozím nastavení povolit úpravy od správců
+settings.releases_desc=Povolit vydání v repozitáři
 settings.packages_desc=Povolit registr balíčků repozitáře
 settings.projects_desc=Povolit projekty v repozitáři
+settings.actions_desc=Povolit akce repozitáře
 settings.admin_settings=Nastavení správce
 settings.admin_enable_health_check=Povolit kontrolu stavu repozitáře (git fsck)
 settings.admin_code_indexer=Indexování kódu
@@ -1879,6 +2063,7 @@ settings.transfer.rejected=Převod repozitáře byl zamítnut.
 settings.transfer.success=Převod repozitáře byl úspěšný.
 settings.transfer_abort=Zrušit převod
 settings.transfer_abort_invalid=Nemůžete zrušit neexistující převod repozitáře.
+settings.transfer_abort_success=Převod repozitáře do %s byl úspěšně zrušen.
 settings.transfer_desc=Předat tento repozitář uživateli nebo organizaci, ve které máte administrátorská práva.
 settings.transfer_form_title=Zadejte jméno repozitáře pro potvrzení:
 settings.transfer_in_progress=V současné době probíhá převod. Zrušte jej, pokud chcete převést tento repozitář jinému uživateli.
@@ -1898,6 +2083,7 @@ settings.trust_model.collaborator.long=Spolupracovník: Důvěřovat podpisům s
 settings.trust_model.collaborator.desc=Platné podpisy spolupracovníků tohoto repozitáře budou označeny jako „důvěryhodné“ - (ať se shodují s autorem, či nikoli). V opačném případě budou platné podpisy označeny jako „nedůvěryhodné“, pokud se podpis shoduje s přispěvatelem a „neodpovídající“, pokud ne.
 settings.trust_model.committer=Přispěvatel
 settings.trust_model.committer.long=Přispěvatel: Důvěřovat podpisům, které odpovídají autorům (což odpovídá GitHub a přinutí Giteu nastavit jako tvůrce pro Giteou podepsané revize)
+settings.trust_model.committer.desc=Platné podpisy budou označeny pouze jako „důvěryhodné“, pokud se shodují s přispěvatelem, jinak budou označeny jako „neodpovídající“. To přinutí Giteu, aby byla přispěvatelem podepsaných commitů se skutečným přispěvatelem označeným jako Co-authored-by: a Co-committed-by: na konci commitu. Výchozí klíč Gitea musí odpovídat uživateli v databázi.
 settings.trust_model.collaboratorcommitter=Spolupracovník+Přispěvatel
 settings.trust_model.collaboratorcommitter.long=Spolupracovník+Přispěvatel: Důvěřovat podpisům od spolupracovníků, které odpovídají tvůrci revize
 settings.trust_model.collaboratorcommitter.desc=Platné podpisy spolupracovníků tohoto repozitáře budou označeny jako „důvěryhodné“, pokud se shodují s přispěvatelem. V opačném případě budou platné podpisy označeny jako "nedůvěryhodné", pokud se podpis shoduje s přispěvatelem a „neodpovídajícím“ v opačném případě. To přinutí Giteu, aby byla označena jako přispěvatel podepsaných commitů se skutečným přispěvatelem označeným jako Co-Authored-By: a Co-Committed-By: na konci commitu. Výchozí klíč Gitea musí odpovídat uživateli v databázi.
@@ -1913,6 +2099,7 @@ settings.delete_notices_2=- Tato operace trvale smaže repozitář <strong>%s</s
 settings.delete_notices_fork_1=- Rozštěpení repozitáře bude nezávislé po smazání.
 settings.deletion_success=Repozitář byl odstraněn.
 settings.update_settings_success=Nastavení repozitáře bylo aktualizováno.
+settings.update_settings_no_unit=Repozitář by měl povolit alespoň určitý druh interakce.
 settings.confirm_delete=Smazat repozitář
 settings.add_collaborator=Přidat spolupracovníka
 settings.add_collaborator_success=Spolupracovník byl přidán.
@@ -1943,12 +2130,14 @@ settings.webhook_deletion_desc=Odstranění webového háčku smaže jeho nastav
 settings.webhook_deletion_success=Webový háček byl smazán.
 settings.webhook.test_delivery=Test doručitelnosti
 settings.webhook.test_delivery_desc=Vyzkoušet tento webový háček pomocí falešné události.
+settings.webhook.test_delivery_desc_disabled=Chcete-li tento webový háček otestovat s falešnou událostí, aktivujte ho.
 settings.webhook.request=Požadavek
 settings.webhook.response=Odpověď
 settings.webhook.headers=Hlavičky
 settings.webhook.payload=Obsah
 settings.webhook.body=Tělo zprávy
 settings.webhook.replay.description=Zopakovat tento webový háček.
+settings.webhook.replay.description_disabled=Chcete-li znovu spustit tento webový háček, aktivujte jej.
 settings.webhook.delivery.success=Událost byla přidána do fronty doručení. Může to trvat několik sekund, než se zobrazí v historii doručení.
 settings.githooks_desc=Jelikož háčky Gitu jsou spravovány Gitem samotným, můžete upravit soubory háčků k provádění uživatelských operací.
 settings.githook_edit_desc=Je-li háček neaktivní, bude zobrazen vzorový obsah. Nebude-li zadán žádný obsah, háček bude vypnut.
@@ -2010,6 +2199,7 @@ settings.event_pull_request_review=Požadavek na natažení přezkoumán
 settings.event_pull_request_review_desc=Požadavek na natažení schválen, odmítnut nebo zkontrolován.
 settings.event_pull_request_sync=Požadavek na natažení synchronizován
 settings.event_pull_request_sync_desc=Požadavek na natažení synchronizován.
+settings.event_pull_request_review_request=Vyžádán požadavek na natažení
 settings.event_package=Balíček
 settings.event_package_desc=Balíček vytvořen nebo odstraněn v repozitáři.
 settings.branch_filter=Filtr větví
@@ -2054,6 +2244,7 @@ settings.title=Název
 settings.deploy_key_content=Obsah
 settings.key_been_used=Klíč pro nasazení se stejným obsahem je již používán.
 settings.key_name_used=Klíč pro nasazení se stejným jménem již existuje.
+settings.add_key_success=Klíč pro nasazení „%s“ byl přidán.
 settings.deploy_key_deletion=Odstranit klíč pro nasazení
 settings.deploy_key_deletion_desc=Odstranění klíče pro nasazení zruší jeho přístup k repozitáři. Pokračovat?
 settings.deploy_key_deletion_success=Klíč pro nasazení byl odstraněn.
@@ -2071,6 +2262,7 @@ settings.protect_disable_push=Zakázat nahrávání
 settings.protect_disable_push_desc=Žádné nahrávání do této větve nebude povoleno.
 settings.protect_enable_push=Povolit nahrávání
 settings.protect_enable_push_desc=Každý, kdo má přístup k zápisu, bude moci nahrávat do této větve (ale ne vynucená nahrávání).
+settings.protect_enable_merge=Povolit sloučení
 settings.protect_whitelist_committers=Povolit nahrání jen vyjmenovaným
 settings.protect_whitelist_committers_desc=Pouze povolení uživatelé budou moci nahrávat do této větve (ale ne vynucení nahrávání).
 settings.protect_whitelist_deploy_keys=Povolit nahrání klíčům pro nasazení s přístupem pro zápis.
@@ -2083,8 +2275,12 @@ settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným už
 settings.protect_merge_whitelist_users=Povolení uživatelé pro slučování:
 settings.protect_merge_whitelist_teams=Povolené týmy pro slučování:
 settings.protect_check_status_contexts=Povolit kontrolu stavu
+settings.protect_status_check_patterns=Vzorce kontroly stavu:
 settings.protect_check_status_contexts_desc=Požadovat kontrolu stavu před sloučením. Vyberte, jaké kontroly stavu musí projít před tím, než je možné větev sloučit do větve, která vyhovuje tomuto pravidlu. Pokud je povoleno, revize musí být nejprve nahrány do jiné větve, projít kontrolou stavu, a následné sloučeny nebo přímo nahrány do větve, která vyhovuje tomuto pravidlu. Pokud nejsou vybrány žádné kontexty, musí být poslední potvrzení úspěšné bez ohledu na kontext.
 settings.protect_check_status_contexts_list=Kontroly stavu pro tento repozitář zjištěné během posledního týdne
+settings.protect_status_check_matched=Odpovídá
+settings.protect_invalid_status_check_pattern=Neplatný vzor kontroly stavu: „%s“.
+settings.protect_no_valid_status_check_patterns=Žádné platné vzory kontroly stavu.
 settings.protect_required_approvals=Požadovaná schválení:
 settings.protect_required_approvals_desc=Umožnit sloučení pouze požadavkům na natažení s dostatečným pozitivním hodnocením.
 settings.protect_approvals_whitelist_enabled=Omezit schválení na povolené uživatele nebo týmy
@@ -2095,8 +2291,18 @@ settings.dismiss_stale_approvals=Odmítnout nekvalitní schválení
 settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto požadavku na natažení, všechna stará schválení budou zamítnuta.
 settings.require_signed_commits=Vyžadovat podepsané revize
 settings.require_signed_commits_desc=Odmítnout nahrání do této větve pokud nejsou podepsaná nebo jsou neověřitelná.
+settings.protect_branch_name_pattern=Vzor jména chráněných větví
+settings.protect_branch_name_pattern_desc=Vzory jmen chráněných větví. Pro vzorovou syntaxi viz <a href="https://github.com/gobwas/glob">dokumentace</a>. Příklady: main, release/**
+settings.protect_patterns=Vzory
+settings.protect_protected_file_patterns=Vzory chráněných souborů (oddělené středníkem „;“):
+settings.protect_protected_file_patterns_desc=Chráněné soubory, které nemají povoleno být měněny přímo, i když uživatel má právo přidávat, upravovat nebo mazat soubory v této větvi. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentaci pro syntaxi vzoru. Příklady: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
+settings.protect_unprotected_file_patterns=Vzory nechráněných souborů (oddělené středníkem „;“):
+settings.protect_unprotected_file_patterns_desc=Nechráněné soubory, které je možné měnit přímo, pokud má uživatel právo zápisu, čímž se obejde omezení push. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentaci pro syntaxi vzoru. Příklady: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
 settings.add_protected_branch=Zapnout ochranu
 settings.delete_protected_branch=Vypnout ochranu
+settings.update_protect_branch_success=Ochrana větví pro větev „%s“ byla aktualizována.
+settings.remove_protected_branch_success=Ochrana větví pro větev „%s“ byla zakázána.
+settings.remove_protected_branch_failed=Odstranění ochranného pravidla větve „%s“ se nezdařilo.
 settings.protected_branch_deletion=Zakázat ochranu větví
 settings.protected_branch_deletion_desc=Zakázání ochrany větví umožní uživatelům s právem zápisu nahrávat do této větve. Pokračovat?
 settings.block_rejected_reviews=Blokovat sloučení při zamítavých posouzeních
@@ -2106,10 +2312,13 @@ settings.block_on_official_review_requests_desc=Slučování nebude možné, pok
 settings.block_outdated_branch=Blokovat sloučení, pokud je požadavek na natažení zastaralý
 settings.block_outdated_branch_desc=Slučování nebude možné, pokud je hlavní větev za základní větví.
 settings.default_branch_desc=Vybrat výchozí větev repozitáře pro požadavky na natažení a revize kódu:
+settings.merge_style_desc=Sloučit styly
 settings.default_merge_style_desc=Výchozí styl sloučení pro požadavky na natažení:
 settings.choose_branch=Vyberte větev…
 settings.no_protected_branch=Nejsou tu žádné chráněné větve.
 settings.edit_protected_branch=Upravit
+settings.protected_branch_required_rule_name=Požadovaný název pravidla
+settings.protected_branch_duplicate_rule_name=Duplikovat název pravidla
 settings.protected_branch_required_approvals_min=Požadovaná schválení nesmí být záporné číslo.
 settings.tags=Značky
 settings.tags.protection=Ochrana značek
@@ -2120,8 +2329,10 @@ settings.tags.protection.allowed.teams=Povolené týmy
 settings.tags.protection.allowed.noone=Nikdo
 settings.tags.protection.create=Chránit značku
 settings.tags.protection.none=Neexistují žádné chráněné značky.
+settings.tags.protection.pattern.description=Můžete použít jediné jméno nebo vzor glob nebo regulární výraz, který bude odpovídat více značek. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">průvodci chráněnými značkami</a>.
 settings.bot_token=Token pro robota
 settings.chat_id=ID chatu
+settings.thread_id=ID vlákna
 settings.matrix.homeserver_url=URL adresa Homeserveru
 settings.matrix.room_id=ID místnosti
 settings.matrix.message_type=Typ zprávy
@@ -2132,6 +2343,11 @@ settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédn
 settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář.
 settings.archive.branchsettings_unavailable=Nastavení větví není dostupné, pokud je repozitář archivovaný.
 settings.archive.tagsettings_unavailable=Nastavení značek není k dispozici, pokud je repozitář archivován.
+settings.unarchive.button=Obnovit repozitář
+settings.unarchive.header=Obnovit tento repozitář
+settings.unarchive.text=Obnovení repozitáře vrátí možnost přijímání commitů a nahrávání. Stejně tak se obnoví i možnost zadávání nových úkolů a požadavků na natažení.
+settings.unarchive.success=Repozitář byl úspěšně obnoven.
+settings.unarchive.error=Nastala chyba při obnovování repozitáře. Prohlédněte si záznam pro více detailů.
 settings.update_avatar_success=Avatar repozitáře byl aktualizován.
 settings.lfs=LFS
 settings.lfs_filelist=LFS soubory uložené v tomto repozitáři
@@ -2198,6 +2414,7 @@ diff.show_more=Zobrazit více
 diff.load=Načíst rozdílové porovnání
 diff.generated=vygenerováno
 diff.vendored=vendorováno
+diff.comment.add_line_comment=Přidat jednořádkový komentář
 diff.comment.placeholder=Zanechat komentář
 diff.comment.markdown_info=Je podporována úprava vzhledu pomocí markdown.
 diff.comment.add_single_comment=Přidat jeden komentář
@@ -2209,7 +2426,9 @@ diff.review.header=Odeslat posouzení
 diff.review.placeholder=Posoudit komentář
 diff.review.comment=Okomentovat
 diff.review.approve=Schválit
+diff.review.self_reject=Autoři požadavků na natažení nemohou požadovat změny na svém vlastním požadavku na natažení
 diff.review.reject=Požadovat změny
+diff.review.self_approve=Autoři požadavku na natažení nemohou schválit svůj vlastní požadavek na natažení
 diff.committed_by=odevzdal
 diff.protected=Chráněno
 diff.image.side_by_side=Vedle sebe
@@ -2231,13 +2450,18 @@ release.compare=Porovnat
 release.edit=upravit
 release.ahead.commits=<strong>%d</strong> revizí
 release.ahead.target=do %s od tohoto vydání
+tag.ahead.target=do %s od této značky
 release.source_code=Zdrojový kód
 release.new_subheader=Vydání organizuje verze projektu.
 release.edit_subheader=Vydání organizuje verze projektu.
 release.tag_name=Název značky
 release.target=Cíl
 release.tag_helper=Vyberte existující značku nebo vytvořte novou značku.
+release.tag_helper_new=Nová značka. Tato značka bude vytvořen z cíle.
+release.tag_helper_existing=Stávající značka.
 release.title=Název vydání
+release.title_empty=Název nesmí být prázdný.
+release.message=Popište toto vydání
 release.prerelease_desc=Označit jako předběžná verze
 release.prerelease_helper=Označit vydání jako nevhodné pro produkční nasazení.
 release.cancel=Zrušit
@@ -2247,6 +2471,7 @@ release.edit_release=Aktualizovat vydání
 release.delete_release=Smazat vydání
 release.delete_tag=Smazat značku
 release.deletion=Smazat vydání
+release.deletion_desc=Smazání vydání jej pouze odebere z Gitea. Nebude to mít vliv na značku Git, obsah vašeho repozitáře nebo jeho historii. Pokračovat?
 release.deletion_success=Vydání bylo odstraněno.
 release.deletion_tag_desc=Odstraní tuto značku z repozitáře. Obsah repozitáře a historie zůstanou nezměněny. Pokračovat?
 release.deletion_tag_success=Značka byla odstraněna.
@@ -2258,32 +2483,56 @@ release.downloads=Soubory ke stažení
 release.download_count=Stažení: %s
 release.add_tag_msg=Použít název a obsah vydání jako zprávu značky.
 release.add_tag=Vytvořit pouze značku
+release.releases_for=Vydání pro %s
 release.tags_for=Značky pro %s
 
 branch.name=Jméno větve
+branch.already_exists=Větev pojmenovaná „%s“ již existuje.
 branch.delete_head=Smazat
+branch.delete=Smazat větev „%s“
 branch.delete_html=Smazat větev
+branch.delete_desc=Smazání větve je trvalé. Přestože zrušená větev může existovat i po krátkou dobu, než bude skutečně odstraněna, NELZE ji většinou vrátit. Pokračovat?
+branch.deletion_success=Větev „%s“ byla smazána.
+branch.deletion_failed=Nepodařilo se odstranit větev „%s“.
+branch.delete_branch_has_new_commits=Větev „%s“ nemůže být smazána, protože byly přidány nové commity po sloučení.
 branch.create_branch=Vytvořit větev <strong>%s</strong>
+branch.create_from=z „%s“
+branch.create_success=Větev „%s“ byla vytvořena.
 branch.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři.
+branch.branch_name_conflict=Jméno větve „%s“ koliduje s již existující větví „%s“.
+branch.tag_collision=Větev „%s“ nemůže být vytvořena, protože v repozitáři existuje značka se stejným jménem.
 branch.deleted_by=Odstranil %s
+branch.restore_success=Větev „%s“ byla obnovena.
+branch.restore_failed=Nepodařilo se obnovit větev „%s“.
+branch.protected_deletion_failed=Větev „%s“ je chráněna. Nemůže být smazána.
+branch.default_deletion_failed=Větev „%s“ je výchozí větev. Nelze ji odstranit.
+branch.restore=Obnovit větev „%s“
+branch.download=Stáhnout větev „%s“
+branch.rename=Přejmenovat větev „%s“
+branch.search=Hledat větev
 branch.included_desc=Tato větev je součástí výchozí větve
 branch.included=Zahrnuje
 branch.create_new_branch=Vytvořit větev z větve:
 branch.confirm_create_branch=Vytvořit větev
+branch.warning_rename_default_branch=Přejmenováváte výchozí větev.
+branch.rename_branch_to=Přejmenovat „%s“ na:
 branch.confirm_rename_branch=Přejmenovat větev
 branch.create_branch_operation=Vytvořit větev
 branch.new_branch=Vytvořit novou větev
+branch.new_branch_from=Vytvořit novou větev z „%s“
 branch.renamed=Větev %s byla přejmenována na %s.
 
 tag.create_tag=Vytvořit značku <strong>%s</strong>
 tag.create_tag_operation=Vytvořit značku
 tag.confirm_create_tag=Vytvořit značku
+tag.create_tag_from=Vytvořit novou značku z „%s“
 
 tag.create_success=Značka „%s“ byla vytvořena.
 
 topic.manage_topics=Spravovat témata
 topic.done=Hotovo
 topic.count_prompt=Nelze vybrat více než 25 témat
+topic.format_prompt=Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a tečky („.“) a může být dlouhé až 35 znaků. Písmena musí být malá.
 
 find_file.go_to_file=Přejít na soubor
 find_file.no_matching=Nebyl nalezen žádný odpovídající soubor
@@ -2315,23 +2564,28 @@ team_permission_desc=Oprávnění
 team_unit_desc=Povolit přístup do částí repozitáře
 team_unit_disabled=(zakázaná)
 
+form.name_reserved=Název organizace „%s“ je rezervován.
+form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu organizace.
 form.create_org_not_allowed=Nemáte oprávnění vytvářet nové organizace.
 
 settings=Nastavení
 settings.options=Organizace
 settings.full_name=Celé jméno
+settings.email=Kontaktní e-mail
 settings.website=Webové stránky
 settings.location=Umístění
 settings.permission=Oprávnění
 settings.repoadminchangeteam=Správce úložišť může týmům přidávat a odebírat přístup
 settings.visibility=Viditelnost
 settings.visibility.public=Veřejná
+settings.visibility.limited=Omezeno (Viditelné pouze pro ověřené uživatele)
 settings.visibility.limited_shortname=Omezený
 settings.visibility.private=Soukromá (viditelné jen členům organizace)
 settings.visibility.private_shortname=Soukromý
 
 settings.update_settings=Upravit nastavení
 settings.update_setting_success=Nastavení organizace bylo upraveno.
+settings.change_orgname_prompt=Poznámka: Změna názvu organizace také změní adresu URL vaší organizace a uvolní staré jméno této organizace.
 settings.change_orgname_redirect_prompt=Staré jméno bude přesměrovávat, dokud nebude znovu obsazeno.
 settings.update_avatar_success=Avatar organizace byl aktualizován.
 settings.delete=Smazat organizaci
@@ -2396,6 +2650,7 @@ teams.remove_all_repos_title=Odstranit všechny repozitáře týmu
 teams.remove_all_repos_desc=Tímto odeberete všechny repozitáře z týmu.
 teams.add_all_repos_title=Přidat všechny repozitáře
 teams.add_all_repos_desc=Tímto přidáte do týmu všechny repozitáře organizace.
+teams.add_nonexistent_repo=Repositář, který se snažíte přidat, neexistuje. Nejdříve jej vytvořte, prosím.
 teams.add_duplicate_users=Uživatel je již členem týmu.
 teams.repos.none=Tento tým nemůže přistoupit k žádným repozitářům.
 teams.members.none=Žádní členové v tomto týmu.
@@ -2406,15 +2661,18 @@ teams.all_repositories_helper=Tým má přístup ke všem repositářům. Výbě
 teams.all_repositories_read_permission_desc=Tomuto týmu je udělen přístup pro <strong>Čtení</strong> <strong>všech repozitářů</strong>: členové mohou prohlížet a klonovat repozitáře.
 teams.all_repositories_write_permission_desc=Tomuto týmu je udělen přístup pro <strong>Zápis</strong> do <strong>všech repozitářů</strong>: členové mohou prohlížet a nahrávat do repozitářů.
 teams.all_repositories_admin_permission_desc=Tomuto týmu je udělen <strong>Administrátorský</strong> přístup do <strong>všech repozitářů</strong>: členové mohou prohlížet, nahrávat a přidávat spolupracovníky do repozitářů.
+teams.invite.title=Byli jste pozváni do týmu <strong>%s</strong> v organizaci <strong>%s</strong>.
 teams.invite.by=Pozvání od %s
 teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže.
 
 [admin]
 dashboard=Přehled
+identity_access=Identita a přístup
 users=Uživatelské účty
 organizations=Organizace
 repositories=Repozitáře
 hooks=Webové háčky
+integrations=Integrace
 authentication=Zdroje ověření
 emails=Uživatelské e-maily
 config=Nastavení
@@ -2423,7 +2681,9 @@ monitor=Sledování
 first_page=První
 last_page=Poslední
 total=Celkem: %d
+settings=Nastavení správce
 
+dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací.
 dashboard.statistic=Souhrn
 dashboard.operations=Operace údržby
 dashboard.system_status=Status systému
@@ -2432,13 +2692,15 @@ dashboard.operation_switch=Přepnout
 dashboard.operation_run=Spustit
 dashboard.clean_unbind_oauth=Vyčistit neprovázané OAuth spojení
 dashboard.clean_unbind_oauth_success=Všechna neprovázaná OAuth spojení byla smazána.
-dashboard.task.started=Zahájen úkol: %[1]s
-dashboard.task.process=Úkol: %[1]s
-dashboard.task.error=Chyba v úkolu: %[1]s: %[3]s
-dashboard.task.finished=Úkol: %[1]s začalo %[2]s skončilo
-dashboard.task.unknown=Neznámý úkol: %[1]s
+dashboard.task.started=Zahájena úloha: %[1]s
+dashboard.task.process=Úloha: %[1]s
+dashboard.task.cancelled=Úloha: %[1]s zrušean: %[3]s
+dashboard.task.error=Chyba v úloze: %[1]s: %[3]s
+dashboard.task.finished=Úloha: %[1]s začala %[2]s skončila
+dashboard.task.unknown=Neznámá úloha: %[1]s
 dashboard.cron.started=Začala naplánovaná úloha: %[1]s
 dashboard.cron.process=Naplánovaná úloha: %[1]s
+dashboard.cron.cancelled=Naplánovaná úloha: %[1]s zrušena: %[3]s
 dashboard.cron.error=Chyba v naplánované úloze: %s: %[3]s
 dashboard.cron.finished=Naplánovaná úloha: %[1]s skončila
 dashboard.delete_inactive_accounts=Smazat všechny neaktivované účty
@@ -2495,9 +2757,11 @@ dashboard.delete_old_actions=Odstranit všechny staré akce z databáze
 dashboard.delete_old_actions.started=Začalo odstraňování všech starých akcí z databáze.
 dashboard.update_checker=Kontrola aktualizací
 dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze
-dashboard.stop_zombie_tasks=Zastavit zombie úkoly
-dashboard.stop_endless_tasks=Zastavit nekonečné úkoly
+dashboard.stop_zombie_tasks=Zastavit zombie úlohy
+dashboard.stop_endless_tasks=Zastavit nekonečné úlohy
 dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy
+dashboard.start_schedule_tasks=Spustit naplánované úlohy
+dashboard.sync_branch.started=Synchronizace větví byla spuštěna
 
 users.user_manage_panel=Správa uživatelských účtů
 users.new_account=Vytvořit uživatelský účet
@@ -2506,12 +2770,16 @@ users.full_name=Celé jméno
 users.activated=Aktivován
 users.admin=Správce
 users.restricted=Omezený
+users.reserved=Rezervováno
+users.bot=Bot
+users.remote=Vzdálený
 users.2fa=2FA
 users.repos=Repozitáře
 users.created=Vytvořen
 users.last_login=Poslední přihlášení
 users.never_login=Nikdy nepřihlášen
 users.send_register_notify=Odeslat upozornění při registraci uživatele
+users.new_success=Uživatelský účet „%s“ byl vytvořen.
 users.edit=Upravit
 users.auth_source=Zdroj ověřování
 users.local=Místní
@@ -2536,6 +2804,7 @@ users.still_own_repo=Tento uživatel stále vlastní jeden nebo více repozitá
 users.still_has_org=Uživatel je člen organizace. Nejprve odstraňte uživatele ze všech organizací.
 users.purge=Vymazat uživatele
 users.purge_help=Vynuceně smazat uživatele a všechny repositáře, organizace a balíčky vlastněné uživatelem. Všechny komentáře budou také smazány.
+users.still_own_packages=Tento uživatel stále vlastní jeden nebo více balíčků, nejprve odstraňte tyto balíčky.
 users.deletion_success=Uživatelský účet byl smazán.
 users.reset_2fa=Resetovat 2FA
 users.list_status_filter.menu_text=Filtr
@@ -2550,6 +2819,7 @@ users.list_status_filter.is_prohibit_login=Zakázat přihlášení
 users.list_status_filter.not_prohibit_login=Povolit přihlášení
 users.list_status_filter.is_2fa_enabled=2FA povoleno
 users.list_status_filter.not_2fa_enabled=2FA zakázáno
+users.details=Detaily uživatele
 
 emails.email_manage_panel=Správa e-mailů uživatele
 emails.primary=Hlavní
@@ -2562,6 +2832,7 @@ emails.updated=E-mail aktualizován
 emails.not_updated=Aktualizace požadované e-mailové adresy se nezdařila: %v
 emails.duplicate_active=Tato e-mailová adresa je již aktivní pro jiného uživatele.
 emails.change_email_header=Aktualizovat vlastnosti e-mailu
+emails.change_email_text=Opravdu chcete aktualizovat tuto e-mailovou adresu?
 
 orgs.org_manage_panel=Správa organizací
 orgs.name=Název
@@ -2580,9 +2851,12 @@ repos.stars=Oblíbení
 repos.forks=Rozštěpení
 repos.issues=Úkoly
 repos.size=Velikost
+repos.lfs_size=Velikost LFS
 
 packages.package_manage_panel=Správa balíčků
 packages.total_size=Celková velikost: %s
+packages.unreferenced_size=Neodkazovaná velikost: %s
+packages.cleanup=Vyčistit prošlá data
 packages.owner=Vlastník
 packages.creator=Tvůrce
 packages.name=Název
@@ -2688,6 +2962,7 @@ auths.sspi_default_language=Výchozí jazyk uživatele
 auths.sspi_default_language_helper=Výchozí jazyk pro uživatele automaticky vytvořené pomocí SSPI auth metody. Pokud dáváte přednost automatickému zjištění jazyka, ponechte prázdné.
 auths.tips=Tipy
 auths.tips.oauth2.general=Ověřování OAuth2
+auths.tips.oauth2.general.tip=Při registraci nové OAuth2 autentizace by URL callbacku/přesměrování měla být:
 auths.tip.oauth2_provider=Poskytovatel OAuth2
 auths.tip.bitbucket=Vytvořte nového OAuth konzumenta na https://bitbucket.org/account/user/<vase-uzivatelske-jmeno>/oauth-consumers/new a přidejte oprávnění „Account“ - „Read“
 auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci pomocí následujícího menu „Nastavení -> Zabezpečení -> OAuth 2.0 klient“
@@ -2699,10 +2974,12 @@ auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzo
 auths.tip.openid_connect=Použijte OpenID URL pro objevování spojení (<server>/.well-known/openid-configuration) k nastavení koncových bodů
 auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená
 auths.tip.discord=Registrujte novou aplikaci na https://discordapp.com/developers/applications/me
+auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://docs.gitea.com/development/oauth2-provider
 auths.tip.yandex=Vytvořte novou aplikaci na https://oauth.yandex.com/client/new. Vyberte následující oprávnění z „Yandex.Passport API“ sekce: „Přístup k e-mailové adrese“, „Přístup k uživatelskému avataru“ a „Přístup k uživatelskému jménu, jménu a příjmení, pohlaví“
 auths.tip.mastodon=Vložte vlastní URL instance pro mastodon, kterou se chcete autentizovat (nebo použijte výchozí)
 auths.edit=Upravit zdroj ověřování
 auths.activated=Tento zdroj ověřování je aktivován
+auths.new_success=Zdroj ověřování „%s“ byl přidán.
 auths.update_success=Zdroj ověřování byl aktualizován.
 auths.update=Aktualizovat zdroj ověřování
 auths.delete=Smazat zdroj ověřování
@@ -2710,7 +2987,9 @@ auths.delete_auth_title=Smazat zdroj ověřování
 auths.delete_auth_desc=Zamezíte přihlášení uživatelům pomocí tohoto zdroje ověřování, pokud ho smažete. Pokračovat?
 auths.still_in_used=Zdroj ověřování je stále používán. Nejprve převeďte nebo smažte všechny uživatele, kteří používají tento způsob ověřování.
 auths.deletion_success=Zdroj ověřování byl smazán.
+auths.login_source_exist=Zdroj ověřování „%s“ již existuje.
 auths.login_source_of_type_exist=Zdroj ověřování tohoto typu již existuje.
+auths.unable_to_initialize_openid=Nelze inicializovat poskytovatele OpenID Connect: %s
 auths.invalid_openIdConnectAutoDiscoveryURL=Neplatná URL adresa pro automatické vyhledání (musí být platná adresa URL začínající http:// nebo https://)
 
 config.server_config=Nastavení serveru
@@ -2725,6 +3004,7 @@ config.disable_router_log=Vypnout log směrovače
 config.run_user=Spustit jako uživatel
 config.run_mode=Režim spouštění
 config.git_version=Verze Gitu
+config.app_data_path=Cesta k datům aplikace
 config.repo_root_path=Kořenový adresář repozitářů
 config.lfs_root_path=Kořenový adresář LFS
 config.log_file_root_path=Adresář logů
@@ -2799,6 +3079,9 @@ config.mailer_sendmail_timeout=Časový limit Sandmail
 config.mailer_use_dummy=Fiktivní
 config.test_email_placeholder=E-mail (např.: test@example.com)
 config.send_test_mail=Odeslat zkušební e-mail
+config.send_test_mail_submit=Odeslat
+config.test_mail_failed=Odeslání testovacího e-mailu na „%s“ selhalo: %v
+config.test_mail_sent=Zkušební e-mail byl odeslán na „%s“.
 
 config.oauth_config=Nastavení ověření OAuth
 config.oauth_enabled=Zapnutý
@@ -2838,10 +3121,12 @@ config.git_gc_timeout=Časový limit operace GC
 config.log_config=Nastavení logů
 config.disabled_logger=Zakázané
 config.access_log_mode=Režim logování přístupu
+config.access_log_template=Šablona záznamu přístupu
 config.xorm_log_sql=Logovat SQL
 
 config.set_setting_failed=Nastavení %s se nezdařilo
 
+monitor.stats=Statistiky
 
 monitor.cron=Naplánované úlohy
 monitor.name=Název
@@ -2851,6 +3136,7 @@ monitor.previous=Předešlý čas spuštění
 monitor.execute_times=Vykonání
 monitor.process=Spuštěné procesy
 monitor.stacktrace=Výpisy zásobníku
+monitor.processes_count=%d procesů
 monitor.desc=Popis
 monitor.start=Čas zahájení
 monitor.execute_time=Doba provádění
@@ -2868,6 +3154,7 @@ monitor.queue.exemplar=Typ vzoru
 monitor.queue.numberworkers=Počet workerů
 monitor.queue.maxnumberworkers=Maximální počet workerů
 monitor.queue.numberinqueue=Číslo ve frontě
+monitor.queue.review_add=Posoudit / přidat workery
 monitor.queue.settings.title=Nastavení fondu
 monitor.queue.settings.maxnumberworkers=Maximální počet workerů
 monitor.queue.settings.maxnumberworkers.placeholder=V současné době %[1]d
@@ -2987,6 +3274,7 @@ desc=Správa balíčků repozitáře.
 empty=Zatím nejsou žádné balíčky.
 empty.documentation=Další informace o registru balíčků naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
 empty.repo=Nahráli jste balíček, ale nezobrazil se zde? Přejděte na <a href="%[1]s">nastavení balíčku</a> a propojte jej s tímto repozitářem.
+registry.documentation=Další informace o registru %s naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
 filter.type=Typ
 filter.type.all=Vše
 filter.no_result=Váš filtr nepřinesl žádné výsledky.
@@ -3050,6 +3338,7 @@ debian.repository.distributions=Distribuce
 debian.repository.components=Komponenty
 debian.repository.architectures=Architektury
 generic.download=Stáhnout balíček z příkazové řádky:
+go.install=Nainstalujte balíček z příkazové řádky:
 helm.registry=Nastavte tento registr z příkazového řádku:
 helm.install=Pro instalaci balíčku spusťte následující příkaz:
 maven.registry=Nastavte tento registr ve vašem projektu <code>pom.xml</code> souboru:
@@ -3071,6 +3360,8 @@ pub.install=Chcete-li nainstalovat balíček pomocí Dart, spusťte následujíc
 pypi.requires=Vyžaduje Python
 pypi.install=Pro instalaci balíčku pomocí pip spusťte následující příkaz:
 rpm.registry=Nastavte tento registr z příkazového řádku:
+rpm.distros.redhat=na distribuce založené na RedHat
+rpm.distros.suse=na distribuce založené na SUSE
 rpm.install=Pro instalaci balíčku spusťte následující příkaz:
 rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz:
 rubygems.install2=nebo ho přidejte do Gemfie:
@@ -3079,6 +3370,8 @@ rubygems.dependencies.development=Vývojové závislosti
 rubygems.required.ruby=Vyžaduje verzi Ruby
 rubygems.required.rubygems=Vyžaduje verzi RubyGem
 swift.registry=Nastavte tento registr z příkazového řádku:
+swift.install=Přidejte balíček do svého <code>Package.swift</code> souboru:
+swift.install2=a spustit následující příkaz:
 vagrant.install=Pro přidání Vagrant box spusťte následující příkaz:
 settings.link=Propojit tento balíček s repozitářem
 settings.link.description=Pokud propojíte balíček s repozitářem, je tento balíček uveden v seznamu balíčků repozitáře.
@@ -3093,6 +3386,7 @@ settings.delete.success=Balíček byl odstraněn.
 settings.delete.error=Nepodařilo se odstranit balíček.
 owner.settings.cargo.title=Index registru Cargo
 owner.settings.cargo.initialize=Inicializovat index
+owner.settings.cargo.initialize.description=Pro použití Cargo registru je zapotřebí speciální index Git. Použití této možnosti (znovu)vytvoří repozitář a automaticky jej nastaví.
 owner.settings.cargo.initialize.error=Nepodařilo se inicializovat Cargo index: %v
 owner.settings.cargo.initialize.success=Index Cargo byl úspěšně vytvořen.
 owner.settings.cargo.rebuild=Znovu vytvořit Index
@@ -3101,6 +3395,7 @@ owner.settings.cargo.rebuild.success=Cargo Index byl úspěšně obnoven.
 owner.settings.cleanuprules.title=Spravovat pravidla pro čištění
 owner.settings.cleanuprules.add=Přidat pravidlo pro čištění
 owner.settings.cleanuprules.edit=Upravit pravidlo pro čištění
+owner.settings.cleanuprules.none=Nejsou k dispozici žádná pravidla čištění. Prohlédněte si prosím dokumentaci.
 owner.settings.cleanuprules.preview=Náhled pravidla pro čištění
 owner.settings.cleanuprules.preview.overview=%d balíčků má být odstraněno.
 owner.settings.cleanuprules.preview.none=Pravidlo čištění neodpovídá žádným balíčkům.
@@ -3119,6 +3414,7 @@ owner.settings.cleanuprules.success.update=Pravidlo pro čištění bylo aktuali
 owner.settings.cleanuprules.success.delete=Pravidlo pro čištění bylo odstraněno.
 owner.settings.chef.title=Registr Chef
 owner.settings.chef.keypair=Generovat pár klíčů
+owner.settings.chef.keypair.description=Pro autentizaci do registru Chef je zapotřebí pár klíčů. Pokud jste předtím vytvořili pár klíčů, nově vygenerovaný pár klíčů vyřadí starý pár klíčů.
 
 [secrets]
 secrets=Tajné klíče
@@ -3145,6 +3441,7 @@ status.waiting=Čekání
 status.running=Probíhá
 status.success=Úspěch
 status.failure=Chyba
+status.cancelled=Zrušeno
 status.skipped=Přeskočeno
 status.blocked=Blokováno
 
@@ -3158,7 +3455,8 @@ runners.description=Popis
 runners.labels=Štítky
 runners.last_online=Poslední čas online
 runners.runner_title=Runner
-runners.task_list=Nedávné úkoly na tomto runneru
+runners.task_list=Nedávné úlohy na tomto runneru
+runners.task_list.no_tasks=Zatím zde nejsou žádné úlohy.
 runners.task_list.run=Spustit
 runners.task_list.status=Status
 runners.task_list.repository=Repozitář
@@ -3172,23 +3470,65 @@ runners.delete_runner=Odstranit tento runner
 runners.delete_runner_success=Runner byl úspěšně odstraněn
 runners.delete_runner_failed=Odstranění runneru selhalo
 runners.delete_runner_header=Potvrdit odstranění tohoto runneru
+runners.delete_runner_notice=Pokud na tomto runneru běží úloha, bude ukončena a označena jako neúspěšná. Může dojít k přerušení vytváření pracovního postupu.
 runners.status.unspecified=Neznámý
 runners.status.idle=Nečinný
 runners.status.active=Aktivní
 runners.status.offline=Offline
 runners.version=Verze
+runners.reset_registration_token=Resetovat registrační token
+runners.reset_registration_token_success=Registrační token runneru byl úspěšně obnoven
 
 runs.all_workflows=Všechny pracovní postupy
 runs.commit=Commit
+runs.scheduled=Naplánováno
+runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s
+runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s
+runs.actor=Aktér
 runs.status=Status
+runs.actors_no_select=Všichni aktéři
+runs.status_no_select=Všechny stavy
+runs.no_results=Nebyly nalezeny žádné výsledky.
+runs.no_workflows=Zatím neexistují žádné pracovní postupy.
+runs.no_workflows.quick_start=Nevíte jak začít s Gitea Action? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">průvodce rychlým startem</a>.
+runs.no_workflows.documentation=Další informace o Gitea Action, viz <a target="_blank" rel="noopener noreferrer" href="%s">dokumentace</a>.
+runs.no_runs=Pracovní postup zatím nebyl spuštěn.
+runs.empty_commit_message=(prázdná zpráva commitu)
+
+workflow.disable=Zakázat pracovní postup
+workflow.disable_success=Pracovní postup „%s“ byl úspěšně deaktivován.
+workflow.enable=Povolit pracovní postup
+workflow.enable_success=Pracovní postup „%s“ byl úspěšně aktivován.
+workflow.disabled=Pracovní postup je zakázán.
 
 
-
+variables=Proměnné
+variables.management=Správa proměnných
+variables.creation=Přidat proměnnou
+variables.none=Zatím nejsou žádné proměnné.
+variables.deletion=Odstranit proměnnou
+variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat?
+variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak.
+variables.id_not_exist=Proměnná s id %d neexistuje.
+variables.edit=Upravit proměnnou
+variables.deletion.failed=Nepodařilo se odstranit proměnnou.
+variables.deletion.success=Proměnná byla odstraněna.
+variables.creation.failed=Přidání proměnné se nezdařilo.
+variables.creation.success=Proměnná „%s“ byla přidána.
+variables.update.failed=Úprava proměnné se nezdařila.
+variables.update.success=Proměnná byla upravena.
 
 [projects]
+type-1.display_name=Samostatný projekt
+type-2.display_name=Projekt repozitíře
 type-3.display_name=Projekt organizace
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Adresář
+normal_file=Normální soubor
+executable_file=Spustitelný soubor
 symbolic_link=Symbolický odkaz
+submodule=Submodul
 

From 8f9c9d3a5fa185f4a61f71e49f15b6d5e611b44a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 16 Feb 2024 03:20:50 +0100
Subject: [PATCH 047/679] Update JS and PY dependencies (#29184)

- Update all excluding `@mcaptcha/vanilla-glue` and
`eslint-plugin-array-func`
- Tested pdf, chart.js, swagger
---
 package-lock.json | 733 +++++++++++++++++++++++++---------------------
 package.json      |  16 +-
 poetry.lock       |   8 +-
 pyproject.toml    |   2 +-
 4 files changed, 404 insertions(+), 355 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 764ae51f9d..48da8124e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,8 +18,8 @@
         "@webcomponents/custom-elements": "1.6.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
-        "asciinema-player": "3.6.3",
-        "chart.js": "4.3.0",
+        "asciinema-player": "3.6.4",
+        "chart.js": "4.4.1",
         "chartjs-adapter-dayjs-4": "1.0.4",
         "chartjs-plugin-zoom": "2.0.1",
         "clippie": "4.0.6",
@@ -39,22 +39,22 @@
         "minimatch": "9.0.3",
         "monaco-editor": "0.46.0",
         "monaco-editor-webpack-plugin": "7.1.0",
-        "pdfobject": "2.2.12",
+        "pdfobject": "2.3.0",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
-        "swagger-ui-dist": "5.11.3",
+        "swagger-ui-dist": "5.11.6",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
         "tippy.js": "6.3.7",
         "toastify-js": "1.12.0",
         "tributejs": "5.1.3",
         "uint8-to-base64": "0.2.0",
-        "vue": "3.4.18",
+        "vue": "3.4.19",
         "vue-bar-graph": "2.0.0",
         "vue-chartjs": "5.3.0",
         "vue-loader": "17.4.2",
         "vue3-calendar-heatmap": "2.0.5",
-        "webpack": "5.90.1",
+        "webpack": "5.90.2",
         "webpack-cli": "5.1.4",
         "wrap-ansi": "9.0.0"
       },
@@ -62,7 +62,7 @@
         "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
         "@playwright/test": "1.41.2",
         "@stoplight/spectral-cli": "6.11.0",
-        "@stylistic/eslint-plugin-js": "1.6.1",
+        "@stylistic/eslint-plugin-js": "1.6.2",
         "@stylistic/stylelint-plugin": "2.0.0",
         "@vitejs/plugin-vue": "5.0.4",
         "eslint": "8.56.0",
@@ -72,7 +72,7 @@
         "eslint-plugin-no-jquery": "2.7.0",
         "eslint-plugin-no-use-extend-native": "0.5.0",
         "eslint-plugin-regexp": "2.2.0",
-        "eslint-plugin-sonarjs": "0.23.0",
+        "eslint-plugin-sonarjs": "0.24.0",
         "eslint-plugin-unicorn": "51.0.1",
         "eslint-plugin-vitest": "0.3.22",
         "eslint-plugin-vitest-globals": "1.4.0",
@@ -1221,9 +1221,9 @@
       }
     },
     "node_modules/@jridgewell/resolve-uri": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
-      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
       "engines": {
         "node": ">=6.0.0"
       }
@@ -1456,9 +1456,9 @@
       "dev": true
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
-      "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.11.0.tgz",
+      "integrity": "sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==",
       "cpu": [
         "arm"
       ],
@@ -1469,9 +1469,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz",
-      "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.11.0.tgz",
+      "integrity": "sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==",
       "cpu": [
         "arm64"
       ],
@@ -1482,9 +1482,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz",
-      "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.11.0.tgz",
+      "integrity": "sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==",
       "cpu": [
         "arm64"
       ],
@@ -1495,9 +1495,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz",
-      "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.11.0.tgz",
+      "integrity": "sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==",
       "cpu": [
         "x64"
       ],
@@ -1508,9 +1508,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz",
-      "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.11.0.tgz",
+      "integrity": "sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==",
       "cpu": [
         "arm"
       ],
@@ -1521,9 +1521,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz",
-      "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.11.0.tgz",
+      "integrity": "sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==",
       "cpu": [
         "arm64"
       ],
@@ -1534,9 +1534,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz",
-      "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.11.0.tgz",
+      "integrity": "sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==",
       "cpu": [
         "arm64"
       ],
@@ -1547,9 +1547,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz",
-      "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.11.0.tgz",
+      "integrity": "sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==",
       "cpu": [
         "riscv64"
       ],
@@ -1560,9 +1560,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz",
-      "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.11.0.tgz",
+      "integrity": "sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==",
       "cpu": [
         "x64"
       ],
@@ -1573,9 +1573,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz",
-      "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.11.0.tgz",
+      "integrity": "sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==",
       "cpu": [
         "x64"
       ],
@@ -1586,9 +1586,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz",
-      "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.11.0.tgz",
+      "integrity": "sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==",
       "cpu": [
         "arm64"
       ],
@@ -1599,9 +1599,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz",
-      "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.11.0.tgz",
+      "integrity": "sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==",
       "cpu": [
         "ia32"
       ],
@@ -1612,9 +1612,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz",
-      "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.11.0.tgz",
+      "integrity": "sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==",
       "cpu": [
         "x64"
       ],
@@ -2082,11 +2082,12 @@
       "dev": true
     },
     "node_modules/@stylistic/eslint-plugin-js": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.1.tgz",
-      "integrity": "sha512-gHRxkbA5p8S1fnChE7Yf5NFltRZCzbCuQOcoTe93PSKBC4GqVjZmlWUSLz9pJKHvDAUTjWkfttWHIOaFYPEhRQ==",
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.2.tgz",
+      "integrity": "sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==",
       "dev": true,
       "dependencies": {
+        "@types/eslint": "^8.56.2",
         "acorn": "^8.11.3",
         "escape-string-regexp": "^4.0.0",
         "eslint-visitor-keys": "^3.4.3",
@@ -2225,9 +2226,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.14",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz",
-      "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==",
+      "version": "20.11.19",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
+      "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -2245,9 +2246,9 @@
       "dev": true
     },
     "node_modules/@types/semver": {
-      "version": "7.5.6",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
-      "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+      "version": "7.5.7",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
+      "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
       "dev": true
     },
     "node_modules/@types/tern": {
@@ -2512,36 +2513,36 @@
       }
     },
     "node_modules/@vue/compiler-core": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.18.tgz",
-      "integrity": "sha512-F7YK8lMK0iv6b9/Gdk15A67wM0KKZvxDxed0RR60C1z9tIJTKta+urs4j0RTN5XqHISzI3etN3mX0uHhjmoqjQ==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
+      "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
       "dependencies": {
         "@babel/parser": "^7.23.9",
-        "@vue/shared": "3.4.18",
+        "@vue/shared": "3.4.19",
         "entities": "^4.5.0",
         "estree-walker": "^2.0.2",
         "source-map-js": "^1.0.2"
       }
     },
     "node_modules/@vue/compiler-dom": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.18.tgz",
-      "integrity": "sha512-24Eb8lcMfInefvQ6YlEVS18w5Q66f4+uXWVA+yb7praKbyjHRNuKVWGuinfSSjM0ZIiPi++QWukhkgznBaqpEA==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
+      "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
       "dependencies": {
-        "@vue/compiler-core": "3.4.18",
-        "@vue/shared": "3.4.18"
+        "@vue/compiler-core": "3.4.19",
+        "@vue/shared": "3.4.19"
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.18.tgz",
-      "integrity": "sha512-rG5tqtnzwrVpMqAQ7FHtvHaV70G6LLfJIWLYZB/jZ9m/hrnZmIQh+H3ewnC5onwe/ibljm9+ZupxeElzqCkTAw==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz",
+      "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==",
       "dependencies": {
         "@babel/parser": "^7.23.9",
-        "@vue/compiler-core": "3.4.18",
-        "@vue/compiler-dom": "3.4.18",
-        "@vue/compiler-ssr": "3.4.18",
-        "@vue/shared": "3.4.18",
+        "@vue/compiler-core": "3.4.19",
+        "@vue/compiler-dom": "3.4.19",
+        "@vue/compiler-ssr": "3.4.19",
+        "@vue/shared": "3.4.19",
         "estree-walker": "^2.0.2",
         "magic-string": "^0.30.6",
         "postcss": "^8.4.33",
@@ -2560,57 +2561,57 @@
       }
     },
     "node_modules/@vue/compiler-ssr": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.18.tgz",
-      "integrity": "sha512-hSlv20oUhPxo2UYUacHgGaxtqP0tvFo6ixxxD6JlXIkwzwoZ9eKK6PFQN4hNK/R13JlNyldwWt/fqGBKgWJ6nQ==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
+      "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.18",
-        "@vue/shared": "3.4.18"
+        "@vue/compiler-dom": "3.4.19",
+        "@vue/shared": "3.4.19"
       }
     },
     "node_modules/@vue/reactivity": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.18.tgz",
-      "integrity": "sha512-7uda2/I0jpLiRygprDo5Jxs2HJkOVXcOMlyVlY54yRLxoycBpwGJRwJT9EdGB4adnoqJDXVT2BilUAYwI7qvmg==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz",
+      "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==",
       "dependencies": {
-        "@vue/shared": "3.4.18"
+        "@vue/shared": "3.4.19"
       }
     },
     "node_modules/@vue/runtime-core": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.18.tgz",
-      "integrity": "sha512-7mU9diCa+4e+8/wZ7Udw5pwTH10A11sZ1nldmHOUKJnzCwvZxfJqAtw31mIf4T5H2FsLCSBQT3xgioA9vIjyDQ==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz",
+      "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==",
       "dependencies": {
-        "@vue/reactivity": "3.4.18",
-        "@vue/shared": "3.4.18"
+        "@vue/reactivity": "3.4.19",
+        "@vue/shared": "3.4.19"
       }
     },
     "node_modules/@vue/runtime-dom": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.18.tgz",
-      "integrity": "sha512-2y1Mkzcw1niSfG7z3Qx+2ir9Gb4hdTkZe5p/I8x1aTIKQE0vY0tPAEUPhZm5tx6183gG3D/KwHG728UR0sIufA==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz",
+      "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==",
       "dependencies": {
-        "@vue/runtime-core": "3.4.18",
-        "@vue/shared": "3.4.18",
+        "@vue/runtime-core": "3.4.19",
+        "@vue/shared": "3.4.19",
         "csstype": "^3.1.3"
       }
     },
     "node_modules/@vue/server-renderer": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.18.tgz",
-      "integrity": "sha512-YJd1wa7mzUN3NRqLEsrwEYWyO+PUBSROIGlCc3J/cvn7Zu6CxhNLgXa8Z4zZ5ja5/nviYO79J1InoPeXgwBTZA==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz",
+      "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==",
       "dependencies": {
-        "@vue/compiler-ssr": "3.4.18",
-        "@vue/shared": "3.4.18"
+        "@vue/compiler-ssr": "3.4.19",
+        "@vue/shared": "3.4.19"
       },
       "peerDependencies": {
-        "vue": "3.4.18"
+        "vue": "3.4.19"
       }
     },
     "node_modules/@vue/shared": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.18.tgz",
-      "integrity": "sha512-CxouGFxxaW5r1WbrSmWwck3No58rApXgRSBxrqgnY1K+jk20F6DrXJkHdH9n4HVT+/B6G2CAn213Uq3npWiy8Q=="
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
+      "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw=="
     },
     "node_modules/@webassemblyjs/ast": {
       "version": "1.11.6",
@@ -2975,13 +2976,16 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
     "node_modules/array-buffer-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
-      "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+      "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "is-array-buffer": "^3.0.1"
+        "call-bind": "^1.0.5",
+        "is-array-buffer": "^3.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3005,17 +3009,18 @@
       }
     },
     "node_modules/arraybuffer.prototype.slice": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
-      "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+      "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
       "dev": true,
       "dependencies": {
-        "array-buffer-byte-length": "^1.0.0",
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "get-intrinsic": "^1.2.1",
-        "is-array-buffer": "^3.0.2",
+        "array-buffer-byte-length": "^1.0.1",
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.22.3",
+        "es-errors": "^1.2.1",
+        "get-intrinsic": "^1.2.3",
+        "is-array-buffer": "^3.0.4",
         "is-shared-array-buffer": "^1.0.2"
       },
       "engines": {
@@ -3035,9 +3040,9 @@
       }
     },
     "node_modules/asciinema-player": {
-      "version": "3.6.3",
-      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.3.tgz",
-      "integrity": "sha512-62aDgLpbuduhmpFfNgPOzf6fOluACLsftVnjpWJjUXX6dqhqTckFqWoJ+ayA0XjSlZ9l9wXTcJqRqvAAIpMblg==",
+      "version": "3.6.4",
+      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.4.tgz",
+      "integrity": "sha512-yyMHTjoDuz82/BYPrc3J5GjOtlNI5t2VHTZWss8BmRcY/6nXv+Vilip+XzwIyRBa3/2SSn9FJIEg8bJXBc9o4w==",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
         "solid-js": "^1.3.0"
@@ -3101,9 +3106,9 @@
       }
     },
     "node_modules/available-typed-arrays": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
-      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz",
+      "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
@@ -3170,9 +3175,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.22.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
-      "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
+      "version": "4.23.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -3188,8 +3193,8 @@
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001580",
-        "electron-to-chromium": "^1.4.648",
+        "caniuse-lite": "^1.0.30001587",
+        "electron-to-chromium": "^1.4.668",
         "node-releases": "^2.0.14",
         "update-browserslist-db": "^1.0.13"
       },
@@ -3256,14 +3261,19 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
-      "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
       "dev": true,
       "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.1",
-        "set-function-length": "^1.1.1"
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3279,9 +3289,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001581",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
-      "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
+      "version": "1.0.30001587",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz",
+      "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==",
       "funding": [
         {
           "type": "opencollective",
@@ -3340,9 +3350,9 @@
       }
     },
     "node_modules/chart.js": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz",
-      "integrity": "sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
+      "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
       "dependencies": {
         "@kurkle/color": "^0.3.0"
       },
@@ -3572,12 +3582,12 @@
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
     "node_modules/core-js-compat": {
-      "version": "3.35.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
-      "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
+      "version": "3.36.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
+      "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.22.2"
+        "browserslist": "^4.22.3"
       },
       "funding": {
         "type": "opencollective",
@@ -4327,17 +4337,20 @@
       "dev": true
     },
     "node_modules/define-data-property": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
-      "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.1",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/define-properties": {
@@ -4392,9 +4405,9 @@
       }
     },
     "node_modules/diff": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
-      "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+      "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
       "engines": {
         "node": ">=0.3.1"
       }
@@ -4520,9 +4533,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.653",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.653.tgz",
-      "integrity": "sha512-wA2A2LQCqnEwQAvwADQq3KpMpNwgAUBnRmrFgRzHnPhbQUFArTR32Ab46f4p0MovDLcg4uqd4nCsN2hTltslpA=="
+      "version": "1.4.671",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.671.tgz",
+      "integrity": "sha512-UUlE+/rWbydmp+FW8xlnnTA5WNA0ZZd2XL8CuMS72rh+k4y1f8+z6yk3UQhEwqHQWj6IBdL78DwWOdGMvYfQyA=="
     },
     "node_modules/elkjs": {
       "version": "0.9.1",
@@ -4575,9 +4588,9 @@
       }
     },
     "node_modules/envinfo": {
-      "version": "7.11.0",
-      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz",
-      "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==",
+      "version": "7.11.1",
+      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz",
+      "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==",
       "bin": {
         "envinfo": "dist/cli.js"
       },
@@ -4595,50 +4608,52 @@
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.22.3",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
-      "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
+      "version": "1.22.4",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz",
+      "integrity": "sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==",
       "dev": true,
       "dependencies": {
-        "array-buffer-byte-length": "^1.0.0",
-        "arraybuffer.prototype.slice": "^1.0.2",
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.5",
-        "es-set-tostringtag": "^2.0.1",
+        "array-buffer-byte-length": "^1.0.1",
+        "arraybuffer.prototype.slice": "^1.0.3",
+        "available-typed-arrays": "^1.0.6",
+        "call-bind": "^1.0.7",
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "es-set-tostringtag": "^2.0.2",
         "es-to-primitive": "^1.2.1",
         "function.prototype.name": "^1.1.6",
-        "get-intrinsic": "^1.2.2",
-        "get-symbol-description": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "get-symbol-description": "^1.0.2",
         "globalthis": "^1.0.3",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0",
+        "has-property-descriptors": "^1.0.2",
         "has-proto": "^1.0.1",
         "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0",
-        "internal-slot": "^1.0.5",
-        "is-array-buffer": "^3.0.2",
+        "hasown": "^2.0.1",
+        "internal-slot": "^1.0.7",
+        "is-array-buffer": "^3.0.4",
         "is-callable": "^1.2.7",
         "is-negative-zero": "^2.0.2",
         "is-regex": "^1.1.4",
         "is-shared-array-buffer": "^1.0.2",
         "is-string": "^1.0.7",
-        "is-typed-array": "^1.1.12",
+        "is-typed-array": "^1.1.13",
         "is-weakref": "^1.0.2",
         "object-inspect": "^1.13.1",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.4",
-        "regexp.prototype.flags": "^1.5.1",
-        "safe-array-concat": "^1.0.1",
-        "safe-regex-test": "^1.0.0",
+        "object.assign": "^4.1.5",
+        "regexp.prototype.flags": "^1.5.2",
+        "safe-array-concat": "^1.1.0",
+        "safe-regex-test": "^1.0.3",
         "string.prototype.trim": "^1.2.8",
         "string.prototype.trimend": "^1.0.7",
         "string.prototype.trimstart": "^1.0.7",
-        "typed-array-buffer": "^1.0.0",
+        "typed-array-buffer": "^1.0.1",
         "typed-array-byte-length": "^1.0.0",
         "typed-array-byte-offset": "^1.0.0",
         "typed-array-length": "^1.0.4",
         "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.13"
+        "which-typed-array": "^1.1.14"
       },
       "engines": {
         "node": ">= 0.4"
@@ -4648,18 +4663,18 @@
       }
     },
     "node_modules/es-aggregate-error": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.11.tgz",
-      "integrity": "sha512-DCiZiNlMlbvofET/cE55My387NiLvuGToBEZDdK9U2G3svDCjL8WOgO5Il6lO83nQ8qmag/R9nArdpaFQ/m3lA==",
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.12.tgz",
+      "integrity": "sha512-j0PupcmELoVbYS2NNrsn5zcLLEsryQwP02x8fRawh7c2eEaPHwJFAxltZsqV7HJjsF57+SMpYyVRWgbVLfOagg==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.1.0",
+        "define-data-property": "^1.1.1",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.1",
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.2.1",
+        "es-abstract": "^1.22.3",
+        "es-errors": "^1.1.0",
+        "function-bind": "^1.1.2",
         "globalthis": "^1.0.3",
-        "has-property-descriptors": "^1.0.0",
+        "has-property-descriptors": "^1.0.1",
         "set-function-name": "^2.0.1"
       },
       "engines": {
@@ -4669,6 +4684,27 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/es-define-property": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.2.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es-module-lexer": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
@@ -4760,9 +4796,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
       "engines": {
         "node": ">=6"
       }
@@ -5020,12 +5056,12 @@
       }
     },
     "node_modules/eslint-plugin-sonarjs": {
-      "version": "0.23.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.23.0.tgz",
-      "integrity": "sha512-z44T3PBf9W7qQ/aR+NmofOTyg6HLhSEZOPD4zhStqBpLoMp8GYhFksuUBnCxbnf1nfISpKBVkQhiBLFI/F4Wlg==",
+      "version": "0.24.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.24.0.tgz",
+      "integrity": "sha512-87zp50mbbNrSTuoEOebdRQBPa0mdejA5UEjyuScyIw8hEpEjfWP89Qhkq5xVZfVyVSRQKZc9alVm7yRKQvvUmg==",
       "dev": true,
       "engines": {
-        "node": ">=14"
+        "node": ">=16"
       },
       "peerDependencies": {
         "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -5391,9 +5427,9 @@
       }
     },
     "node_modules/fastq": {
-      "version": "1.17.0",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz",
-      "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==",
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
       "dependencies": {
         "reusify": "^1.0.4"
       }
@@ -5648,16 +5684,20 @@
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
-      "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
       "dev": true,
       "dependencies": {
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
         "has-proto": "^1.0.1",
         "has-symbols": "^1.0.3",
         "hasown": "^2.0.0"
       },
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -5706,13 +5746,14 @@
       }
     },
     "node_modules/get-symbol-description": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
-      "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+      "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.1"
+        "call-bind": "^1.0.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5912,9 +5953,9 @@
       "dev": true
     },
     "node_modules/gsap": {
-      "version": "3.12.2",
-      "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.2.tgz",
-      "integrity": "sha512-EkYnpG8qHgYBFAwsgsGEqvT1WUidX0tt/ijepx7z8EUJHElykg91RvW1XbkT59T0gZzzszOpjQv7SE41XuIXyQ=="
+      "version": "3.12.5",
+      "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
+      "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ=="
     },
     "node_modules/hammerjs": {
       "version": "2.0.8",
@@ -5942,12 +5983,12 @@
       }
     },
     "node_modules/has-property-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
-      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.2"
+        "es-define-property": "^1.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -5978,12 +6019,12 @@
       }
     },
     "node_modules/has-tostringtag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
-      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
       "dev": true,
       "dependencies": {
-        "has-symbols": "^1.0.2"
+        "has-symbols": "^1.0.3"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5998,9 +6039,9 @@
       "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="
     },
     "node_modules/hasown": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
-      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
+      "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
       "dependencies": {
         "function-bind": "^1.1.2"
       },
@@ -6077,9 +6118,9 @@
       "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA=="
     },
     "node_modules/http-proxy-agent": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
-      "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
       "dev": true,
       "dependencies": {
         "agent-base": "^7.1.0",
@@ -6090,9 +6131,9 @@
       }
     },
     "node_modules/https-proxy-agent": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
-      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
+      "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
       "dev": true,
       "dependencies": {
         "agent-base": "^7.0.2",
@@ -6153,9 +6194,9 @@
       ]
     },
     "node_modules/ignore": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
-      "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
       "dev": true,
       "engines": {
         "node": ">= 4"
@@ -6247,12 +6288,12 @@
       }
     },
     "node_modules/internal-slot": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
-      "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+      "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.2",
+        "es-errors": "^1.3.0",
         "hasown": "^2.0.0",
         "side-channel": "^1.0.4"
       },
@@ -6277,14 +6318,16 @@
       }
     },
     "node_modules/is-array-buffer": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
-      "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+      "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.0",
-        "is-typed-array": "^1.1.10"
+        "get-intrinsic": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -6582,12 +6625,12 @@
       }
     },
     "node_modules/is-typed-array": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
-      "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+      "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
       "dev": true,
       "dependencies": {
-        "which-typed-array": "^1.1.11"
+        "which-typed-array": "^1.1.14"
       },
       "engines": {
         "node": ">= 0.4"
@@ -6693,9 +6736,9 @@
       "dev": true
     },
     "node_modules/js-tokens": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.2.tgz",
-      "integrity": "sha512-Olnt+V7xYdvGze9YTbGFZIfQXuGV4R3nQwwl8BrtgaPE/wq8UFpUHWuTNc05saowhSr1ZO6tx+V6RjE9D5YQog==",
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz",
+      "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==",
       "dev": true
     },
     "node_modules/js-types": {
@@ -7344,9 +7387,9 @@
       "dev": true
     },
     "node_modules/meow": {
-      "version": "13.1.0",
-      "resolved": "https://registry.npmjs.org/meow/-/meow-13.1.0.tgz",
-      "integrity": "sha512-o5R/R3Tzxq0PJ3v3qcQJtSvSE9nKOLSAaDuuoMzDVuGTwHdccMWcYomh9Xolng2tjT6O/Y83d+0coVGof6tqmA==",
+      "version": "13.2.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+      "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
       "dev": true,
       "engines": {
         "node": ">=18"
@@ -8421,9 +8464,9 @@
       }
     },
     "node_modules/pdfobject": {
-      "version": "2.2.12",
-      "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.2.12.tgz",
-      "integrity": "sha512-D0oyD/sj8j82AMaJhoyMaY1aD5TkbpU3FbJC6w9/cpJlZRpYHqAkutXw1Ca/FKjYPZmTAu58uGIfgOEaDlbY8A=="
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.3.0.tgz",
+      "integrity": "sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA=="
     },
     "node_modules/picocolors": {
       "version": "1.0.0",
@@ -8566,9 +8609,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.33",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
-      "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+      "version": "8.4.35",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
+      "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
       "funding": [
         {
           "type": "opencollective",
@@ -9025,14 +9068,15 @@
       }
     },
     "node_modules/regexp.prototype.flags": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
-      "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+      "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "set-function-name": "^2.0.0"
+        "call-bind": "^1.0.6",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "set-function-name": "^2.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -9287,13 +9331,13 @@
       ]
     },
     "node_modules/safe-regex-test": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
-      "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+      "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
-        "get-intrinsic": "^1.2.2",
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
         "is-regex": "^1.1.4"
       },
       "engines": {
@@ -9365,9 +9409,9 @@
       }
     },
     "node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
       "dependencies": {
         "lru-cache": "^6.0.0"
       },
@@ -9406,14 +9450,15 @@
       }
     },
     "node_modules/set-function-length": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
-      "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
+      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.1.1",
+        "define-data-property": "^1.1.2",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.2",
+        "get-intrinsic": "^1.2.3",
         "gopd": "^1.0.1",
         "has-property-descriptors": "^1.0.1"
       },
@@ -9466,14 +9511,18 @@
       }
     },
     "node_modules/side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
+      "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
+        "object-inspect": "^1.13.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -9536,9 +9585,9 @@
       }
     },
     "node_modules/solid-js": {
-      "version": "1.8.12",
-      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.12.tgz",
-      "integrity": "sha512-sLE/i6M9FSWlov3a2pTC5ISzanH2aKwqXTZj+bbFt4SUrVb4iGEa7fpILBMOxsQjkv3eXqEk6JVLlogOdTe0UQ==",
+      "version": "1.8.15",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.15.tgz",
+      "integrity": "sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==",
       "dependencies": {
         "csstype": "^3.1.0",
         "seroval": "^1.0.3",
@@ -9619,9 +9668,9 @@
       }
     },
     "node_modules/spdx-exceptions": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
-      "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw=="
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="
     },
     "node_modules/spdx-expression-parse": {
       "version": "3.0.1",
@@ -9641,9 +9690,9 @@
       }
     },
     "node_modules/spdx-license-ids": {
-      "version": "3.0.16",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
-      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw=="
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
+      "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg=="
     },
     "node_modules/spdx-ranges": {
       "version": "2.1.1",
@@ -10162,9 +10211,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.11.3",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.3.tgz",
-      "integrity": "sha512-vQ+Pe73xt7vMVbX40L6nHu4sDmNCM6A+eMVJPGvKrifHQ4LO3smH0jCiiefKzsVl7OlOcVEnrZ9IFzYwElfMkA=="
+      "version": "5.11.6",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.6.tgz",
+      "integrity": "sha512-K5BpYuMoPpJY7NwCHIWohH6tU9o0fs1+plNT5KJ+3BBlVEh4H1CpeKJV8o91lpscVY9oqb2jmaAassnW3wVoTg=="
     },
     "node_modules/symbol-tree": {
       "version": "3.2.4",
@@ -10209,9 +10258,9 @@
       }
     },
     "node_modules/terser": {
-      "version": "5.27.0",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
-      "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
+      "version": "5.27.1",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz",
+      "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
@@ -10343,9 +10392,9 @@
       }
     },
     "node_modules/tinyspy": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
-      "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
+      "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
@@ -10476,14 +10525,14 @@
       }
     },
     "node_modules/typed-array-buffer": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
-      "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz",
+      "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1",
-        "is-typed-array": "^1.1.10"
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.13"
       },
       "engines": {
         "node": ">= 0.4"
@@ -10555,9 +10604,9 @@
       }
     },
     "node_modules/typo-js": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.3.tgz",
-      "integrity": "sha512-67Hyl94beZX8gmTap7IDPrG5hy2cHftgsCAcGvE1tzuxGT+kRB+zSBin0wIMwysYw8RUCBCvv9UfQl8TNM75dA=="
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz",
+      "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA=="
     },
     "node_modules/uc.micro": {
       "version": "2.0.0",
@@ -10745,13 +10794,13 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.0.12",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
-      "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
+      "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
       "dev": true,
       "dependencies": {
         "esbuild": "^0.19.3",
-        "postcss": "^8.4.32",
+        "postcss": "^8.4.35",
         "rollup": "^4.2.0"
       },
       "bin": {
@@ -10848,9 +10897,9 @@
       }
     },
     "node_modules/vite/node_modules/rollup": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz",
-      "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.11.0.tgz",
+      "integrity": "sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==",
       "dev": true,
       "dependencies": {
         "@types/estree": "1.0.5"
@@ -10863,19 +10912,19 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.9.6",
-        "@rollup/rollup-android-arm64": "4.9.6",
-        "@rollup/rollup-darwin-arm64": "4.9.6",
-        "@rollup/rollup-darwin-x64": "4.9.6",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.9.6",
-        "@rollup/rollup-linux-arm64-gnu": "4.9.6",
-        "@rollup/rollup-linux-arm64-musl": "4.9.6",
-        "@rollup/rollup-linux-riscv64-gnu": "4.9.6",
-        "@rollup/rollup-linux-x64-gnu": "4.9.6",
-        "@rollup/rollup-linux-x64-musl": "4.9.6",
-        "@rollup/rollup-win32-arm64-msvc": "4.9.6",
-        "@rollup/rollup-win32-ia32-msvc": "4.9.6",
-        "@rollup/rollup-win32-x64-msvc": "4.9.6",
+        "@rollup/rollup-android-arm-eabi": "4.11.0",
+        "@rollup/rollup-android-arm64": "4.11.0",
+        "@rollup/rollup-darwin-arm64": "4.11.0",
+        "@rollup/rollup-darwin-x64": "4.11.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.11.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.11.0",
+        "@rollup/rollup-linux-arm64-musl": "4.11.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.11.0",
+        "@rollup/rollup-linux-x64-gnu": "4.11.0",
+        "@rollup/rollup-linux-x64-musl": "4.11.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.11.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.11.0",
+        "@rollup/rollup-win32-x64-msvc": "4.11.0",
         "fsevents": "~2.3.2"
       }
     },
@@ -10958,15 +11007,15 @@
       }
     },
     "node_modules/vue": {
-      "version": "3.4.18",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.18.tgz",
-      "integrity": "sha512-0zLRYamFRe0wF4q2L3O24KQzLyLpL64ye1RUToOgOxuWZsb/FhaNRdGmeozdtVYLz6tl94OXLaK7/WQIrVCw1A==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",
+      "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.18",
-        "@vue/compiler-sfc": "3.4.18",
-        "@vue/runtime-dom": "3.4.18",
-        "@vue/server-renderer": "3.4.18",
-        "@vue/shared": "3.4.18"
+        "@vue/compiler-dom": "3.4.19",
+        "@vue/compiler-sfc": "3.4.19",
+        "@vue/runtime-dom": "3.4.19",
+        "@vue/server-renderer": "3.4.19",
+        "@vue/shared": "3.4.19"
       },
       "peerDependencies": {
         "typescript": "*"
@@ -11100,9 +11149,9 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.90.1",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
-      "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
+      "version": "5.90.2",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz",
+      "integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==",
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^1.0.5",
@@ -11362,16 +11411,16 @@
       }
     },
     "node_modules/which-typed-array": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
-      "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+      "version": "1.1.14",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz",
+      "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==",
       "dev": true,
       "dependencies": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.4",
+        "available-typed-arrays": "^1.0.6",
+        "call-bind": "^1.0.5",
         "for-each": "^0.3.3",
         "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.0"
+        "has-tostringtag": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
diff --git a/package.json b/package.json
index dbb57b1624..ac79741711 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,8 @@
     "@webcomponents/custom-elements": "1.6.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
-    "asciinema-player": "3.6.3",
-    "chart.js": "4.3.0",
+    "asciinema-player": "3.6.4",
+    "chart.js": "4.4.1",
     "chartjs-adapter-dayjs-4": "1.0.4",
     "chartjs-plugin-zoom": "2.0.1",
     "clippie": "4.0.6",
@@ -38,22 +38,22 @@
     "minimatch": "9.0.3",
     "monaco-editor": "0.46.0",
     "monaco-editor-webpack-plugin": "7.1.0",
-    "pdfobject": "2.2.12",
+    "pdfobject": "2.3.0",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
-    "swagger-ui-dist": "5.11.3",
+    "swagger-ui-dist": "5.11.6",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
     "tippy.js": "6.3.7",
     "toastify-js": "1.12.0",
     "tributejs": "5.1.3",
     "uint8-to-base64": "0.2.0",
-    "vue": "3.4.18",
+    "vue": "3.4.19",
     "vue-bar-graph": "2.0.0",
     "vue-chartjs": "5.3.0",
     "vue-loader": "17.4.2",
     "vue3-calendar-heatmap": "2.0.5",
-    "webpack": "5.90.1",
+    "webpack": "5.90.2",
     "webpack-cli": "5.1.4",
     "wrap-ansi": "9.0.0"
   },
@@ -61,7 +61,7 @@
     "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
     "@playwright/test": "1.41.2",
     "@stoplight/spectral-cli": "6.11.0",
-    "@stylistic/eslint-plugin-js": "1.6.1",
+    "@stylistic/eslint-plugin-js": "1.6.2",
     "@stylistic/stylelint-plugin": "2.0.0",
     "@vitejs/plugin-vue": "5.0.4",
     "eslint": "8.56.0",
@@ -71,7 +71,7 @@
     "eslint-plugin-no-jquery": "2.7.0",
     "eslint-plugin-no-use-extend-native": "0.5.0",
     "eslint-plugin-regexp": "2.2.0",
-    "eslint-plugin-sonarjs": "0.23.0",
+    "eslint-plugin-sonarjs": "0.24.0",
     "eslint-plugin-unicorn": "51.0.1",
     "eslint-plugin-vitest": "0.3.22",
     "eslint-plugin-vitest-globals": "1.4.0",
diff --git a/poetry.lock b/poetry.lock
index 4897496a40..4cb58c6ef2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -342,13 +342,13 @@ telegram = ["requests"]
 
 [[package]]
 name = "yamllint"
-version = "1.34.0"
+version = "1.35.0"
 description = "A linter for YAML files."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "yamllint-1.34.0-py3-none-any.whl", hash = "sha256:33b813f6ff2ffad2e57a288281098392b85f7463ce1f3d5cd45aa848b916a806"},
-    {file = "yamllint-1.34.0.tar.gz", hash = "sha256:7f0a6a41e8aab3904878da4ae34b6248b6bc74634e0d3a90f0fb2d7e723a3d4f"},
+    {file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"},
+    {file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"},
 ]
 
 [package.dependencies]
@@ -361,4 +361,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "e4ea4301a70487379fce7008493d15c005af3aada7d88fbf0bd3167147ec6502"
+content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece"
diff --git a/pyproject.toml b/pyproject.toml
index eb6d4b2311..bef41d6266 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ python = "^3.8"
 
 [tool.poetry.group.dev.dependencies]
 djlint = "1.34.1"
-yamllint = "1.34.0"
+yamllint = "1.35.0"
 
 [tool.djlint]
 profile="golang"

From e9a1ffba2c294f74d985870e9b7b5b07e9000857 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 16 Feb 2024 03:27:45 +0100
Subject: [PATCH 048/679] Avoid vue warning in dev mode (#29188)

`vue` currently outputs a warning for this undefined variable during
development, which is apparently caused by a bug in `vue-cli`.
Workaround by setting this variable.

Ref: https://github.com/vuejs/vue-cli/pull/7443
Ref: https://stackoverflow.com/a/77765007/808699
---
 webpack.config.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/webpack.config.js b/webpack.config.js
index 16afa0ff9c..8b3b8477c1 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -172,6 +172,7 @@ export default {
     new DefinePlugin({
       __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API
       __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production
+      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // https://github.com/vuejs/vue-cli/pull/7443
     }),
     new VueLoaderPlugin(),
     new MiniCssExtractPlugin({

From c70f65e83bc1876fb368fd117d342573ff18a9e8 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 16 Feb 2024 04:52:25 +0200
Subject: [PATCH 049/679] Auto-update the system status in admin dashboard
 (#29163)

- Refactor the system status list into its own template
- Change the backend to return only the system status if htmx initiated
the request
- `hx-get="{{$.Link}}/system_status`: reuse the backend handler
- `hx-swap="innerHTML"`: replace the `<div>`'s innerHTML (essentially
the new template)
- `hx-trigger="every 5s"`: call every 5 seconds
- `hx-indicator=".divider"`: the `is-loading` class shouldn't be added
to the div during the request, so set it on an element it has no effect
on
- Render "Since Last GC Time" with `<relative-time>`, so we send a
timestamp

# Auto-update in action GIF

![action](https://github.com/go-gitea/gitea/assets/20454870/c6e1f220-f0fb-4460-ac3b-59f315e30e29)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 package-lock.json                  |  6 +++
 package.json                       |  1 +
 routers/web/admin/admin.go         | 26 +++++++-----
 routers/web/web.go                 |  1 +
 templates/admin/dashboard.tmpl     | 66 ++----------------------------
 templates/admin/system_status.tmpl | 62 ++++++++++++++++++++++++++++
 templates/base/head.tmpl           |  2 +-
 web_src/js/htmx.js                 |  3 ++
 webpack.config.js                  |  4 ++
 9 files changed, 97 insertions(+), 74 deletions(-)
 create mode 100644 templates/admin/system_status.tmpl

diff --git a/package-lock.json b/package-lock.json
index 48da8124e0..13f03b8d28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
         "escape-goat": "4.0.0",
         "fast-glob": "3.3.2",
         "htmx.org": "1.9.10",
+        "idiomorph": "0.3.0",
         "jquery": "3.7.1",
         "katex": "0.16.9",
         "license-checker-webpack-plugin": "0.2.1",
@@ -6174,6 +6175,11 @@
         "postcss": "^8.1.0"
       }
     },
+    "node_modules/idiomorph": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz",
+      "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA=="
+    },
     "node_modules/ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
diff --git a/package.json b/package.json
index ac79741711..3d753a567c 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
     "escape-goat": "4.0.0",
     "fast-glob": "3.3.2",
     "htmx.org": "1.9.10",
+    "idiomorph": "0.3.0",
     "jquery": "3.7.1",
     "katex": "0.16.9",
     "license-checker-webpack-plugin": "0.2.1",
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index d31cb1cd25..9fbd429f74 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -28,13 +28,14 @@ import (
 )
 
 const (
-	tplDashboard   base.TplName = "admin/dashboard"
-	tplSelfCheck   base.TplName = "admin/self_check"
-	tplCron        base.TplName = "admin/cron"
-	tplQueue       base.TplName = "admin/queue"
-	tplStacktrace  base.TplName = "admin/stacktrace"
-	tplQueueManage base.TplName = "admin/queue_manage"
-	tplStats       base.TplName = "admin/stats"
+	tplDashboard    base.TplName = "admin/dashboard"
+	tplSystemStatus base.TplName = "admin/system_status"
+	tplSelfCheck    base.TplName = "admin/self_check"
+	tplCron         base.TplName = "admin/cron"
+	tplQueue        base.TplName = "admin/queue"
+	tplStacktrace   base.TplName = "admin/stacktrace"
+	tplQueueManage  base.TplName = "admin/queue_manage"
+	tplStats        base.TplName = "admin/stats"
 )
 
 var sysStatus struct {
@@ -72,7 +73,7 @@ var sysStatus struct {
 
 	// Garbage collector statistics.
 	NextGC       string // next run in HeapAlloc time (bytes)
-	LastGC       string // last run in absolute time (ns)
+	LastGCTime   string // last run time
 	PauseTotalNs string
 	PauseNs      string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
 	NumGC        uint32
@@ -110,7 +111,7 @@ func updateSystemStatus() {
 	sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
 
 	sysStatus.NextGC = base.FileSize(int64(m.NextGC))
-	sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
+	sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339)
 	sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
 	sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
 	sysStatus.NumGC = m.NumGC
@@ -132,7 +133,6 @@ func Dashboard(ctx *context.Context) {
 	ctx.Data["PageIsAdminDashboard"] = true
 	ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx)
 	ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
-	// FIXME: update periodically
 	updateSystemStatus()
 	ctx.Data["SysStatus"] = sysStatus
 	ctx.Data["SSH"] = setting.SSH
@@ -140,6 +140,12 @@ func Dashboard(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplDashboard)
 }
 
+func SystemStatus(ctx *context.Context) {
+	updateSystemStatus()
+	ctx.Data["SysStatus"] = sysStatus
+	ctx.HTML(http.StatusOK, tplSystemStatus)
+}
+
 // DashboardPost run an admin operation
 func DashboardPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.AdminDashboardForm)
diff --git a/routers/web/web.go b/routers/web/web.go
index a6288caaf6..0528b20328 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -677,6 +677,7 @@ func registerRoutes(m *web.Route) {
 	// ***** START: Admin *****
 	m.Group("/admin", func() {
 		m.Get("", admin.Dashboard)
+		m.Get("/system_status", admin.SystemStatus)
 		m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
 
 		m.Get("/self_check", admin.SelfCheck)
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl
index f43b4c5385..8088315f17 100644
--- a/templates/admin/dashboard.tmpl
+++ b/templates/admin/dashboard.tmpl
@@ -75,69 +75,9 @@
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "admin.dashboard.system_status"}}
 		</h4>
-		<div class="ui attached table segment">
-			<dl class="admin-dl-horizontal">
-				<dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt>
-				<dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt>
-				<dd>{{.SysStatus.NumGoroutine}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt>
-				<dd>{{.SysStatus.MemAllocated}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt>
-				<dd>{{.SysStatus.MemTotal}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt>
-				<dd>{{.SysStatus.MemSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt>
-				<dd>{{.SysStatus.Lookups}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt>
-				<dd>{{.SysStatus.MemMallocs}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt>
-				<dd>{{.SysStatus.MemFrees}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt>
-				<dd>{{.SysStatus.HeapAlloc}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt>
-				<dd>{{.SysStatus.HeapSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt>
-				<dd>{{.SysStatus.HeapIdle}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt>
-				<dd>{{.SysStatus.HeapInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt>
-				<dd>{{.SysStatus.HeapReleased}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt>
-				<dd>{{.SysStatus.HeapObjects}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt>
-				<dd>{{.SysStatus.StackInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt>
-				<dd>{{.SysStatus.StackSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt>
-				<dd>{{.SysStatus.MSpanInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt>
-				<dd>{{.SysStatus.MSpanSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt>
-				<dd>{{.SysStatus.MCacheInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt>
-				<dd>{{.SysStatus.MCacheSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt>
-				<dd>{{.SysStatus.BuckHashSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt>
-				<dd>{{.SysStatus.GCSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt>
-				<dd>{{.SysStatus.OtherSys}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt>
-				<dd>{{.SysStatus.NextGC}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt>
-				<dd>{{.SysStatus.LastGC}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt>
-				<dd>{{.SysStatus.PauseTotalNs}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt>
-				<dd>{{.SysStatus.PauseNs}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt>
-				<dd>{{.SysStatus.NumGC}}</dd>
-			</dl>
+		{{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}}
+		<div hx-get="{{$.Link}}/system_status" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".divider" class="ui attached table segment">
+			{{template "admin/system_status" .}}
 		</div>
 	</div>
 {{template "admin/layout_footer" .}}
diff --git a/templates/admin/system_status.tmpl b/templates/admin/system_status.tmpl
new file mode 100644
index 0000000000..7b5c9be6cc
--- /dev/null
+++ b/templates/admin/system_status.tmpl
@@ -0,0 +1,62 @@
+<dl class="admin-dl-horizontal">
+	<dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt>
+	<dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt>
+	<dd>{{.SysStatus.NumGoroutine}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt>
+	<dd>{{.SysStatus.MemAllocated}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt>
+	<dd>{{.SysStatus.MemTotal}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt>
+	<dd>{{.SysStatus.MemSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt>
+	<dd>{{.SysStatus.Lookups}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt>
+	<dd>{{.SysStatus.MemMallocs}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt>
+	<dd>{{.SysStatus.MemFrees}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt>
+	<dd>{{.SysStatus.HeapAlloc}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt>
+	<dd>{{.SysStatus.HeapSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt>
+	<dd>{{.SysStatus.HeapIdle}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt>
+	<dd>{{.SysStatus.HeapInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt>
+	<dd>{{.SysStatus.HeapReleased}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt>
+	<dd>{{.SysStatus.HeapObjects}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt>
+	<dd>{{.SysStatus.StackInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt>
+	<dd>{{.SysStatus.StackSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt>
+	<dd>{{.SysStatus.MSpanInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt>
+	<dd>{{.SysStatus.MSpanSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt>
+	<dd>{{.SysStatus.MCacheInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt>
+	<dd>{{.SysStatus.MCacheSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt>
+	<dd>{{.SysStatus.BuckHashSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt>
+	<dd>{{.SysStatus.GCSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt>
+	<dd>{{.SysStatus.OtherSys}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt>
+	<dd>{{.SysStatus.NextGC}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt>
+	<dd><relative-time format="duration" datetime="{{.SysStatus.LastGCTime}}">{{.SysStatus.LastGCTime}}</relative-time></dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt>
+	<dd>{{.SysStatus.PauseTotalNs}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt>
+	<dd>{{.SysStatus.PauseNs}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt>
+	<dd>{{.SysStatus.NumGC}}</dd>
+</dl>
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index b9c050fdd5..e910bb0cd9 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -29,7 +29,7 @@
 	{{template "base/head_style" .}}
 	{{template "custom/header" .}}
 </head>
-<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-push-url="false">
+<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
 	{{ctx.DataRaceCheck $.Context}}
 	{{template "custom/body_outer_pre" .}}
 
diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js
index 92400d1cbe..5ca3018308 100644
--- a/web_src/js/htmx.js
+++ b/web_src/js/htmx.js
@@ -1,6 +1,9 @@
 import * as htmx from 'htmx.org';
 import {showErrorToast} from './modules/toast.js';
 
+// https://github.com/bigskysoftware/idiomorph#htmx
+import 'idiomorph/dist/idiomorph-ext.js';
+
 // https://htmx.org/reference/#config
 htmx.config.requestClass = 'is-loading';
 htmx.config.scrollIntoViewOnBoost = false;
diff --git a/webpack.config.js b/webpack.config.js
index 8b3b8477c1..82d76d9e8d 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -169,6 +169,9 @@ export default {
     ],
   },
   plugins: [
+    new webpack.ProvidePlugin({ // for htmx extensions
+      htmx: 'htmx.org',
+    }),
     new DefinePlugin({
       __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API
       __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production
@@ -207,6 +210,7 @@ export default {
       override: {
         'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33
         'htmx.org@1.9.10': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause"
+        'idiomorph@0.3.0': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause"
       },
       emitError: true,
       allow: '(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)',

From 69ed1a4afbc9604cabe83041de31752dd5d101ee Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 16 Feb 2024 04:17:34 +0100
Subject: [PATCH 050/679] Disable parallel Make execution (#29186)

Ref:
https://www.gnu.org/software/make/manual/html_node/Parallel-Disable.html

> If the .NOTPARALLEL special target with no prerequisites is specified
anywhere then the entire instance of make will be run serially,
regardless of the parallel setting
---
 Makefile  | 5 +++++
 README.md | 2 --
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile
index da4806d9c4..3065d9e683 100644
--- a/Makefile
+++ b/Makefile
@@ -988,3 +988,8 @@ docker:
 
 # This endif closes the if at the top of the file
 endif
+
+# Disable parallel execution because it would break some targets that don't
+# specify exact dependencies like 'backend' which does currently not depend
+# on 'frontend' to enable Node.js-less builds from source tarballs.
+.NOTPARALLEL:
diff --git a/README.md b/README.md
index 174e37769c..adba74d8bb 100644
--- a/README.md
+++ b/README.md
@@ -89,8 +89,6 @@ The `build` target is split into two sub-targets:
 
 Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
 
-Parallelism (`make -j <num>`) is not supported.
-
 More info: https://docs.gitea.com/installation/install-from-source
 
 ## Using

From 8e2831611c06e84dd8fedf7a0b2cce9f98d4188f Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Fri, 16 Feb 2024 18:50:20 +0900
Subject: [PATCH 051/679] Fix gitea-action user avatar broken on edited menu
 (#29190)

Fix #29178
---
 models/issues/content_history.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/models/issues/content_history.go b/models/issues/content_history.go
index 8c333bc6dd..8b00adda99 100644
--- a/models/issues/content_history.go
+++ b/models/issues/content_history.go
@@ -161,7 +161,11 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6
 	}
 
 	for _, item := range res {
-		item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
+		if item.UserID > 0 {
+			item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
+		} else {
+			item.UserAvatarLink = avatars.DefaultAvatarLink()
+		}
 	}
 	return res, nil
 }

From f2d5c6eddedb75f49af614e362b6f8b1317e3f5d Mon Sep 17 00:00:00 2001
From: wienans <40465543+wienans@users.noreply.github.com>
Date: Fri, 16 Feb 2024 14:22:00 +0100
Subject: [PATCH 052/679] Docker Tag Information in Docs (#29047)

Add more details for the docker tag when using container registry.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 docs/content/usage/packages/container.en-us.md | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/docs/content/usage/packages/container.en-us.md b/docs/content/usage/packages/container.en-us.md
index 6be21c2b27..5676aa36fb 100644
--- a/docs/content/usage/packages/container.en-us.md
+++ b/docs/content/usage/packages/container.en-us.md
@@ -39,6 +39,16 @@ Images must follow this naming convention:
 
 `{registry}/{owner}/{image}`
 
+When building your docker image, using the naming convention above, this looks like:
+
+```shell
+# build an image with tag
+docker build -t {registry}/{owner}/{image}:{tag} .
+# name an existing image with tag
+docker tag {some-existing-image}:{tag} {registry}/{owner}/{image}:{tag}
+```
+
+where your registry is the domain of your gitea instance (e.g. gitea.example.com).
 For example, these are all valid image names for the owner `testuser`:
 
 `gitea.example.com/testuser/myimage`

From c40ee6fb7382bc2d1398dc685f98a0277d3bfb68 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 16 Feb 2024 14:27:00 +0100
Subject: [PATCH 053/679] Refactor request function (#29187)

- Remove and prevent use of `body` argument, it is not used anywhere
- Remove uppercasing of method, we can require it to be uppercase
---
 web_src/js/modules/fetch.js | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/web_src/js/modules/fetch.js b/web_src/js/modules/fetch.js
index b3529d27fc..2191a8d4db 100644
--- a/web_src/js/modules/fetch.js
+++ b/web_src/js/modules/fetch.js
@@ -8,19 +8,17 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
 // fetch wrapper, use below method name functions and the `data` option to pass in data
 // which will automatically set an appropriate headers. For json content, only object
 // and array types are currently supported.
-export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
-  let contentType;
-  if (!body) {
-    if (data instanceof FormData || data instanceof URLSearchParams) {
-      body = data;
-    } else if (isObject(data) || Array.isArray(data)) {
-      contentType = 'application/json';
-      body = JSON.stringify(data);
-    }
+export function request(url, {method = 'GET', data, headers = {}, ...other} = {}) {
+  let body, contentType;
+  if (data instanceof FormData || data instanceof URLSearchParams) {
+    body = data;
+  } else if (isObject(data) || Array.isArray(data)) {
+    contentType = 'application/json';
+    body = JSON.stringify(data);
   }
 
   const headersMerged = new Headers({
-    ...(!safeMethods.has(method.toUpperCase()) && {'x-csrf-token': csrfToken}),
+    ...(!safeMethods.has(method) && {'x-csrf-token': csrfToken}),
     ...(contentType && {'content-type': contentType}),
   });
 
@@ -31,8 +29,8 @@ export function request(url, {method = 'GET', headers = {}, data, body, ...other
   return fetch(url, {
     method,
     headers: headersMerged,
-    ...(body && {body}),
     ...other,
+    ...(body && {body}),
   });
 }
 

From 236e12184404998c8edf7efa6de7fccf9d0ee814 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 16 Feb 2024 15:34:29 +0200
Subject: [PATCH 054/679] Remove jQuery from SSH key form parser (#29193)

- Switched to plain JavaScript
- Tested the SSH key title functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/4785c13d-8d30-448e-b74a-263935e2769f)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/sshkey-helper.js | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/web_src/js/features/sshkey-helper.js b/web_src/js/features/sshkey-helper.js
index 099b54d3a6..3960eefe8e 100644
--- a/web_src/js/features/sshkey-helper.js
+++ b/web_src/js/features/sshkey-helper.js
@@ -1,12 +1,10 @@
-import $ from 'jquery';
-
 export function initSshKeyFormParser() {
-// Parse SSH Key
-  $('#ssh-key-content').on('change paste keyup', function () {
-    const arrays = $(this).val().split(' ');
-    const $title = $('#ssh-key-title');
-    if ($title.val() === '' && arrays.length === 3 && arrays[2] !== '') {
-      $title.val(arrays[2]);
+  // Parse SSH Key
+  document.getElementById('ssh-key-content')?.addEventListener('input', function () {
+    const arrays = this.value.split(' ');
+    const title = document.getElementById('ssh-key-title');
+    if (!title.value && arrays.length === 3 && arrays[2] !== '') {
+      title.value = arrays[2];
     }
   });
 }

From 7132a0ba75d6fe734d9f950f217a5ceb81375328 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 16 Feb 2024 15:59:48 +0200
Subject: [PATCH 055/679] Reference labels by IDs instead of names in `keys`
 settings (#29194)

Here's the spec for the `for` attribute:
https://html.spec.whatwg.org/multipage/forms.html#attr-label-for

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 templates/repo/settings/deploy_keys.tmpl    | 4 ++--
 templates/user/settings/keys_gpg.tmpl       | 4 ++--
 templates/user/settings/keys_principal.tmpl | 2 +-
 templates/user/settings/keys_ssh.tmpl       | 4 ++--
 4 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index c5d2d2a04a..a283150c60 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -18,11 +18,11 @@
 						{{ctx.Locale.Tr "repo.settings.deploy_key_desc"}}
 					</div>
 					<div class="field {{if .Err_Title}}error{{end}}">
-						<label for="title">{{ctx.Locale.Tr "repo.settings.title"}}</label>
+						<label for="ssh-key-title">{{ctx.Locale.Tr "repo.settings.title"}}</label>
 						<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required>
 					</div>
 					<div class="field {{if .Err_Content}}error{{end}}">
-						<label for="content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label>
+						<label for="ssh-key-content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label>
 						<textarea id="ssh-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea>
 					</div>
 					<div class="field">
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index 481d7482b4..c562aaeab0 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -10,7 +10,7 @@
 			{{.CsrfTokenHtml}}
 			<input type="hidden" name="title" value="none">
 			<div class="field {{if .Err_Content}}error{{end}}">
-				<label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label>
+				<label for="gpg-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label>
 				<textarea id="gpg-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea>
 			</div>
 			{{if .Err_Signature}}
@@ -26,7 +26,7 @@
 					</div>
 				</div>
 				<div class="field">
-					<label for="signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
+					<label for="gpg-key-signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
 					<textarea id="gpg-key-signature" name="signature" placeholder="{{ctx.Locale.Tr "settings.key_signature_gpg_placeholder"}}" required>{{.signature}}</textarea>
 				</div>
 			{{end}}
diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl
index 513afc2b61..a7ab12dd78 100644
--- a/templates/user/settings/keys_principal.tmpl
+++ b/templates/user/settings/keys_principal.tmpl
@@ -44,7 +44,7 @@
 			<form class="ui form" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<div class="field {{if .Err_Content}}error{{end}}">
-					<label for="content">{{ctx.Locale.Tr "settings.principal_content"}}</label>
+					<label for="ssh-principal-content">{{ctx.Locale.Tr "settings.principal_content"}}</label>
 					<input id="ssh-principal-content" name="content" value="{{.content}}" autofocus required>
 				</div>
 				<input name="title" type="hidden" value="principal">
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index fc8b70ea28..91e8ccfcfa 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -11,11 +11,11 @@
 		<form class="ui form" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="field {{if .Err_Title}}error{{end}}">
-				<label for="title">{{ctx.Locale.Tr "settings.key_name"}}</label>
+				<label for="ssh-key-title">{{ctx.Locale.Tr "settings.key_name"}}</label>
 				<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required maxlength="50">
 			</div>
 			<div class="field {{if .Err_Content}}error{{end}}">
-				<label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label>
+				<label for="ssh-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label>
 				<textarea id="ssh-key-content" name="content" class="js-quick-submit" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea>
 			</div>
 			<input name="type" type="hidden" value="ssh">

From 45c15387b292c25b5d0572b2eb3f85414156372a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 16 Feb 2024 23:18:30 +0800
Subject: [PATCH 056/679] Refactor JWT secret generating & decoding code
 (#29172)

Old code is not consistent for generating & decoding the JWT secrets.

Now, the callers only need to use 2 consistent functions:
NewJwtSecretWithBase64 and DecodeJwtSecretBase64

And remove a non-common function Base64FixedDecode from util.go
---
 cmd/generate.go                              |  2 +-
 modules/generate/generate.go                 | 24 ++++++++------
 modules/generate/generate_test.go            | 34 ++++++++++++++++++++
 modules/setting/lfs.go                       |  6 ++--
 modules/setting/oauth2.go                    |  7 ++--
 modules/util/util.go                         | 11 -------
 modules/util/util_test.go                    | 14 --------
 routers/install/install.go                   |  2 +-
 services/auth/source/oauth2/jwtsigningkey.go |  3 +-
 9 files changed, 57 insertions(+), 46 deletions(-)
 create mode 100644 modules/generate/generate_test.go

diff --git a/cmd/generate.go b/cmd/generate.go
index 4ab10da22a..90b32ecaf0 100644
--- a/cmd/generate.go
+++ b/cmd/generate.go
@@ -70,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
 }
 
 func runGenerateLfsJwtSecret(c *cli.Context) error {
-	_, jwtSecretBase64, err := generate.NewJwtSecretBase64()
+	_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
 	if err != nil {
 		return err
 	}
diff --git a/modules/generate/generate.go b/modules/generate/generate.go
index ee3c76059b..2d9a3dd902 100644
--- a/modules/generate/generate.go
+++ b/modules/generate/generate.go
@@ -7,6 +7,7 @@ package generate
 import (
 	"crypto/rand"
 	"encoding/base64"
+	"fmt"
 	"io"
 	"time"
 
@@ -38,19 +39,24 @@ func NewInternalToken() (string, error) {
 	return internalToken, nil
 }
 
-// NewJwtSecret generates a new value intended to be used for JWT secrets.
-func NewJwtSecret() ([]byte, error) {
-	bytes := make([]byte, 32)
-	_, err := io.ReadFull(rand.Reader, bytes)
-	if err != nil {
+const defaultJwtSecretLen = 32
+
+// DecodeJwtSecretBase64 decodes a base64 encoded jwt secret into bytes, and check its length
+func DecodeJwtSecretBase64(src string) ([]byte, error) {
+	encoding := base64.RawURLEncoding
+	decoded := make([]byte, encoding.DecodedLen(len(src))+3)
+	if n, err := encoding.Decode(decoded, []byte(src)); err != nil {
 		return nil, err
+	} else if n != defaultJwtSecretLen {
+		return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen)
 	}
-	return bytes, nil
+	return decoded[:defaultJwtSecretLen], nil
 }
 
-// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets.
-func NewJwtSecretBase64() ([]byte, string, error) {
-	bytes, err := NewJwtSecret()
+// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file
+func NewJwtSecretWithBase64() ([]byte, string, error) {
+	bytes := make([]byte, defaultJwtSecretLen)
+	_, err := io.ReadFull(rand.Reader, bytes)
 	if err != nil {
 		return nil, "", err
 	}
diff --git a/modules/generate/generate_test.go b/modules/generate/generate_test.go
new file mode 100644
index 0000000000..af640a60c1
--- /dev/null
+++ b/modules/generate/generate_test.go
@@ -0,0 +1,34 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package generate
+
+import (
+	"encoding/base64"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDecodeJwtSecretBase64(t *testing.T) {
+	_, err := DecodeJwtSecretBase64("abcd")
+	assert.ErrorContains(t, err, "invalid base64 decoded length")
+	_, err = DecodeJwtSecretBase64(strings.Repeat("a", 64))
+	assert.ErrorContains(t, err, "invalid base64 decoded length")
+
+	str32 := strings.Repeat("x", 32)
+	encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
+	decoded32, err := DecodeJwtSecretBase64(encoded32)
+	assert.NoError(t, err)
+	assert.Equal(t, str32, string(decoded32))
+}
+
+func TestNewJwtSecretWithBase64(t *testing.T) {
+	secret, encoded, err := NewJwtSecretWithBase64()
+	assert.NoError(t, err)
+	assert.Len(t, secret, 32)
+	decoded, err := DecodeJwtSecretBase64(encoded)
+	assert.NoError(t, err)
+	assert.Equal(t, secret, decoded)
+}
diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go
index a5ea537cef..22a75f6008 100644
--- a/modules/setting/lfs.go
+++ b/modules/setting/lfs.go
@@ -4,12 +4,10 @@
 package setting
 
 import (
-	"encoding/base64"
 	"fmt"
 	"time"
 
 	"code.gitea.io/gitea/modules/generate"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // LFS represents the configuration for Git LFS
@@ -62,9 +60,9 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
 	}
 
 	LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
-	LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32)
+	LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(LFS.JWTSecretBase64)
 	if err != nil {
-		LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64()
+		LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
 		if err != nil {
 			return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
 		}
diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index 0d15e91ef0..e16e167024 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -4,13 +4,11 @@
 package setting
 
 import (
-	"encoding/base64"
 	"math"
 	"path/filepath"
 
 	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
@@ -137,13 +135,12 @@ func loadOAuth2From(rootCfg ConfigProvider) {
 	}
 
 	if InstallLock {
-		if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil {
-			key, err := generate.NewJwtSecret()
+		if _, err := generate.DecodeJwtSecretBase64(OAuth2.JWTSecretBase64); err != nil {
+			_, OAuth2.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
 			if err != nil {
 				log.Fatal("error generating JWT secret: %v", err)
 			}
 
-			OAuth2.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(key)
 			saveCfg, err := rootCfg.PrepareSaving()
 			if err != nil {
 				log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
diff --git a/modules/util/util.go b/modules/util/util.go
index c47931f6c9..0e5c6a4e64 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -6,7 +6,6 @@ package util
 import (
 	"bytes"
 	"crypto/rand"
-	"encoding/base64"
 	"fmt"
 	"math/big"
 	"strconv"
@@ -246,13 +245,3 @@ func ToFloat64(number any) (float64, error) {
 func ToPointer[T any](val T) *T {
 	return &val
 }
-
-func Base64FixedDecode(encoding *base64.Encoding, src []byte, length int) ([]byte, error) {
-	decoded := make([]byte, encoding.DecodedLen(len(src))+3)
-	if n, err := encoding.Decode(decoded, src); err != nil {
-		return nil, err
-	} else if n != length {
-		return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, length)
-	}
-	return decoded[:length], nil
-}
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 8509d8aced..c5830ce01c 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -4,7 +4,6 @@
 package util
 
 import (
-	"encoding/base64"
 	"regexp"
 	"strings"
 	"testing"
@@ -234,16 +233,3 @@ func TestToPointer(t *testing.T) {
 	val123 := 123
 	assert.False(t, &val123 == ToPointer(val123))
 }
-
-func TestBase64FixedDecode(t *testing.T) {
-	_, err := Base64FixedDecode(base64.RawURLEncoding, []byte("abcd"), 32)
-	assert.ErrorContains(t, err, "invalid base64 decoded length")
-	_, err = Base64FixedDecode(base64.RawURLEncoding, []byte(strings.Repeat("a", 64)), 32)
-	assert.ErrorContains(t, err, "invalid base64 decoded length")
-
-	str32 := strings.Repeat("x", 32)
-	encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
-	decoded32, err := Base64FixedDecode(base64.RawURLEncoding, []byte(encoded32), 32)
-	assert.NoError(t, err)
-	assert.Equal(t, str32, string(decoded32))
-}
diff --git a/routers/install/install.go b/routers/install/install.go
index 5c0290d2cc..064575d34c 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -409,7 +409,7 @@ func SubmitInstall(ctx *context.Context) {
 		cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
 		cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
 		var lfsJwtSecret string
-		if _, lfsJwtSecret, err = generate.NewJwtSecretBase64(); err != nil {
+		if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil {
 			ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
 			return
 		}
diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
index eca0b8b7e1..2afe557b0d 100644
--- a/services/auth/source/oauth2/jwtsigningkey.go
+++ b/services/auth/source/oauth2/jwtsigningkey.go
@@ -18,6 +18,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -336,7 +337,7 @@ func InitSigningKey() error {
 // loadSymmetricKey checks if the configured secret is valid.
 // If it is not valid, it will return an error.
 func loadSymmetricKey() (any, error) {
-	return util.Base64FixedDecode(base64.RawURLEncoding, []byte(setting.OAuth2.JWTSecretBase64), 32)
+	return generate.DecodeJwtSecretBase64(setting.OAuth2.JWTSecretBase64)
 }
 
 // loadOrCreateAsymmetricKey checks if the configured private key exists.

From 5902372e63db2d3f31150251dfffdb305fa9aaee Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 16 Feb 2024 17:48:01 +0200
Subject: [PATCH 057/679] Remove jQuery from organization rename prompt toggle
 (#29195)

- Switched to plain JavaScript
- Tested the organization rename prompt toggling functionality and it
works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/e6f641b0-aa46-4b85-9693-0d608cca855e)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/common-organization.js | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/web_src/js/features/common-organization.js b/web_src/js/features/common-organization.js
index 352e824b05..a950af3adf 100644
--- a/web_src/js/features/common-organization.js
+++ b/web_src/js/features/common-organization.js
@@ -1,14 +1,15 @@
-import $ from 'jquery';
 import {initCompLabelEdit} from './comp/LabelEdit.js';
 import {toggleElem} from '../utils/dom.js';
 
 export function initCommonOrganization() {
-  if ($('.organization').length === 0) {
+  if (!document.querySelectorAll('.organization').length) {
     return;
   }
 
-  $('.organization.settings.options #org_name').on('input', function () {
-    const nameChanged = $(this).val().toLowerCase() !== $(this).attr('data-org-name').toLowerCase();
+  const orgNameInput = document.querySelector('.organization.settings.options #org_name');
+  if (!orgNameInput) return;
+  orgNameInput.addEventListener('input', function () {
+    const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase();
     toggleElem('#org-name-change-prompt', nameChanged);
   });
 

From 0768842ef56758b3290406656c5ebbd605358f6e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 16 Feb 2024 17:52:50 +0200
Subject: [PATCH 058/679] Remove jQuery from username change prompt and fix its
 detection (#29197)

- Switched to plain JavaScript
- Tested the user rename prompt toggling functionality and it works as
before
- Fixed bug that allowed pasting with the mouse to avoid the prompt

# Before

![before](https://github.com/go-gitea/gitea/assets/20454870/aa300ad7-612b-461e-bbb2-3f74b3b83ede)

# After

![after](https://github.com/go-gitea/gitea/assets/20454870/f2b5a51b-7b39-43c7-8a4a-62f1f77acae4)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/user-settings.js | 29 ++++++++++++++--------------
 1 file changed, 15 insertions(+), 14 deletions(-)

diff --git a/web_src/js/features/user-settings.js b/web_src/js/features/user-settings.js
index d49bf39275..0dd908f34a 100644
--- a/web_src/js/features/user-settings.js
+++ b/web_src/js/features/user-settings.js
@@ -1,18 +1,19 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 
 export function initUserSettings() {
-  if ($('.user.settings.profile').length > 0) {
-    $('#username').on('keyup', function () {
-      const $prompt = $('#name-change-prompt');
-      const $prompt_redirect = $('#name-change-redirect-prompt');
-      if ($(this).val().toString().toLowerCase() !== $(this).data('name').toString().toLowerCase()) {
-        showElem($prompt);
-        showElem($prompt_redirect);
-      } else {
-        hideElem($prompt);
-        hideElem($prompt_redirect);
-      }
-    });
-  }
+  if (document.querySelectorAll('.user.settings.profile').length === 0) return;
+
+  const usernameInput = document.getElementById('username');
+  if (!usernameInput) return;
+  usernameInput.addEventListener('input', function () {
+    const prompt = document.getElementById('name-change-prompt');
+    const promptRedirect = document.getElementById('name-change-redirect-prompt');
+    if (this.value.toLowerCase() !== this.getAttribute('data-name').toLowerCase()) {
+      showElem(prompt);
+      showElem(promptRedirect);
+    } else {
+      hideElem(prompt);
+      hideElem(promptRedirect);
+    }
+  });
 }

From 2d8756a9607ee6029ad7a44985e9751988d5fdaa Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 16 Feb 2024 18:03:52 +0200
Subject: [PATCH 059/679] Fix `initCompLabelEdit` not being called (#29198)

Fix broken `if` from https://github.com/go-gitea/gitea/pull/29195

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/common-organization.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/web_src/js/features/common-organization.js b/web_src/js/features/common-organization.js
index a950af3adf..442714a3d6 100644
--- a/web_src/js/features/common-organization.js
+++ b/web_src/js/features/common-organization.js
@@ -6,9 +6,7 @@ export function initCommonOrganization() {
     return;
   }
 
-  const orgNameInput = document.querySelector('.organization.settings.options #org_name');
-  if (!orgNameInput) return;
-  orgNameInput.addEventListener('input', function () {
+  document.querySelector('.organization.settings.options #org_name')?.addEventListener('input', function () {
     const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase();
     toggleElem('#org-name-change-prompt', nameChanged);
   });

From d8d4b33b31d959e4b600cc90a7fa1779b69cadf5 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 16 Feb 2024 22:03:50 +0200
Subject: [PATCH 060/679] Remove jQuery from the "quick submit" handler
 (#29200)

- Switched to plain JavaScript
- Tested the quick submit functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/abbd6c49-ad0f-4f95-b4ba-e969b85a46e8)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/comp/QuickSubmit.js | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/web_src/js/features/comp/QuickSubmit.js b/web_src/js/features/comp/QuickSubmit.js
index 2587375a71..e6d7080bcf 100644
--- a/web_src/js/features/comp/QuickSubmit.js
+++ b/web_src/js/features/comp/QuickSubmit.js
@@ -1,5 +1,3 @@
-import $ from 'jquery';
-
 export function handleGlobalEnterQuickSubmit(target) {
   const form = target.closest('form');
   if (form) {
@@ -8,14 +6,9 @@ export function handleGlobalEnterQuickSubmit(target) {
       return;
     }
 
-    if (form.classList.contains('form-fetch-action')) {
-      form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
-      return;
-    }
-
     // here use the event to trigger the submit event (instead of calling `submit()` method directly)
     // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
-    $(form).trigger('submit');
+    form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
   } else {
     // if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
     // the 'ce-' prefix means this is a CustomEvent

From 26b17537e651fe93ef9b64f961633cb4c0b8c2c3 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 16 Feb 2024 22:41:23 +0100
Subject: [PATCH 061/679] Add `eslint-plugin-github` and fix issues (#29201)

This plugin has a few useful rules. The only thing I dislike about it is
that it pulls in a rather big number of dependencies for react-related
rules we don't use, but it can't really be avoided.

Rule docs:
https://github.com/github/eslint-plugin-github?tab=readme-ov-file#rules
---
 .eslintrc.yaml                         |  24 +
 build/generate-images.js               |   6 +-
 build/generate-svg.js                  |   6 +-
 package-lock.json                      | 954 +++++++++++++++++++++++++
 package.json                           |   1 +
 web_src/js/features/repo-code.js       |   4 +-
 web_src/js/features/repo-issue-list.js |  14 +-
 web_src/js/features/repo-issue.js      |  10 +-
 web_src/js/features/repo-legacy.js     |  45 +-
 9 files changed, 1026 insertions(+), 38 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index ed0309dbea..ab9c218849 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -12,6 +12,7 @@ plugins:
   - "@eslint-community/eslint-plugin-eslint-comments"
   - "@stylistic/eslint-plugin-js"
   - eslint-plugin-array-func
+  - eslint-plugin-github
   - eslint-plugin-i
   - eslint-plugin-jquery
   - eslint-plugin-no-jquery
@@ -209,6 +210,29 @@ rules:
   func-names: [0]
   func-style: [0]
   getter-return: [2]
+  github/a11y-aria-label-is-well-formatted: [0]
+  github/a11y-no-title-attribute: [0]
+  github/a11y-no-visually-hidden-interactive-element: [0]
+  github/a11y-role-supports-aria-props: [0]
+  github/a11y-svg-has-accessible-name: [0]
+  github/array-foreach: [0]
+  github/async-currenttarget: [2]
+  github/async-preventdefault: [2]
+  github/authenticity-token: [0]
+  github/get-attribute: [0]
+  github/js-class-name: [0]
+  github/no-blur: [0]
+  github/no-d-none: [0]
+  github/no-dataset: [2]
+  github/no-dynamic-script-tag: [2]
+  github/no-implicit-buggy-globals: [2]
+  github/no-inner-html: [0]
+  github/no-innerText: [2]
+  github/no-then: [2]
+  github/no-useless-passive: [2]
+  github/prefer-observers: [2]
+  github/require-passive-events: [2]
+  github/unescaped-html-literal: [0]
   grouped-accessor-pairs: [2]
   guard-for-in: [0]
   id-blacklist: [0]
diff --git a/build/generate-images.js b/build/generate-images.js
index a3a0f8d8f3..09e3e068af 100755
--- a/build/generate-images.js
+++ b/build/generate-images.js
@@ -79,4 +79,8 @@ async function main() {
   ]);
 }
 
-main().then(exit).catch(exit);
+try {
+  exit(await main());
+} catch (err) {
+  exit(err);
+}
diff --git a/build/generate-svg.js b/build/generate-svg.js
index b845da9367..2c0a5e37ba 100755
--- a/build/generate-svg.js
+++ b/build/generate-svg.js
@@ -63,4 +63,8 @@ async function main() {
   ]);
 }
 
-main().then(exit).catch(exit);
+try {
+  exit(await main());
+} catch (err) {
+  exit(err);
+}
diff --git a/package-lock.json b/package-lock.json
index 13f03b8d28..f1f8cc4705 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -68,6 +68,7 @@
         "@vitejs/plugin-vue": "5.0.4",
         "eslint": "8.56.0",
         "eslint-plugin-array-func": "4.0.0",
+        "eslint-plugin-github": "4.10.1",
         "eslint-plugin-i": "2.29.1",
         "eslint-plugin-jquery": "1.5.1",
         "eslint-plugin-no-jquery": "2.7.0",
@@ -1022,6 +1023,12 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@github/browserslist-config": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@github/browserslist-config/-/browserslist-config-1.0.0.tgz",
+      "integrity": "sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==",
+      "dev": true
+    },
     "node_modules/@github/combobox-nav": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.3.1.tgz",
@@ -1380,6 +1387,18 @@
         "node": ">=14"
       }
     },
+    "node_modules/@pkgr/core": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
+      "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/unts"
+      }
+    },
     "node_modules/@playwright/test": {
       "version": "1.41.2",
       "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
@@ -2208,6 +2227,12 @@
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
     },
+    "node_modules/@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+      "dev": true
+    },
     "node_modules/@types/marked": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz",
@@ -2271,6 +2296,69 @@
       "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==",
       "dev": true
     },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+      "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/type-utils": "6.21.0",
+        "@typescript-eslint/utils": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+      "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@typescript-eslint/scope-manager": {
       "version": "6.21.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
@@ -2288,6 +2376,33 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+      "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/utils": "6.21.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@typescript-eslint/types": {
       "version": "6.21.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
@@ -2976,6 +3091,15 @@
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
+    "node_modules/aria-query": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+      "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+      "dev": true,
+      "dependencies": {
+        "dequal": "^2.0.3"
+      }
+    },
     "node_modules/array-buffer-byte-length": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
@@ -3000,6 +3124,25 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/array-includes": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
+      "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1",
+        "get-intrinsic": "^1.2.1",
+        "is-string": "^1.0.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -3009,6 +3152,80 @@
         "node": ">=8"
       }
     },
+    "node_modules/array.prototype.filter": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz",
+      "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1",
+        "es-array-method-boxes-properly": "^1.0.0",
+        "is-string": "^1.0.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.findlastindex": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz",
+      "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.22.3",
+        "es-errors": "^1.3.0",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flat": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+      "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1",
+        "es-shim-unscopables": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flatmap": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+      "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1",
+        "es-shim-unscopables": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/arraybuffer.prototype.slice": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
@@ -3070,6 +3287,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/ast-types-flow": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+      "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+      "dev": true
+    },
     "node_modules/astral-regex": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -3088,6 +3311,15 @@
         "astring": "bin/astring"
       }
     },
+    "node_modules/asynciterator.prototype": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
+      "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==",
+      "dev": true,
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      }
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3118,6 +3350,24 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/axe-core": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
+      "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/axobject-query": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
+      "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
+      "dev": true,
+      "dependencies": {
+        "dequal": "^2.0.3"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -4243,6 +4493,12 @@
         "lodash-es": "^4.17.21"
       }
     },
+    "node_modules/damerau-levenshtein": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+      "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+      "dev": true
+    },
     "node_modules/data-uri-to-buffer": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
@@ -4685,6 +4941,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/es-array-method-boxes-properly": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+      "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+      "dev": true
+    },
     "node_modules/es-define-property": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -4706,6 +4968,32 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/es-iterator-helpers": {
+      "version": "1.0.17",
+      "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz",
+      "integrity": "sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==",
+      "dev": true,
+      "dependencies": {
+        "asynciterator.prototype": "^1.0.0",
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.22.4",
+        "es-errors": "^1.3.0",
+        "es-set-tostringtag": "^2.0.2",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "globalthis": "^1.0.3",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "internal-slot": "^1.0.7",
+        "iterator.prototype": "^1.1.2",
+        "safe-array-concat": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es-module-lexer": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
@@ -4725,6 +5013,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/es-shim-unscopables": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+      "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+      "dev": true,
+      "dependencies": {
+        "hasown": "^2.0.0"
+      }
+    },
     "node_modules/es-to-primitive": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
@@ -4897,6 +5194,18 @@
         "eslint": ">=6.0.0"
       }
     },
+    "node_modules/eslint-config-prettier": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+      "dev": true,
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
     "node_modules/eslint-import-resolver-node": {
       "version": "0.3.9",
       "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@@ -4955,6 +5264,92 @@
         "eslint": ">=8.40.0"
       }
     },
+    "node_modules/eslint-plugin-escompat": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-escompat/-/eslint-plugin-escompat-3.4.0.tgz",
+      "integrity": "sha512-ufTPv8cwCxTNoLnTZBFTQ5SxU2w7E7wiMIS7PSxsgP1eAxFjtSaoZ80LRn64hI8iYziE6kJG6gX/ZCJVxh48Bg==",
+      "dev": true,
+      "dependencies": {
+        "browserslist": "^4.21.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=5.14.1"
+      }
+    },
+    "node_modules/eslint-plugin-eslint-comments": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz",
+      "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==",
+      "dev": true,
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5",
+        "ignore": "^5.0.5"
+      },
+      "engines": {
+        "node": ">=6.5.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      },
+      "peerDependencies": {
+        "eslint": ">=4.19.1"
+      }
+    },
+    "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/eslint-plugin-filenames": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz",
+      "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==",
+      "dev": true,
+      "dependencies": {
+        "lodash.camelcase": "4.3.0",
+        "lodash.kebabcase": "4.1.1",
+        "lodash.snakecase": "4.1.1",
+        "lodash.upperfirst": "4.3.1"
+      },
+      "peerDependencies": {
+        "eslint": "*"
+      }
+    },
+    "node_modules/eslint-plugin-github": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.1.tgz",
+      "integrity": "sha512-1AqQBockOM+m0ZUpwfjWtX0lWdX5cRi/hwJnSNvXoOmz/Hh+ULH6QFz6ENWueTWjoWpgPv0af3bj+snps6o4og==",
+      "dev": true,
+      "dependencies": {
+        "@github/browserslist-config": "^1.0.0",
+        "@typescript-eslint/eslint-plugin": "^6.0.0",
+        "@typescript-eslint/parser": "^6.0.0",
+        "aria-query": "^5.3.0",
+        "eslint-config-prettier": ">=8.0.0",
+        "eslint-plugin-escompat": "^3.3.3",
+        "eslint-plugin-eslint-comments": "^3.2.0",
+        "eslint-plugin-filenames": "^1.3.2",
+        "eslint-plugin-i18n-text": "^1.0.1",
+        "eslint-plugin-import": "^2.25.2",
+        "eslint-plugin-jsx-a11y": "^6.7.1",
+        "eslint-plugin-no-only-tests": "^3.0.0",
+        "eslint-plugin-prettier": "^5.0.0",
+        "eslint-rule-documentation": ">=1.0.0",
+        "jsx-ast-utils": "^3.3.2",
+        "prettier": "^3.0.0",
+        "svg-element-attributes": "^1.3.1"
+      },
+      "bin": {
+        "eslint-ignore-errors": "bin/eslint-ignore-errors.js"
+      },
+      "peerDependencies": {
+        "eslint": "^8.0.1"
+      }
+    },
     "node_modules/eslint-plugin-i": {
       "version": "2.29.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-i/-/eslint-plugin-i-2.29.1.tgz",
@@ -5002,6 +5397,98 @@
         "node": "*"
       }
     },
+    "node_modules/eslint-plugin-i18n-text": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz",
+      "integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==",
+      "dev": true,
+      "peerDependencies": {
+        "eslint": ">=5.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-import": {
+      "version": "2.29.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
+      "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
+      "dev": true,
+      "dependencies": {
+        "array-includes": "^3.1.7",
+        "array.prototype.findlastindex": "^1.2.3",
+        "array.prototype.flat": "^1.3.2",
+        "array.prototype.flatmap": "^1.3.2",
+        "debug": "^3.2.7",
+        "doctrine": "^2.1.0",
+        "eslint-import-resolver-node": "^0.3.9",
+        "eslint-module-utils": "^2.8.0",
+        "hasown": "^2.0.0",
+        "is-core-module": "^2.13.1",
+        "is-glob": "^4.0.3",
+        "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.7",
+        "object.groupby": "^1.0.1",
+        "object.values": "^1.1.7",
+        "semver": "^6.3.1",
+        "tsconfig-paths": "^3.15.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dev": true,
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/eslint-plugin-jquery": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-jquery/-/eslint-plugin-jquery-1.5.1.tgz",
@@ -5011,6 +5498,64 @@
         "eslint": ">=5.4.0"
       }
     },
+    "node_modules/eslint-plugin-jsx-a11y": {
+      "version": "6.8.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
+      "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "aria-query": "^5.3.0",
+        "array-includes": "^3.1.7",
+        "array.prototype.flatmap": "^1.3.2",
+        "ast-types-flow": "^0.0.8",
+        "axe-core": "=4.7.0",
+        "axobject-query": "^3.2.1",
+        "damerau-levenshtein": "^1.0.8",
+        "emoji-regex": "^9.2.2",
+        "es-iterator-helpers": "^1.0.15",
+        "hasown": "^2.0.0",
+        "jsx-ast-utils": "^3.3.5",
+        "language-tags": "^1.0.9",
+        "minimatch": "^3.1.2",
+        "object.entries": "^1.1.7",
+        "object.fromentries": "^2.0.7"
+      },
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependencies": {
+        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true
+    },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/eslint-plugin-no-jquery": {
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz",
@@ -5020,6 +5565,15 @@
         "eslint": ">=2.3.0"
       }
     },
+    "node_modules/eslint-plugin-no-only-tests": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.1.0.tgz",
+      "integrity": "sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==",
+      "dev": true,
+      "engines": {
+        "node": ">=5.0.0"
+      }
+    },
     "node_modules/eslint-plugin-no-use-extend-native": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-no-use-extend-native/-/eslint-plugin-no-use-extend-native-0.5.0.tgz",
@@ -5035,6 +5589,36 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/eslint-plugin-prettier": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
+      "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
+      "dev": true,
+      "dependencies": {
+        "prettier-linter-helpers": "^1.0.0",
+        "synckit": "^0.8.6"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint-plugin-prettier"
+      },
+      "peerDependencies": {
+        "@types/eslint": ">=8.0.0",
+        "eslint": ">=8.0.0",
+        "eslint-config-prettier": "*",
+        "prettier": ">=3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/eslint": {
+          "optional": true
+        },
+        "eslint-config-prettier": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/eslint-plugin-regexp": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.2.0.tgz",
@@ -5191,6 +5775,15 @@
         "eslint": ">=5"
       }
     },
+    "node_modules/eslint-rule-documentation": {
+      "version": "1.0.23",
+      "resolved": "https://registry.npmjs.org/eslint-rule-documentation/-/eslint-rule-documentation-1.0.23.tgz",
+      "integrity": "sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
     "node_modules/eslint-scope": {
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -6345,6 +6938,21 @@
       "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
       "dev": true
     },
+    "node_modules/is-async-function": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+      "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+      "dev": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-bigint": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
@@ -6434,6 +7042,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-finalizationregistry": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
+      "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-fullwidth-code-point": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -6442,6 +7062,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-generator-function": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+      "dev": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-get-set-prop": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-get-set-prop/-/is-get-set-prop-1.0.0.tgz",
@@ -6472,6 +7107,15 @@
         "js-types": "^1.0.0"
       }
     },
+    "node_modules/is-map": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
+      "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-negative-zero": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -6576,6 +7220,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-set": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
+      "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-shared-array-buffer": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
@@ -6654,6 +7307,15 @@
         "is-potential-custom-element-name": "^1.0.0"
       }
     },
+    "node_modules/is-weakmap": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
+      "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-weakref": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -6666,6 +7328,19 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-weakset": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
+      "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "get-intrinsic": "^1.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/isarray": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -6685,6 +7360,19 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/iterator.prototype": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
+      "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
+      "dev": true,
+      "dependencies": {
+        "define-properties": "^1.2.1",
+        "get-intrinsic": "^1.2.1",
+        "has-symbols": "^1.0.3",
+        "reflect.getprototypeof": "^1.0.4",
+        "set-function-name": "^2.0.1"
+      }
+    },
     "node_modules/jackspeak": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@@ -6915,6 +7603,21 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/jsx-ast-utils": {
+      "version": "3.3.5",
+      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+      "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+      "dev": true,
+      "dependencies": {
+        "array-includes": "^3.1.6",
+        "array.prototype.flat": "^1.3.1",
+        "object.assign": "^4.1.4",
+        "object.values": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
     "node_modules/just-extend": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
@@ -6971,6 +7674,24 @@
       "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
       "dev": true
     },
+    "node_modules/language-subtag-registry": {
+      "version": "0.3.22",
+      "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+      "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+      "dev": true
+    },
+    "node_modules/language-tags": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+      "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+      "dev": true,
+      "dependencies": {
+        "language-subtag-registry": "^0.3.20"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/layout-base": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
@@ -7140,12 +7861,30 @@
       "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
       "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA=="
     },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "dev": true
+    },
+    "node_modules/lodash.kebabcase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+      "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+      "dev": true
+    },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
       "dev": true
     },
+    "node_modules/lodash.snakecase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+      "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+      "dev": true
+    },
     "node_modules/lodash.sortedlastindex": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/lodash.sortedlastindex/-/lodash.sortedlastindex-4.1.0.tgz",
@@ -7181,6 +7920,12 @@
       "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
       "dev": true
     },
+    "node_modules/lodash.upperfirst": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+      "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+      "dev": true
+    },
     "node_modules/loupe": {
       "version": "2.3.7",
       "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
@@ -8260,6 +9005,67 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/object.entries": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz",
+      "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.fromentries": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
+      "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.groupby": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz",
+      "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==",
+      "dev": true,
+      "dependencies": {
+        "array.prototype.filter": "^1.0.3",
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.22.3",
+        "es-errors": "^1.0.0"
+      }
+    },
+    "node_modules/object.values": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
+      "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -8804,6 +9610,33 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/prettier": {
+      "version": "3.2.5",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+      "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "dependencies": {
+        "fast-diff": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/pretty-format": {
       "version": "29.7.0",
       "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -9046,6 +9879,27 @@
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
       }
     },
+    "node_modules/reflect.getprototypeof": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz",
+      "integrity": "sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.22.3",
+        "es-errors": "^1.0.0",
+        "get-intrinsic": "^1.2.3",
+        "globalthis": "^1.0.3",
+        "which-builtin-type": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/regenerator-runtime": {
       "version": "0.14.1",
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -9834,6 +10688,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/strip-final-newline": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -10176,6 +11039,16 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/svg-element-attributes": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/svg-element-attributes/-/svg-element-attributes-1.3.1.tgz",
+      "integrity": "sha512-Bh05dSOnJBf3miNMqpsormfNtfidA/GxQVakhtn0T4DECWKeXQRQUceYjJ+OxYiiLdGe4Jo9iFV8wICFapFeIA==",
+      "dev": true,
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/svg-tags": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
@@ -10239,6 +11112,22 @@
         "node": ">=14"
       }
     },
+    "node_modules/synckit": {
+      "version": "0.8.8",
+      "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
+      "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
+      "dev": true,
+      "dependencies": {
+        "@pkgr/core": "^0.1.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/unts"
+      }
+    },
     "node_modules/table": {
       "version": "6.8.1",
       "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@@ -10491,6 +11380,30 @@
         "node": ">=6.10"
       }
     },
+    "node_modules/tsconfig-paths": {
+      "version": "3.15.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+      "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "node_modules/tsconfig-paths/node_modules/json5": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
+    },
     "node_modules/tslib": {
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -11416,6 +12329,47 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/which-builtin-type": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
+      "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
+      "dev": true,
+      "dependencies": {
+        "function.prototype.name": "^1.1.5",
+        "has-tostringtag": "^1.0.0",
+        "is-async-function": "^2.0.0",
+        "is-date-object": "^1.0.5",
+        "is-finalizationregistry": "^1.0.2",
+        "is-generator-function": "^1.0.10",
+        "is-regex": "^1.1.4",
+        "is-weakref": "^1.0.2",
+        "isarray": "^2.0.5",
+        "which-boxed-primitive": "^1.0.2",
+        "which-collection": "^1.0.1",
+        "which-typed-array": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-collection": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
+      "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
+      "dev": true,
+      "dependencies": {
+        "is-map": "^2.0.1",
+        "is-set": "^2.0.1",
+        "is-weakmap": "^2.0.1",
+        "is-weakset": "^2.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/which-typed-array": {
       "version": "1.1.14",
       "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz",
diff --git a/package.json b/package.json
index 3d753a567c..fdea78ca29 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
     "@vitejs/plugin-vue": "5.0.4",
     "eslint": "8.56.0",
     "eslint-plugin-array-func": "4.0.0",
+    "eslint-plugin-github": "4.10.1",
     "eslint-plugin-i": "2.29.1",
     "eslint-plugin-jquery": "1.5.1",
     "eslint-plugin-no-jquery": "2.7.0",
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index 306f38829f..a142313211 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -194,7 +194,7 @@ export function initRepoCodeView() {
     const blob = await $.get(`${url}?${query}&anchor=${anchor}`);
     currentTarget.closest('tr').outerHTML = blob;
   });
-  $(document).on('click', '.copy-line-permalink', async (e) => {
-    await clippie(toAbsoluteUrl(e.currentTarget.getAttribute('data-url')));
+  $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => {
+    await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url')));
   });
 }
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index ca20cfbe38..efc7671204 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -69,16 +69,12 @@ function initRepoIssueListCheckboxes() {
       }
     }
 
-    updateIssuesMeta(
-      url,
-      action,
-      issueIDs,
-      elementId,
-    ).then(() => {
+    try {
+      await updateIssuesMeta(url, action, issueIDs, elementId);
       window.location.reload();
-    }).catch((reason) => {
-      showErrorToast(reason.responseJSON.error);
-    });
+    } catch (err) {
+      showErrorToast(err.responseJSON?.error ?? err.message);
+    }
   });
 }
 
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 6908e0c912..3437565c80 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -344,19 +344,15 @@ export async function updateIssuesMeta(url, action, issueIds, elementId) {
 export function initRepoIssueComments() {
   if ($('.repository.view.issue .timeline').length === 0) return;
 
-  $('.re-request-review').on('click', function (e) {
+  $('.re-request-review').on('click', async function (e) {
     e.preventDefault();
     const url = $(this).data('update-url');
     const issueId = $(this).data('issue-id');
     const id = $(this).data('id');
     const isChecked = $(this).hasClass('checked');
 
-    updateIssuesMeta(
-      url,
-      isChecked ? 'detach' : 'attach',
-      issueId,
-      id,
-    ).then(() => window.location.reload());
+    await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
+    window.location.reload();
   });
 
   $(document).on('click', (event) => {
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 08fe21190a..ce1bff11a2 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -205,12 +205,15 @@ export function initRepoCommentForm() {
     $listMenu.find('.no-select.item').on('click', function (e) {
       e.preventDefault();
       if (hasUpdateAction) {
-        updateIssuesMeta(
-          $listMenu.data('update-url'),
-          'clear',
-          $listMenu.data('issue-id'),
-          '',
-        ).then(reloadConfirmDraftComment);
+        (async () => {
+          await updateIssuesMeta(
+            $listMenu.data('update-url'),
+            'clear',
+            $listMenu.data('issue-id'),
+            '',
+          );
+          reloadConfirmDraftComment();
+        })();
       }
 
       $(this).parent().find('.item').each(function () {
@@ -248,12 +251,15 @@ export function initRepoCommentForm() {
 
       $(this).addClass('selected active');
       if (hasUpdateAction) {
-        updateIssuesMeta(
-          $menu.data('update-url'),
-          '',
-          $menu.data('issue-id'),
-          $(this).data('id'),
-        ).then(reloadConfirmDraftComment);
+        (async () => {
+          await updateIssuesMeta(
+            $menu.data('update-url'),
+            '',
+            $menu.data('issue-id'),
+            $(this).data('id'),
+          );
+          reloadConfirmDraftComment();
+        })();
       }
 
       let icon = '';
@@ -281,12 +287,15 @@ export function initRepoCommentForm() {
       });
 
       if (hasUpdateAction) {
-        updateIssuesMeta(
-          $menu.data('update-url'),
-          '',
-          $menu.data('issue-id'),
-          $(this).data('id'),
-        ).then(reloadConfirmDraftComment);
+        (async () => {
+          await updateIssuesMeta(
+            $menu.data('update-url'),
+            '',
+            $menu.data('issue-id'),
+            $(this).data('id'),
+          );
+          reloadConfirmDraftComment();
+        })();
       }
 
       $list.find('.selected').html('');

From e936d2b338859c527482d1569c92d1f8f97f4d51 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Sat, 17 Feb 2024 00:23:24 +0000
Subject: [PATCH 062/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_el-GR.ini | 50 +++++++++++++++++++++++
 options/locale/locale_tr-TR.ini | 71 +++++++++++++++++++++++++++++++++
 2 files changed, 121 insertions(+)

diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 5164217616..749a2ae403 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -381,6 +381,7 @@ email_not_associate=Η διεύθυνση ηλεκτρονικού ταχυδρ
 send_reset_mail=Αποστολή Email Ανάκτησης Λογαριασμού
 reset_password=Ανάκτηση Λογαριασμού
 invalid_code=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έχει λήξει.
+invalid_code_forgot_password=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έληξε. Πατήστε <a href="%s">εδώ</a> για να ξεκινήσετε νέα συνεδρία.
 invalid_password=Ο κωδικός πρόσβασης σας δεν ταιριάζει με τον κωδικό που χρησιμοποιήθηκε για τη δημιουργία του λογαριασμού.
 reset_password_helper=Ανάκτηση Λογαριασμού
 reset_password_wrong_user=Έχετε συνδεθεί ως %s, αλλά ο σύνδεσμος ανάκτησης λογαριασμού προορίζεται για το %s
@@ -864,10 +865,12 @@ revoke_oauth2_grant_description=Η ανάκληση πρόσβασης για α
 revoke_oauth2_grant_success=Η πρόσβαση ανακλήθηκε επιτυχώς.
 
 twofa_desc=Ο έλεγχος ταυτότητας δύο παραγόντων ενισχύει την ασφάλεια του λογαριασμού σας.
+twofa_recovery_tip=Αν χάσετε τη συσκευή σας, θα είστε σε θέση να χρησιμοποιήσετε ένα κλειδί ανάκτησης μιας χρήσης για να ανακτήσετε την πρόσβαση στο λογαριασμό σας.
 twofa_is_enrolled=Ο λογαριασμός σας είναι <strong>εγγεγραμμένος</strong> σε έλεγχο ταυτότητας δύο παραγόντων.
 twofa_not_enrolled=Ο λογαριασμός σας δεν είναι εγγεγραμμένος σε έλεγχο ταυτότητας δύο παραγόντων.
 twofa_disable=Απενεργοποίηση Ταυτοποίησης Δύο Παραμέτρων
 twofa_scratch_token_regenerate=Αναδημιουργία Διακριτικού Μίας Χρήσης
+twofa_scratch_token_regenerated=Το κλειδί ανάκτησης μιας χρήσης είναι τώρα %s. Αποθηκεύστε το σε ασφαλές μέρος, καθώς δε θα εμφανιστεί ξανά.
 twofa_enroll=Εγγραφή στην ταυτοποίηση δύο παραγόντων
 twofa_disable_note=Μπορείτε να απενεργοποιήσετε την ταυτοποίηση δύο παραγόντων αν χρειαστεί.
 twofa_disable_desc=Η απενεργοποίηση της ταυτοποίησης δύο παραγόντων θα καταστήσει τον λογαριασμό σας λιγότερο ασφαλή. Συνέχεια;
@@ -885,6 +888,8 @@ webauthn_register_key=Προσθήκη Κλειδιού Ασφαλείας
 webauthn_nickname=Ψευδώνυμο
 webauthn_delete_key=Αφαίρεση Κλειδιού Ασφαλείας
 webauthn_delete_key_desc=Αν αφαιρέσετε ένα κλειδί ασφαλείας δεν μπορείτε πλέον να συνδεθείτε με αυτό. Συνέχεια;
+webauthn_key_loss_warning=Αν χάσετε τα κλειδιά ασφαλείας σας, θα χάσετε την πρόσβαση στο λογαριασμό σας.
+webauthn_alternative_tip=Μπορεί να θέλετε να ρυθμίσετε μια πρόσθετη μέθοδο ταυτοποίησης.
 
 manage_account_links=Διαχείριση Συνδεδεμένων Λογαριασμών
 manage_account_links_desc=Αυτοί οι εξωτερικοί λογαριασμοί είναι συνδεδεμένοι στον Gitea λογαριασμό σας.
@@ -894,6 +899,7 @@ remove_account_link=Αφαίρεση Συνδεδεμένου Λογαριασμ
 remove_account_link_desc=Η κατάργηση ενός συνδεδεμένου λογαριασμού θα ανακαλέσει την πρόσβασή του στο λογαριασμό σας στο Gitea. Συνέχεια;
 remove_account_link_success=Ο συνδεδεμένος λογαριασμός έχει αφαιρεθεί.
 
+hooks.desc=Προσθήκη webhooks που θα ενεργοποιούνται για <strong>όλα τα αποθετήρια</strong> που σας ανήκουν.
 
 orgs_none=Δεν είστε μέλος σε κάποιο οργανισμό.
 repos_none=Δεν κατέχετε κάποιο αποθετήριο.
@@ -915,9 +921,12 @@ visibility=Ορατότητα χρήστη
 visibility.public=Δημόσια
 visibility.public_tooltip=Ορατό σε όλους
 visibility.limited=Περιορισμένη
+visibility.limited_tooltip=Ορατό μόνο στους ταυτοποιημένους χρήστες
 visibility.private=Ιδιωτική
+visibility.private_tooltip=Ορατό μόνο στα μέλη των οργανισμών που συμμετέχετε
 
 [repo]
+new_repo_helper=Ένα αποθετήριο περιέχει όλα τα αρχεία έργου, συμπεριλαμβανομένου του ιστορικού εκδόσεων. Ήδη φιλοξενείται αλλού; <a href="%s">Μετεγκατάσταση αποθετηρίου.</a>
 owner=Ιδιοκτήτης
 owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων.
 repo_name=Όνομα αποθετηρίου
@@ -940,6 +949,7 @@ fork_to_different_account=Fork σε διαφορετικό λογαριασμό
 fork_visibility_helper=Η ορατότητα ενός fork αποθετηρίου δεν μπορεί να αλλάξει.
 fork_branch=Κλάδος που θα κλωνοποιηθεί στο fork
 all_branches=Όλοι οι κλάδοι
+fork_no_valid_owners=Αυτό το αποθετήριο δεν μπορεί να γίνει fork επειδή δεν υπάρχουν έγκυροι ιδιοκτήτες.
 use_template=Χρήση αυτού του πρότυπου
 clone_in_vsc=Κλωνοποίηση στο VS Code
 download_zip=Λήψη ZIP
@@ -1004,13 +1014,20 @@ delete_preexisting=Διαγραφή αρχείων που προϋπήρχαν
 delete_preexisting_content=Διαγραφή αρχείων στο %s
 delete_preexisting_success=Διαγράφηκαν τα μη υιοθετημένα αρχεία στο %s
 blame_prior=Προβολή ευθύνης πριν από αυτή την αλλαγή
+blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>. Πατήστε <a href="%s">εδώ</a> για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών.
+blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>.
 author_search_tooltip=Εμφάνιση το πολύ 30 χρηστών
 
+tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s
+tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s
+tree_path_not_found_tag=Η διαδρομή %[1]s δεν υπάρχει στην ετικέτα %[2]s
 
 transfer.accept=Αποδοχή Μεταφοράς
 transfer.accept_desc=`Μεταφορά στο "%s"`
 transfer.reject=Απόρριψη Μεταφοράς
 transfer.reject_desc=`Ακύρωση μεταφοράς σε "%s"`
+transfer.no_permission_to_accept=Δεν έχετε άδεια να αποδεχτείτε αυτή τη μεταφορά.
+transfer.no_permission_to_reject=Δεν έχετε άδεια να απορρίψετε αυτή τη μεταφορά.
 
 desc.private=Ιδιωτικό
 desc.public=Δημόσιο
@@ -1029,6 +1046,8 @@ template.issue_labels=Σήματα Ζητήματος
 template.one_item=Πρέπει να επιλέξετε τουλάχιστον ένα αντικείμενο στο πρότυπο
 template.invalid=Πρέπει να επιλέξετε ένα πρότυπο αποθετήριο
 
+archive.title=Αυτό το αποθετήρειο αρχειοθετήθηκε. Μπορείτε να προβάλετε αρχεία και να τα κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests.
+archive.title_date=Αυτό το αποθετήριο έχει αρχειοθετηθεί στο %s. Μπορείτε να προβάλετε αρχεία και να κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests.
 archive.issue.nocomment=Αυτό το αποθετήριο αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε σε ζητήματα.
 archive.pull.nocomment=Αυτό το repo αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε στα pull requests.
 
@@ -1045,6 +1064,7 @@ migrate_options_lfs=Μεταφορά αρχείων LFS
 migrate_options_lfs_endpoint.label=Άκρο LFS
 migrate_options_lfs_endpoint.description=Η μεταφορά θα προσπαθήσει να χρησιμοποιήσει το Git remote για να <a target="_blank" rel="noopener noreferrer" href="%s">καθορίσει τον διακομιστή LFS</a>. Μπορείτε επίσης να καθορίσετε ένα δικό σας endpoint αν τα δεδομένα LFS του αποθετηρίου αποθηκεύονται κάπου αλλού.
 migrate_options_lfs_endpoint.description.local=Μια διαδρομή στο τοπικό διακομιστή επίσης υποστηρίζεται.
+migrate_options_lfs_endpoint.placeholder=Αν αφεθεί κενό, το άκρο θα προκύψει από το URL του κλώνου
 migrate_items=Στοιχεία Μεταφοράς
 migrate_items_wiki=Wiki
 migrate_items_milestones=Ορόσημα
@@ -1147,6 +1167,7 @@ file_view_rendered=Προβολή Απόδοσης
 file_view_raw=Προβολή Ακατέργαστου
 file_permalink=Permalink
 file_too_large=Το αρχείο είναι πολύ μεγάλο για να εμφανιστεί.
+invisible_runes_header=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode `
 invisible_runes_description=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode που δεν διακρίνονται από ανθρώπους, αλλά μπορεί να επεξεργάζονται διαφορετικά από έναν υπολογιστή. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
 ambiguous_runes_header=`Αυτό το αρχείο περιέχει ασαφείς χαρακτήρες Unicode `
 ambiguous_runes_description=`Αυτό το αρχείο περιέχει χαρακτήρες Unicode που μπορεί να συγχέονται με άλλους χαρακτήρες. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
@@ -1425,6 +1446,7 @@ issues.filter_sort.moststars=Περισσότερα αστέρια
 issues.filter_sort.feweststars=Λιγότερα αστέρια
 issues.filter_sort.mostforks=Περισσότερα forks
 issues.filter_sort.fewestforks=Λιγότερα forks
+issues.keyword_search_unavailable=Η αναζήτηση μέσω λέξεων κλειδιών δεν είναι διαθέσιμη. Παρακαλώ επικοινωνήστε με το διαχειριστή.
 issues.action_open=Άνοιγμα
 issues.action_close=Κλείσιμο
 issues.action_label=Σήμα
@@ -1715,8 +1737,12 @@ pulls.is_empty=Οι αλλαγές σε αυτόν τον κλάδο είναι
 pulls.required_status_check_failed=Ορισμένοι απαιτούμενοι έλεγχοι δεν ήταν επιτυχείς.
 pulls.required_status_check_missing=Λείπουν ορισμένοι απαιτούμενοι έλεγχοι.
 pulls.required_status_check_administrator=Ως διαχειριστής, μπορείτε ακόμα να συγχωνεύσετε αυτό το pull request.
+pulls.blocked_by_approvals=Το pull request δεν έχει ακόμα αρκετές εγκρίσεις. Δόθηκαν %d από %d εγκρίσεις.
 pulls.blocked_by_rejection=Αυτό το Pull Request έχει αλλαγές που ζητούνται από έναν επίσημο εξεταστή.
 pulls.blocked_by_official_review_requests=Αυτό το Pull Request έχει επίσημες αιτήσεις αξιολόγησης.
+pulls.blocked_by_outdated_branch=Αυτό το pull request έχει αποκλειστεί επειδή είναι παρωχημένο.
+pulls.blocked_by_changed_protected_files_1=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει ένα προστατευμένο αρχείο:
+pulls.blocked_by_changed_protected_files_n=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει προστατευμένα αρχεία:
 pulls.can_auto_merge_desc=Αυτό το Pull Request μπορεί να συγχωνευθεί αυτόματα.
 pulls.cannot_auto_merge_desc=Αυτό το pull request δεν μπορεί να συγχωνευθεί αυτόματα λόγω συγκρούσεων.
 pulls.cannot_auto_merge_helper=Χειροκίνητη Συγχώνευση για την επίλυση των συγκρούσεων.
@@ -1832,11 +1858,16 @@ milestones.filter_sort.least_issues=Λιγότερα ζητήματα
 
 signing.will_sign=Αυτή η υποβολή θα υπογραφεί με το κλειδί "%s".
 signing.wont_sign.error=Παρουσιάστηκε σφάλμα κατά τον έλεγχο για το αν η υποβολή μπορεί να υπογραφεί.
+signing.wont_sign.nokey=Δεν υπάρχει διαθέσιμο κλειδί για να υπογραφεί αυτή η υποβολή.
 signing.wont_sign.never=Οι υποβολές δεν υπογράφονται ποτέ.
 signing.wont_sign.always=Οι υποβολές υπογράφονται πάντα.
+signing.wont_sign.pubkey=Η υποβολή δε θα υπογραφεί επειδή δεν υπάρχει δημόσιο κλειδί που να συνδέεται με το λογαριασμό σας.
+signing.wont_sign.twofa=Πρέπει να έχετε ενεργοποιημένη την ταυτοποίηση δύο παραγόντων για να υπογράφεται υποβολές.
 signing.wont_sign.parentsigned=Η υποβολή δε θα υπογραφεί καθώς η γονική υποβολή δεν έχει υπογραφεί.
 signing.wont_sign.basesigned=Η συγχώνευση δε θα υπογραφεί καθώς η βασική υποβολή δεν έχει υπογραφή της βάσης.
 signing.wont_sign.headsigned=Η συγχώνευση δε θα υπογραφεί καθώς δεν έχει υπογραφή η υποβολή της κεφαλής.
+signing.wont_sign.commitssigned=Η συγχώνευση δε θα υπογραφεί καθώς όλες οι σχετικές υποβολές δεν έχουν υπογραφεί.
+signing.wont_sign.approved=Η συγχώνευση δε θα υπογραφεί καθώς το PR δεν έχει εγκριθεί.
 signing.wont_sign.not_signed_in=Δεν είστε συνδεδεμένοι.
 
 ext_wiki=Πρόσβαση στο Εξωτερικό Wiki
@@ -1967,7 +1998,9 @@ settings.mirror_settings.docs.disabled_push_mirror.info=Τα είδωλα ώθη
 settings.mirror_settings.docs.no_new_mirrors=Το αποθετήριο σας αντιγράφει τις αλλαγές προς ή από ένα άλλο αποθετήριο. Λάβετε υπόψη ότι δεν μπορείτε να δημιουργήσετε νέα είδωλα αυτή τη στιγμή.
 settings.mirror_settings.docs.can_still_use=Αν και δεν μπορείτε να τροποποιήσετε τα υπάρχοντα είδωλα ή να δημιουργήσετε νέα, μπορείτε να χρησιμοποιείται ακόμα το υπάρχων είδωλο.
 settings.mirror_settings.docs.pull_mirror_instructions=Για να ορίσετε έναν είδωλο έλξης, παρακαλούμε συμβουλευθείτε:
+settings.mirror_settings.docs.more_information_if_disabled=Μπορείτε να μάθετε περισσότερα για τα είδωλα ώθησης και έλξης εδώ:
 settings.mirror_settings.docs.doc_link_title=Πώς μπορώ να αντιγράψω αποθετήρια;
+settings.mirror_settings.docs.doc_link_pull_section=το κεφάλαιο "Pulling from a remote repository" της τεκμηρίωσης.
 settings.mirror_settings.docs.pulling_remote_title=Έλξη από ένα απομακρυσμένο αποθετήριο
 settings.mirror_settings.mirrored_repository=Είδωλο αποθετηρίου
 settings.mirror_settings.direction=Κατεύθυνση
@@ -1980,6 +2013,8 @@ settings.mirror_settings.push_mirror.add=Προσθήκη Είδωλου Push
 settings.mirror_settings.push_mirror.edit_sync_time=Επεξεργασία διαστήματος συγχρονισμού ειδώλου
 
 settings.sync_mirror=Συγχρονισμός Τώρα
+settings.pull_mirror_sync_in_progress=Έλκονται αλλαγές από το απομακρυσμένο %s αυτή τη στιγμή.
+settings.push_mirror_sync_in_progress=Ώθηση αλλαγών στο απομακρυσμένο %s αυτή τη στιγμή.
 settings.site=Ιστοσελίδα
 settings.update_settings=Ενημέρωση Ρυθμίσεων
 settings.update_mirror_settings=Ενημέρωση Ρυθμίσεων Ειδώλου
@@ -2046,6 +2081,7 @@ settings.transfer.rejected=Η μεταβίβαση του αποθετηρίου
 settings.transfer.success=Η μεταβίβαση του αποθετηρίου ήταν επιτυχής.
 settings.transfer_abort=Ακύρωση μεταβίβασης
 settings.transfer_abort_invalid=Δεν μπορείτε να ακυρώσετε μια ανύπαρκτη μεταβίβαση αποθετηρίου.
+settings.transfer_abort_success=Η μεταφορά αποθετηρίου στο %s ακυρώθηκε με επιτυχία.
 settings.transfer_desc=Μεταβιβάστε αυτό το αποθετήριο σε έναν χρήστη ή σε έναν οργανισμό για τον οποίο έχετε δικαιώματα διαχειριστή.
 settings.transfer_form_title=Εισάγετε το όνομα του αποθετηρίου ως επιβεβαίωση:
 settings.transfer_in_progress=Αυτή τη στιγμή υπάρχει μια εν εξελίξει μεταβίβαση. Παρακαλούμε ακυρώστε την αν θέλετε να μεταβιβάσετε αυτό το αποθετήριο σε άλλο χρήστη.
@@ -2335,6 +2371,7 @@ settings.unarchive.button=Απο-Αρχειοθέτηση αποθετηρίου
 settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου
 settings.unarchive.text=Η απο-αρχειοθέτηση του αποθετηρίου θα αποκαταστήσει την ικανότητά του να λαμβάνει υποβολές και ωθήσεις, καθώς και νέα ζητήματα και pull-requests.
 settings.unarchive.success=Το αποθετήριο απο-αρχειοθετήθηκε με επιτυχία.
+settings.unarchive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια απο-αρχειοθέτησης του αποθετηρίου. Δείτε τις καταγραφές για περισσότερες λεπτομέρειες.
 settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί.
 settings.lfs=LFS
 settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο
@@ -2458,6 +2495,7 @@ release.edit_release=Ενημέρωση Κυκλοφορίας
 release.delete_release=Διαγραφή Κυκλοφορίας
 release.delete_tag=Διαγραφή Ετικέτας
 release.deletion=Διαγραφή Κυκλοφορίας
+release.deletion_desc=Διαγράφοντας μια κυκλοφορία, αυτή αφαιρείται μόνο από το Gitea. Δε θα επηρεάσει την ετικέτα Git, τα περιεχόμενα του αποθετηρίου σας ή το ιστορικό της. Συνέχεια;
 release.deletion_success=Η κυκλοφορία έχει διαγραφεί.
 release.deletion_tag_desc=Θα διαγράψει αυτή την ετικέτα από το αποθετήριο. Τα περιεχόμενα του αποθετηρίου και το ιστορικό παραμένουν αμετάβλητα. Συνέχεια;
 release.deletion_tag_success=Η ετικέτα έχει διαγραφεί.
@@ -2477,6 +2515,7 @@ branch.already_exists=Ήδη υπάρχει ένας κλάδος με το όν
 branch.delete_head=Διαγραφή
 branch.delete=`Διαγραφή του Κλάδου "%s"`
 branch.delete_html=Διαγραφή Κλάδου
+branch.delete_desc=Η διαγραφή ενός κλάδου είναι μόνιμη. Αν και ο διαγραμμένος κλάδος μπορεί να συνεχίσει να υπάρχει για σύντομο χρονικό διάστημα πριν να αφαιρεθεί, ΔΕΝ ΜΠΟΡΕΙ να αναιρεθεί στις περισσότερες περιπτώσεις. Συνέχεια;
 branch.deletion_success=Ο κλάδος "%s" διαγράφηκε.
 branch.deletion_failed=Αποτυχία διαγραφής του κλάδου "%s".
 branch.delete_branch_has_new_commits=Ο κλάδος "%s" δεν μπορεί να διαγραφεί επειδή προστέθηκαν νέες υποβολές μετά τη συγχώνευση.
@@ -2711,6 +2750,7 @@ dashboard.reinit_missing_repos=Επανεκκινήστε όλα τα αποθε
 dashboard.sync_external_users=Συγχρονισμός δεδομένων εξωτερικών χρηστών
 dashboard.cleanup_hook_task_table=Εκκαθάριση πίνακα hook_task
 dashboard.cleanup_packages=Εκκαθάριση ληγμένων πακέτων
+dashboard.cleanup_actions=Οι ενέργειες καθαρισμού καταγραφές και αντικείμενα
 dashboard.server_uptime=Διάρκεια Διακομιστή
 dashboard.current_goroutine=Τρέχουσες Goroutines
 dashboard.current_memory_usage=Τρέχουσα Χρήση Μνήμης
@@ -2821,6 +2861,7 @@ emails.updated=Το email ενημερώθηκε
 emails.not_updated=Αποτυχία ενημέρωσης της ζητούμενης διεύθυνσης email: %v
 emails.duplicate_active=Αυτή η διεύθυνση email είναι ήδη ενεργή σε διαφορετικό χρήστη.
 emails.change_email_header=Ενημέρωση Ιδιοτήτων Email
+emails.change_email_text=Είστε βέβαιοι ότι θέλετε να ενημερώσετε αυτή τη διεύθυνση email;
 
 orgs.org_manage_panel=Διαχείριση Οργανισμού
 orgs.name=Όνομα
@@ -2845,6 +2886,7 @@ packages.package_manage_panel=Διαχείριση Πακέτων
 packages.total_size=Συνολικό Μέγεθος: %s
 packages.unreferenced_size=Μέγεθος Χωρίς Αναφορά: %s
 packages.cleanup=Εκκαθάριση ληγμένων δεδομένων
+packages.cleanup.success=Επιτυχής εκκαθάριση δεδομένων που έχουν λήξει
 packages.owner=Ιδιοκτήτης
 packages.creator=Δημιουργός
 packages.name=Όνομα
@@ -2855,10 +2897,12 @@ packages.size=Μέγεθος
 packages.published=Δημοσιευμένα
 
 defaulthooks=Προεπιλεγμένα Webhooks
+defaulthooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούν ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ είναι προκαθορισμένα και θα αντιγραφούν σε όλα τα νέα αποθετήρια. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>.
 defaulthooks.add_webhook=Προσθήκη Προεπιλεγμένου Webhook
 defaulthooks.update_webhook=Ενημέρωση Προεπιλεγμένου Webhook
 
 systemhooks=Webhooks Συστήματος
+systemhooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούνται ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ θα ενεργούν σε όλα τα αποθετήρια του συστήματος, γι 'αυτό παρακαλώ εξετάστε τυχόν επιπτώσεις απόδοσης που μπορεί να έχει. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>.
 systemhooks.add_webhook=Προσθήκη Webhook Συστήματος
 systemhooks.update_webhook=Ενημέρωση Webhook Συστήματος
 
@@ -2951,6 +2995,7 @@ auths.sspi_default_language=Προεπιλεγμένη γλώσσα χρήστη
 auths.sspi_default_language_helper=Προεπιλεγμένη γλώσσα για τους χρήστες που δημιουργούνται αυτόματα με τη μέθοδο ταυτοποίησης SSPI. Αφήστε κενό αν προτιμάτε η γλώσσα να εντοπιστεί αυτόματα.
 auths.tips=Συμβουλές
 auths.tips.oauth2.general=Ταυτοποίηση OAuth2
+auths.tips.oauth2.general.tip=Κατά την εγγραφή μιας νέας ταυτοποίησης OAuth2, το URL κλήσης/ανακατεύθυνσης πρέπει να είναι:
 auths.tip.oauth2_provider=Πάροχος OAuth2
 auths.tip.bitbucket=Καταχωρήστε ένα νέο καταναλωτή OAuth στο https://bitbucket.org/account/user/<your username>/oauth-consumers/new και προσθέστε το δικαίωμα 'Account' - 'Read'
 auths.tip.nextcloud=`Καταχωρήστε ένα νέο καταναλωτή OAuth στην υπηρεσία σας χρησιμοποιώντας το παρακάτω μενού "Settings -> Security -> OAuth 2.0 client"`
@@ -2962,6 +3007,7 @@ auths.tip.google_plus=Αποκτήστε τα διαπιστευτήρια πε
 auths.tip.openid_connect=Χρησιμοποιήστε το OpenID Connect Discovery URL (<server>/.well known/openid-configuration) για να καθορίσετε τα τελικά σημεία
 auths.tip.twitter=Πηγαίνετε στο https://dev.twitter.com/apps, δημιουργήστε μια εφαρμογή και βεβαιωθείτε ότι η επιλογή “Allow this application to be used to Sign in with Twitter” είναι ενεργοποιημένη
 auths.tip.discord=Καταχωρήστε μια νέα εφαρμογή στο https://discordapp.com/developers/applications/me
+auths.tip.gitea=Καταχωρήστε μια νέα εφαρμογή OAuth2. Μπορείτε να βρείτε τον οδηγό στο https://docs.gitea.com/development/oauth2-provider
 auths.tip.yandex=`Δημιουργήστε μια νέα εφαρμογή στο https://oauth.yandex.com/client/new. Επιλέξτε τα ακόλουθα δικαιώματα από την ενότητα "Yandex.Passport API": "Access to email address", "Access to user avatar" και "Access to username, first name and surname, gender"`
 auths.tip.mastodon=Εισαγάγετε ένα προσαρμομένο URL για την υπηρεσία mastodon με την οποία θέλετε να πιστοποιήσετε (ή να χρησιμοποιήσετε την προεπιλεγμένη)
 auths.edit=Επεξεργασία Πηγής Ταυτοποίησης
@@ -3265,6 +3311,7 @@ desc=Διαχείριση πακέτων μητρώου.
 empty=Δεν υπάρχουν πακέτα ακόμα.
 empty.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο πακέτων, ανατρέξτε <a target="_blank" rel="noopener noreferrer" href="%s">στην τεκμηρίωση</a>.
 empty.repo=Μήπως ανεβάσατε ένα πακέτο, αλλά δεν εμφανίζεται εδώ; Πηγαίνετε στις <a href="%[1]s">ρυθμίσεις πακέτων</a> και συνδέστε το σε αυτό το αποθετήριο.
+registry.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο %s, ανατρέξτε στη τεκμηρίωση <a target="_blank" rel="noopener noreferrer" href="%s"></a>.
 filter.type=Τύπος
 filter.type.all=Όλα
 filter.no_result=Το φίλτρο δεν παρήγαγε αποτελέσματα.
@@ -3376,9 +3423,11 @@ settings.delete.success=Το πακέτο έχει διαγραφεί.
 settings.delete.error=Αποτυχία διαγραφής του πακέτου.
 owner.settings.cargo.title=Ευρετήριο Μητρώου Cargo
 owner.settings.cargo.initialize=Αρχικοποίηση Ευρετηρίου
+owner.settings.cargo.initialize.description=Απαιτείται ένα ειδικό αποθετήριο ευρετηρίου Git για τη χρήση του μητρώου Cargo. Χρησιμοποιώντας αυτή την επιλογή θα δημιουργηθεί ξανά το αποθετήριο και θα ρυθμιστεί αυτόματα.
 owner.settings.cargo.initialize.error=Αποτυχία αρχικοποίησης ευρετηρίου Cargo: %v
 owner.settings.cargo.initialize.success=Ο ευρετήριο Cargo δημιουργήθηκε με επιτυχία.
 owner.settings.cargo.rebuild=Αναδημιουργία Ευρετηρίου
+owner.settings.cargo.rebuild.description=Η ανοικοδόμηση μπορεί να είναι χρήσιμη εάν ο δείκτης δεν είναι συγχρονισμένος με τα αποθηκευμένα πακέτα Cargo.
 owner.settings.cargo.rebuild.error=Αποτυχία αναδόμησης του ευρετηρίου Cargo: %v
 owner.settings.cargo.rebuild.success=Το ευρετήριο Cargo αναδομήθηκε με επιτυχία.
 owner.settings.cleanuprules.title=Διαχείριση Κανόνων Εκκαθάρισης
@@ -3403,6 +3452,7 @@ owner.settings.cleanuprules.success.update=Ο κανόνας καθαρισμο
 owner.settings.cleanuprules.success.delete=Ο κανόνας καθαρισμού διαγράφηκε.
 owner.settings.chef.title=Μητρώο Chef
 owner.settings.chef.keypair=Δημιουργία ζεύγους κλειδιών
+owner.settings.chef.keypair.description=Ένα ζεύγος κλειδιών είναι απαραίτητο για ταυτοποίηση στο μητρώο Chef. Αν έχετε δημιουργήσει ένα ζεύγος κλειδιών πριν, η δημιουργία ενός νέου ζεύγους κλειδιών θα απορρίψει το παλιό ζεύγος κλειδιών.
 
 [secrets]
 secrets=Μυστικά
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index dd7d1b066e..ea028657db 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -17,6 +17,7 @@ template=Şablon
 language=Dil
 notifications=Bildirimler
 active_stopwatch=Etkin Zaman Takibi
+tracked_time_summary=Konu listesi süzgeçlerine dayanan takip edilen zamanın özeti
 create_new=Oluştur…
 user_profile_and_more=Profil ve Ayarlar…
 signed_in_as=Giriş yapan:
@@ -90,6 +91,7 @@ remove=Kaldır
 remove_all=Tümünü Kaldır
 remove_label_str=`"%s" öğesini kaldır`
 edit=Düzenle
+view=Görüntüle
 
 enabled=Aktifleştirilmiş
 disabled=Devre Dışı
@@ -97,6 +99,7 @@ locked=Kilitli
 
 copy=Kopyala
 copy_url=URL'yi kopyala
+copy_hash=Hash'i kopyala
 copy_content=İçeriği kopyala
 copy_branch=Dal adını kopyala
 copy_success=Kopyalandı!
@@ -109,6 +112,7 @@ loading=Yükleniyor…
 
 error=Hata
 error404=Ulaşmaya çalıştığınız sayfa <strong>mevcut değil</strong> veya <strong>görüntüleme yetkiniz yok</strong>.
+go_back=Geri Git
 
 never=Asla
 unknown=Bilinmiyor
@@ -180,6 +184,7 @@ network_error=Ağ hatası
 [startpage]
 app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi
 install=Kurulumu kolay
+install_desc=Platformunuz için <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">ikili dosyayı çalıştırın</a>, <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a> ile yükleyin veya <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">paket</a> olarak edinin.
 platform=Farklı platformlarda çalışablir
 platform_desc=Gitea <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> ile derleme yapılabilecek her yerde çalışmaktadır: Windows, macOS, Linux, ARM, vb. Hangisini seviyorsanız onu seçin!
 lightweight=Hafif
@@ -356,6 +361,7 @@ disable_register_prompt=Kayıt işlemi devre dışıdır. Lütfen site yönetici
 disable_register_mail=Kayıt için e-posta doğrulama devre dışıdır.
 manual_activation_only=Etkinleştirmeyi tamamlamak için site yöneticinizle bağlantıya geçin.
 remember_me=Bu Aygıtı hatırla
+remember_me.compromised=Oturum açma tokeni artık geçerli değil, bu ele geçirilmiş bir hesaba işaret ediyor olabilir. Lütfen hesabınızda olağandışı faaliyet olup olmadığını denetleyin.
 forgot_password_title=Şifremi unuttum
 forgot_password=Şifrenizi mi unuttunuz?
 sign_up_now=Bir hesaba mı ihtiyacınız var? Hemen kaydolun.
@@ -375,6 +381,7 @@ email_not_associate=Bu e-posta adresi hiçbir hesap ile ilişkilendirilmemiştir
 send_reset_mail=Hesap Kurtarma E-postası Gönder
 reset_password=Hesap Kurtarma
 invalid_code=Doğrulama kodunuz geçersiz veya süresi dolmuş.
+invalid_code_forgot_password=Onay kodunuz hatalı veya süresi geçmiş. Yeni bir oturum başlatmak için <a href="%s">buraya</a> tıklayın.
 invalid_password=Parolanız hesap oluşturulurken kullanılan parolayla eşleşmiyor.
 reset_password_helper=Hesabı Kurtar
 reset_password_wrong_user=%s olarak oturum açmışsınız, ancak hesap kurtarma bağlantısı %s için
@@ -676,6 +683,7 @@ choose_new_avatar=Yeni Avatar Seç
 update_avatar=Profil Resmini Güncelle
 delete_current_avatar=Güncel Avatarı Sil
 uploaded_avatar_not_a_image=Yüklenen dosya bir resim dosyası değil.
+uploaded_avatar_is_too_big=Yüklenen dosyanın boyutu (%d KiB), azami boyutu (%d KiB) aşıyor.
 update_avatar_success=Profil resminiz değiştirildi.
 update_user_avatar_success=Kullanıcının avatarı güncellendi.
 
@@ -857,6 +865,7 @@ revoke_oauth2_grant_description=Bu üçüncü taraf uygulamasına erişimin ipta
 revoke_oauth2_grant_success=Erişim başarıyla kaldırıldı.
 
 twofa_desc=İki faktörlü kimlik doğrulama, hesabınızın güvenliğini artırır.
+twofa_recovery_tip=Aygıtınızı kaybetmeniz durumunda, hesabınıza tekrar erişmek için tek kullanımlık kurtarma anahtarını kullanabileceksiniz.
 twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde <strong>kaydedilmiş</strong>.
 twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş.
 twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak
@@ -879,6 +888,8 @@ webauthn_register_key=Güvenlik Anahtarı Ekle
 webauthn_nickname=Takma Ad
 webauthn_delete_key=Güvenlik Anahtarını Kaldır
 webauthn_delete_key_desc=Bir güvenlik anahtarını kaldırırsanız, onunla artık giriş yapamazsınız. Devam edilsin mi?
+webauthn_key_loss_warning=Güvenlik anahtarlarınızı kaybederseniz, hesabınıza erişimi kaybedersiniz.
+webauthn_alternative_tip=Ek bir kimlik doğrulama yöntemi ayarlamak isteyebilirsiniz.
 
 manage_account_links=Bağlı Hesapları Yönet
 manage_account_links_desc=Bu harici hesaplar Gitea hesabınızla bağlantılı.
@@ -915,6 +926,7 @@ visibility.private=Özel
 visibility.private_tooltip=Sadece katıldığınız organizasyonların üyeleri tarafından görünür
 
 [repo]
+new_repo_helper=Bir depo, sürüm geçmişi dahil tüm proje dosyalarını içerir. Zaten başka bir yerde mi barındırıyorsunuz? <a href="%s">Depoyu taşıyın.</a>
 owner=Sahibi
 owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir.
 repo_name=Depo İsmi
@@ -935,6 +947,8 @@ fork_from=Buradan Çatalla
 already_forked=%s deposunu zaten çatalladınız
 fork_to_different_account=Başka bir hesaba çatalla
 fork_visibility_helper=Çatallanmış bir deponun görünürlüğü değiştirilemez.
+fork_branch=Çatala klonlanacak dal
+all_branches=Tüm dallar
 fork_no_valid_owners=Geçerli bir sahibi olmadığı için bu depo çatallanamaz.
 use_template=Bu şablonu kullan
 clone_in_vsc=VS Code'ta klonla
@@ -964,6 +978,7 @@ trust_model_helper_collaborator_committer=Ortak çalışan+İşleyen: İşleyenl
 trust_model_helper_default=Varsayılan: Bu kurulum için varsayılan güven modelini kullan
 create_repo=Depo Oluştur
 default_branch=Varsayılan Dal
+default_branch_label=varsayılan
 default_branch_helper=Varsayılan dal, değişiklik istekleri ve kod işlemeleri için temel daldır.
 mirror_prune=Buda
 mirror_prune_desc=Kullanılmayan uzak depoları izleyen referansları kaldır
@@ -999,8 +1014,13 @@ delete_preexisting=Önceden var olan dosyaları sil
 delete_preexisting_content=%s içindeki dosyaları sil
 delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi
 blame_prior=Bu değişiklikten önceki suçu görüntüle
+blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için <a href="%s">buraya tıklayın</a>.
+blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılamadı.
 author_search_tooltip=En fazla 30 kullanıcı görüntüler
 
+tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil
+tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil
+tree_path_not_found_tag=%[1] yolu, %[2]s etiketinde mevcut değil
 
 transfer.accept=Aktarımı Kabul Et
 transfer.accept_desc=`"%s" tarafına aktar`
@@ -1264,6 +1284,7 @@ commits.signed_by_untrusted_user=Güvenilmeyen kullanıcı tarafından imzaland
 commits.signed_by_untrusted_user_unmatched=İşleyici ile eşleşmeyen güvenilmeyen kullanıcı tarafından imzalanmış
 commits.gpg_key_id=GPG Anahtar Kimliği
 commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi
+commits.view_path=Geçmişte bu noktayı görüntüle
 
 commit.operations=İşlemler
 commit.revert=Geri Al
@@ -1474,8 +1495,17 @@ issues.ref_closed_from=`<a href="%[3]s">bu konuyu kapat%[4]s</a> <a id="%[1]s" h
 issues.ref_reopened_from=`<a href="%[3]s">konuyu yeniden aç%[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`%[1]s'den`
 issues.author=Yazar
+issues.author_helper=Bu kullanıcı yazardır.
 issues.role.owner=Sahibi
+issues.role.owner_helper=Bu kullanıcı bu deponun sahibidir.
 issues.role.member=Üye
+issues.role.member_helper=Bu kullanıcı bu deponun sahibi olan organizasyonun üyesidir.
+issues.role.collaborator=Katkıcı
+issues.role.collaborator_helper=Kullanıcı bu depoya işbirliği için davet edildi.
+issues.role.first_time_contributor=İlk defa katkıcı
+issues.role.first_time_contributor_helper=Bu, bu kullanıcının bu depoya ilk katkısı.
+issues.role.contributor=Katılımcı
+issues.role.contributor_helper=Bu kullanıcı bu depoya daha önce işleme gönderdi.
 issues.re_request_review=İncelemeyi yeniden iste
 issues.is_stale=Bu incelemeden bu yana bu istekte değişiklikler oldu
 issues.remove_request_review=İnceleme isteğini kaldır
@@ -1491,6 +1521,8 @@ issues.label_description=Etiket açıklaması
 issues.label_color=Etiket rengi
 issues.label_exclusive=Özel
 issues.label_archive=Etiketi Arşivle
+issues.label_archived_filter=Arşivlenmiş etiketleri göster
+issues.label_archive_tooltip=Arşivlenmiş etiketler, etiket araması yapılırken varsayılan olarak önerilerin dışında tutuluyor.
 issues.label_exclusive_desc=<code>Kapsam/öğe</code> etiketini, diğer <code>kapsam/</code> etiketleriyle ayrışık olacak şekilde adlandırın.
 issues.label_exclusive_warning=Çakışan kapsamlı etiketler, bir konu veya değişiklik isteği etiketleri düzenlenirken kaldırılacaktır.
 issues.label_count=%d etiket
@@ -1745,6 +1777,7 @@ pulls.rebase_conflict_summary=Hata Mesajı
 pulls.unrelated_histories=Birleştirme Başarısız: Birleştirme başlığı ve tabanı ortak bir geçmişi paylaşmıyor. İpucu: Farklı bir strateji deneyin
 pulls.merge_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, taban güncellendi. İpucu: Tekrar deneyin.
 pulls.head_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, ana güncellendi. İpucu: Tekrar deneyin.
+pulls.has_merged=Başarısız: Değişiklik isteği birleştirildi, yeniden birleştiremez veya hedef dalı değiştiremezsiniz.
 pulls.push_rejected=Birleştirme Başarısız Oldu: Gönderme reddedildi. Bu depo için Git İstemcilerini inceleyin.
 pulls.push_rejected_summary=Tam Red Mesajı
 pulls.push_rejected_no_message=Birleştirme başarısız oldu: Gönderme reddedildi, ancak uzak bir mesaj yoktu.<br>Bu depo için Git İstemcilerini inceleyin
@@ -1756,6 +1789,8 @@ pulls.status_checks_failure=Bazı kontroller başarısız oldu
 pulls.status_checks_error=Bazı kontroller hatalar bildirdi
 pulls.status_checks_requested=Gerekli
 pulls.status_checks_details=Ayrıntılar
+pulls.status_checks_hide_all=Tüm denetlemeleri gizle
+pulls.status_checks_show_all=Tüm denetlemeleri göster
 pulls.update_branch=Dalı birleştirmeyle güncelle
 pulls.update_branch_rebase=Dalı yeniden yapılandırmayla güncelle
 pulls.update_branch_success=Dal güncellemesi başarıyla gerçekleştirildi
@@ -1764,6 +1799,11 @@ pulls.outdated_with_base_branch=Bu dal, temel dal ile güncel değil
 pulls.close=Değişiklik İsteğini Kapat
 pulls.closed_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini kapattı`
 pulls.reopened_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini yeniden açtı`
+pulls.cmd_instruction_hint=`<a class="show-instruction">Komut satırı talimatlarını</a> görüntüleyin.`
+pulls.cmd_instruction_checkout_title=Çekme
+pulls.cmd_instruction_checkout_desc=Proje deponuzdan yeni bir dalı çekin ve değişiklikleri test edin.
+pulls.cmd_instruction_merge_title=Birleştir
+pulls.cmd_instruction_merge_desc=Değişiklikleri birleştirin ve Gitea'da güncelleyin.
 pulls.clear_merge_message=Birleştirme iletilerini temizle
 pulls.clear_merge_message_hint=Birleştirme iletisini temizlemek sadece işleme ileti içeriğini kaldırır ama üretilmiş "Co-Authored-By …" gibi git fragmanlarını korur.
 
@@ -1809,6 +1849,8 @@ milestones.edit_success=`"%s" dönüm noktası güncellendi.`
 milestones.deletion=Kilometre Taşını Sil
 milestones.deletion_desc=Bir kilometre taşını silmek, onu ilgili tüm sorunlardan kaldırır. Devam edilsin mi?
 milestones.deletion_success=Kilometre taşı silindi.
+milestones.filter_sort.earliest_due_data=En erken bitiş tarihi
+milestones.filter_sort.latest_due_date=En uzak bitiş tarihi
 milestones.filter_sort.least_complete=En az tamamlama
 milestones.filter_sort.most_complete=En çok tamamlama
 milestones.filter_sort.most_issues=En çok konu
@@ -1971,6 +2013,8 @@ settings.mirror_settings.push_mirror.add=Yansı Gönderimi Ekle
 settings.mirror_settings.push_mirror.edit_sync_time=Yansı eşzamanlama aralığını düzenle
 
 settings.sync_mirror=Şimdi Eşitle
+settings.pull_mirror_sync_in_progress=Şu an %s uzak sunucusundan değişiklikler çekiliyor.
+settings.push_mirror_sync_in_progress=Şu an %s uzak sunucusuna değişiklikler itiliyor.
 settings.site=Web Sitesi
 settings.update_settings=Ayarları Güncelle
 settings.update_mirror_settings=Yansı Ayarları Güncelle
@@ -2104,12 +2148,14 @@ settings.webhook_deletion_desc=Bir web isteğini kaldırmak, ayarlarını ve tes
 settings.webhook_deletion_success=Web isteği silindi.
 settings.webhook.test_delivery=Test Dağıtımı
 settings.webhook.test_delivery_desc=Bu web isteğini sahte bir olayla test edin.
+settings.webhook.test_delivery_desc_disabled=Bu web istemcisini sahte bir olayla denemek için etkinleştirin.
 settings.webhook.request=İstekler
 settings.webhook.response=Cevaplar
 settings.webhook.headers=Başlıklar
 settings.webhook.payload=İçerik
 settings.webhook.body=Gövde
 settings.webhook.replay.description=Bu web kancasını tekrar çalıştır.
+settings.webhook.replay.description_disabled=Bu web istemcisini yeniden oynatmak için etkinleştirin.
 settings.webhook.delivery.success=Teslim kuyruğuna bir olay eklendi. Teslim geçmişinde görünmesi birkaç saniye alabilir.
 settings.githooks_desc=Git İstemcileri Git'in kendisi tarafından desteklenmektedir. Özel işlemler ayarlamak için aşağıdaki istemci dosyalarını düzenleyebilirsiniz.
 settings.githook_edit_desc=İstek aktif değilse örnek içerik sunulacaktır. İçeriği boş bırakmak, isteği devre dışı bırakmayı beraberinde getirecektir.
@@ -2269,6 +2315,7 @@ settings.dismiss_stale_approvals_desc=Değişiklik isteğinin içeriğini deği
 settings.require_signed_commits=İmzalı İşleme Gerekli
 settings.require_signed_commits_desc=Reddetme, onlar imzasızsa veya doğrulanamazsa bu dala gönderir.
 settings.protect_branch_name_pattern=Korunmuş Dal Adı Deseni
+settings.protect_branch_name_pattern_desc=Korunmuş dal isim desenleri. Desen sözdizimi için <a href="https://github.com/gobwas/glob">belgelere</a> bakabilirsiniz. Örnekler: main, release/**
 settings.protect_patterns=Desenler
 settings.protect_protected_file_patterns=Korumalı dosya kalıpları (noktalı virgülle ayrılmış ';'):
 settings.protect_protected_file_patterns_desc=Kullanıcının bu dalda dosya ekleme, düzenleme veya silme hakları olsa bile doğrudan değiştirilmesine izin verilmeyen korumalı dosyalar. Birden çok desen noktalı virgül (';') kullanılarak ayrılabilir. Desen sözdizimi için <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> belgelerine bakın. Örnekler: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2305,6 +2352,7 @@ settings.tags.protection.allowed.teams=İzin verilen takımlar
 settings.tags.protection.allowed.noone=Hiç kimse
 settings.tags.protection.create=Etiketi Koru
 settings.tags.protection.none=Korumalı etiket yok.
+settings.tags.protection.pattern.description=Birden çok etiketi eşleştirmek için tek bir ad, glob deseni veya normal ifade kullanabilirsiniz. Daha fazlası için <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">korumalı etiketler rehberini</a> okuyun.
 settings.bot_token=Bot Jetonu
 settings.chat_id=Sohbet Kimliği
 settings.thread_id=İş Parçacığı ID
@@ -2485,6 +2533,7 @@ branch.default_deletion_failed=`"%s" dalı varsayılan daldır. Silinemez.`
 branch.restore=`"%s" Dalını Geri Yükle`
 branch.download=`"%s" Dalını İndir`
 branch.rename=`"%s" Dalının Adını Değiştir`
+branch.search=Dal Ara
 branch.included_desc=Bu dal varsayılan dalın bir parçasıdır
 branch.included=Dahil
 branch.create_new_branch=Şu daldan dal oluştur:
@@ -2701,6 +2750,7 @@ dashboard.reinit_missing_repos=Kayıtları bulunanlar için tüm eksik Git depol
 dashboard.sync_external_users=Harici kullanıcı verisini senkronize et
 dashboard.cleanup_hook_task_table=Hook_task tablosunu temizleme
 dashboard.cleanup_packages=Süresi dolmuş paketleri temizleme
+dashboard.cleanup_actions=Eylemlerin süresi geçmiş günlük ve yapılarını temizle
 dashboard.server_uptime=Sunucunun Ayakta Kalma Süresi
 dashboard.current_goroutine=Güncel Goroutine'ler
 dashboard.current_memory_usage=Güncel Bellek Kullanımı
@@ -2738,7 +2788,9 @@ dashboard.gc_lfs=LFS üst nesnelerin atıklarını temizle
 dashboard.stop_zombie_tasks=Zombi görevleri durdur
 dashboard.stop_endless_tasks=Daimi görevleri durdur
 dashboard.cancel_abandoned_jobs=Terkedilmiş görevleri iptal et
+dashboard.start_schedule_tasks=Zamanlanmış görevleri başlat
 dashboard.sync_branch.started=Dal Eşzamanlaması başladı
+dashboard.rebuild_issue_indexer=Konu indeksini yeniden oluştur
 
 users.user_manage_panel=Kullanıcı Hesap Yönetimi
 users.new_account=Yeni Kullanıcı Hesabı
@@ -2747,6 +2799,9 @@ users.full_name=Tam İsim
 users.activated=Aktifleştirilmiş
 users.admin=Yönetici
 users.restricted=Kısıtlanmış
+users.reserved=Rezerve
+users.bot=Bot
+users.remote=Uzak
 users.2fa=2FD
 users.repos=Depolar
 users.created=Oluşturuldu
@@ -2793,6 +2848,7 @@ users.list_status_filter.is_prohibit_login=Oturum Açmayı Önle
 users.list_status_filter.not_prohibit_login=Oturum Açmaya İzin Ver
 users.list_status_filter.is_2fa_enabled=2FA Etkin
 users.list_status_filter.not_2fa_enabled=2FA Devre Dışı
+users.details=Kullanıcı Ayrıntıları
 
 emails.email_manage_panel=Kullanıcı E-posta Yönetimi
 emails.primary=Birincil
@@ -2805,6 +2861,7 @@ emails.updated=E-posta güncellendi
 emails.not_updated=İstenen e-posta adresi güncellenemedi: %v
 emails.duplicate_active=Bu e-posta adresi farklı bir kullanıcı için zaten aktif.
 emails.change_email_header=E-posta Özelliklerini Güncelle
+emails.change_email_text=Bu e-posta adresini güncellemek istediğinizden emin misiniz?
 
 orgs.org_manage_panel=Organizasyon Yönetimi
 orgs.name=İsim
@@ -2829,6 +2886,7 @@ packages.package_manage_panel=Paket Yönetimi
 packages.total_size=Toplam Boyut: %s
 packages.unreferenced_size=Referanssız Boyut: %s
 packages.cleanup=Süresi dolmuş veriyi temizle
+packages.cleanup.success=Süresi dolmuş veri başarıyla temizlendi
 packages.owner=Sahibi
 packages.creator=Oluşturan
 packages.name=İsim
@@ -2839,10 +2897,12 @@ packages.size=Boyut
 packages.published=Yayınlandı
 
 defaulthooks=Varsayılan Web İstemcileri
+defaulthooks.desc=Web İstemcileri, belirli Gitea olayları tetiklendiğinde otomatik olarak HTTP POST isteklerini sunucuya yapar. Burada tanımlanan Web İstemcileri varsayılandır ve tüm yeni depolara kopyalanır. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> daha fazla bilgi edinin.
 defaulthooks.add_webhook=Varsayılan Web İstemcisi Ekle
 defaulthooks.update_webhook=Varsayılan Web İstemcisini Güncelle
 
 systemhooks=Sistem Web İstemcileri
+systemhooks.desc=Belirli Gitea olayları tetiklendiğinde Web istemcileri otomatik olarak bir sunucuya HTTP POST istekleri yapar. Burada tanımlanan web istemcileri sistemdeki tüm depolar üzerinde çalışır, bu yüzden lütfen bunun olabilecek tüm performans sonuçlarını göz önünde bulundurun. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> daha fazla bilgi edinin.
 systemhooks.add_webhook=Sistem Web İstemcisi Ekle
 systemhooks.update_webhook=Sistem Web İstemcisi Güncelle
 
@@ -2947,6 +3007,7 @@ auths.tip.google_plus=OAuth2 istemci kimlik bilgilerini https://console.develope
 auths.tip.openid_connect=Bitiş noktalarını belirlemek için OpenID Connect Discovery URL'sini kullanın (<server>/.well-known/openid-configuration)
 auths.tip.twitter=https://dev.twitter.com/apps adresine gidin, bir uygulama oluşturun ve “Bu uygulamanın Twitter ile oturum açmak için kullanılmasına izin ver” seçeneğinin etkin olduğundan emin olun
 auths.tip.discord=https://discordapp.com/developers/applications/me adresinde yeni bir uygulama kaydedin
+auths.tip.gitea=Yeni bir OAuth2 uygulaması kaydedin. Rehber https://docs.gitea.com/development/oauth2-provider adresinde bulunabilir
 auths.tip.yandex=`https://oauth.yandex.com/client/new adresinde yeni bir uygulama oluşturun. "Yandex.Passport API'sı" bölümünden aşağıdaki izinleri seçin: "E-posta adresine erişim", "Kullanıcı avatarına erişim" ve "Kullanıcı adına, ad ve soyadına, cinsiyete erişim"`
 auths.tip.mastodon=Kimlik doğrulaması yapmak istediğiniz mastodon örneği için özel bir örnek URL girin (veya varsayılan olanı kullanın)
 auths.edit=Kimlik Doğrulama Kaynağı Düzenle
@@ -3126,8 +3187,10 @@ monitor.queue.name=İsim
 monitor.queue.type=Tür
 monitor.queue.exemplar=Örnek Türü
 monitor.queue.numberworkers=Çalışan Sayısı
+monitor.queue.activeworkers=Etkin Çalışanlar
 monitor.queue.maxnumberworkers=En Fazla Çalışan Sayısı
 monitor.queue.numberinqueue=Kuyruktaki Sayı
+monitor.queue.review_add=Çalışanları İncele / Ekle
 monitor.queue.settings.title=Havuz Ayarları
 monitor.queue.settings.desc=Havuzlar, çalışan kuyruğu tıkanmasına bir yanıt olarak dinamik olarak büyürler.
 monitor.queue.settings.maxnumberworkers=En fazla çalışan Sayısı
@@ -3454,23 +3517,31 @@ runners.status.idle=Boşta
 runners.status.active=Etkin
 runners.status.offline=Çevrimdışı
 runners.version=Sürüm
+runners.reset_registration_token=Kayıt tokenini sıfırla
 runners.reset_registration_token_success=Çalıştırıcı kayıt belirteci başarıyla sıfırlandı
 
 runs.all_workflows=Tüm İş Akışları
 runs.commit=İşle
+runs.scheduled=Zamanlanmış
 runs.pushed_by=iten
 runs.invalid_workflow_helper=İş akışı yapılandırma dosyası geçersiz. Lütfen yapılandırma dosyanızı denetleyin: %s
+runs.no_matching_online_runner_helper=Şu etiket ile eşleşen çevrimiçi çalıştırıcı bulunamadı: %s
 runs.actor=Aktör
 runs.status=Durum
 runs.actors_no_select=Tüm aktörler
 runs.status_no_select=Tüm durumlar
 runs.no_results=Eşleşen sonuç yok.
+runs.no_workflows=Henüz hiç bir iş akışı yok.
+runs.no_workflows.quick_start=Gitea İşlem'i nasıl başlatacağınızı bilmiyor musunuz? <a target="_blank" rel="noopener noreferrer" href="%s">Hızlı başlangıç rehberine</a> bakabilirsiniz.
+runs.no_workflows.documentation=Gitea İşlem'i hakkında daha fazla bilgi için, <a target="_blank" rel="noopener noreferrer" href="%s">belgeye</a> bakabilirsiniz.
 runs.no_runs=İş akışı henüz hiç çalıştırılmadı.
+runs.empty_commit_message=(boş işleme iletisi)
 
 workflow.disable=İş Akışını Devre Dışı Bırak
 workflow.disable_success='%s' iş akışı başarıyla devre dışı bırakıldı.
 workflow.enable=İş Akışını Etkinleştir
 workflow.enable_success='%s' iş akışı başarıyla etkinleştirildi.
+workflow.disabled=İş akışı devre dışı.
 
 need_approval_desc=Değişiklik isteği çatalında iş akışı çalıştırmak için onay gerekiyor.
 

From 68227996a7a84a240b36c304d04c5c8d82948df8 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Sat, 17 Feb 2024 14:13:37 +0900
Subject: [PATCH 063/679] Fix broken following organization (#29005)

- following organization is broken from #28908
- add login check for the follow button in organization profile page
---
 routers/web/user/profile.go | 14 ++++++++++++--
 templates/org/home.tmpl     | 16 +++++++++-------
 2 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 73ab93caed..e7f133e981 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -29,6 +29,7 @@ import (
 
 const (
 	tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar"
+	tplFollowUnfollow   base.TplName = "shared/user/follow_unfollow"
 )
 
 // OwnerProfile render profile page for a user or a organization (aka, repo owner)
@@ -318,6 +319,15 @@ func Action(ctx *context.Context) {
 		return
 	}
 
-	shared_user.PrepareContextForProfileBigAvatar(ctx)
-	ctx.HTML(http.StatusOK, tplProfileBigAvatar)
+	if ctx.ContextUser.IsIndividual() {
+		shared_user.PrepareContextForProfileBigAvatar(ctx)
+		ctx.HTML(http.StatusOK, tplProfileBigAvatar)
+		return
+	} else if ctx.ContextUser.IsOrganization() {
+		ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+		ctx.HTML(http.StatusOK, tplFollowUnfollow)
+		return
+	}
+	log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
+	ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
 }
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index fc65d4691c..322be3271d 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -25,13 +25,15 @@
 				{{svg "octicon-rss" 24}}
 			</a>
 			{{end}}
-			<button class="link-action ui basic button gt-mr-0" data-url="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
-				{{if $.IsFollowing}}
-					{{ctx.Locale.Tr "user.unfollow"}}
-				{{else}}
-					{{ctx.Locale.Tr "user.follow"}}
-				{{end}}
-			</button>
+			{{if .IsSigned}}
+				<button class="ui basic button gt-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
+					{{if $.IsFollowing}}
+						{{ctx.Locale.Tr "user.unfollow"}}
+					{{else}}
+						{{ctx.Locale.Tr "user.follow"}}
+					{{end}}
+				</button>
+			{{end}}
 		</div>
 	</div>
 

From 33400a02d4eb35a0656fd6d20fc56801de09b959 Mon Sep 17 00:00:00 2001
From: Robin Schoonover <robin@cornhooves.org>
Date: Fri, 16 Feb 2024 22:40:13 -0700
Subject: [PATCH 064/679] Fix debian InRelease Acquire-By-Hash newline (#29204)

There is a missing newline when generating the debian apt repo InRelease
file, which results in output like:

```
[...]
Date: Wed, 14 Feb 2024 05:03:01 UTC
Acquire-By-Hash: yesMD5Sum:
 51a518dbddcd569ac3e0cebf330c800a 3018 main-dev/binary-amd64/Packages
[...]
```

It appears this would probably result in apt ignoring the
Acquire-By-Hash setting and not using the by-hash functionality,
although I'm not sure how to confirm it.
---
 services/packages/debian/repository.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go
index 86c54e40c8..611faa6ade 100644
--- a/services/packages/debian/repository.go
+++ b/services/packages/debian/repository.go
@@ -342,7 +342,7 @@ func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages
 	fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
 	fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
 	fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
-	fmt.Fprint(w, "Acquire-By-Hash: yes")
+	fmt.Fprint(w, "Acquire-By-Hash: yes\n")
 
 	pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
 	if err != nil {

From cb85ebc3ef9bdbe473f90181966243d89edf3440 Mon Sep 17 00:00:00 2001
From: xkcdstickfigure <97917457+xkcdstickfigure@users.noreply.github.com>
Date: Sat, 17 Feb 2024 11:01:54 +0000
Subject: [PATCH 065/679] fix typo (#29212)

---
 docs/content/administration/https-support.en-us.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/content/administration/https-support.en-us.md b/docs/content/administration/https-support.en-us.md
index 4e18722ddf..981a29bd85 100644
--- a/docs/content/administration/https-support.en-us.md
+++ b/docs/content/administration/https-support.en-us.md
@@ -35,7 +35,7 @@ CERT_FILE = cert.pem
 KEY_FILE  = key.pem
 ```
 
-Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to estalbish the trust relationship.
+Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to establish the trust relationship.
 To learn more about the config values, please checkout the [Config Cheat Sheet](administration/config-cheat-sheet.md#server-server).
 
 For the `CERT_FILE` or `KEY_FILE` field, the file path is relative to the `GITEA_CUSTOM` environment variable when it is a relative path. It can be an absolute path as well.

From c282d378bd1f2f11ffc884cd6d7c073b7b5745f8 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 17 Feb 2024 15:11:56 +0200
Subject: [PATCH 066/679] Remove jQuery from issue reference context popup
 attach (#29216)

- Switched to plain JavaScript
- Tested the context popup functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/1d2f173e-e626-4f7d-82c8-d1539d38d247)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/contextpopup.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js
index 23a620b8a2..51363b810a 100644
--- a/web_src/js/features/contextpopup.js
+++ b/web_src/js/features/contextpopup.js
@@ -1,11 +1,10 @@
-import $ from 'jquery';
 import {createApp} from 'vue';
 import ContextPopup from '../components/ContextPopup.vue';
 import {parseIssueHref} from '../utils.js';
 import {createTippy} from '../modules/tippy.js';
 
 export function initContextPopups() {
-  const refIssues = $('.ref-issue');
+  const refIssues = document.querySelectorAll('.ref-issue');
   attachRefIssueContextPopup(refIssues);
 }
 

From 27192bc321161a4e648547bd7b071065a7b18326 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 17 Feb 2024 15:17:04 +0200
Subject: [PATCH 067/679] Remove jQuery from the webhook editor (#29211)

- Switched to plain JavaScript
- Tested the webhook editing functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/b24c264d-d5e5-4954-8789-e72564a99027)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/comp/WebHookEditor.js | 56 +++++++++++------------
 1 file changed, 27 insertions(+), 29 deletions(-)

diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js
index f4c82898fd..86d21dc815 100644
--- a/web_src/js/features/comp/WebHookEditor.js
+++ b/web_src/js/features/comp/WebHookEditor.js
@@ -1,43 +1,41 @@
-import $ from 'jquery';
+import {POST} from '../../modules/fetch.js';
 import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
 
-const {csrfToken} = window.config;
-
 export function initCompWebHookEditor() {
-  if ($('.new.webhook').length === 0) {
+  if (!document.querySelectorAll('.new.webhook').length) {
     return;
   }
 
-  $('.events.checkbox input').on('change', function () {
-    if ($(this).is(':checked')) {
-      showElem($('.events.fields'));
-    }
-  });
-  $('.non-events.checkbox input').on('change', function () {
-    if ($(this).is(':checked')) {
-      hideElem($('.events.fields'));
-    }
-  });
+  for (const input of document.querySelectorAll('.events.checkbox input')) {
+    input.addEventListener('change', function () {
+      if (this.checked) {
+        showElem('.events.fields');
+      }
+    });
+  }
+
+  for (const input of document.querySelectorAll('.non-events.checkbox input')) {
+    input.addEventListener('change', function () {
+      if (this.checked) {
+        hideElem('.events.fields');
+      }
+    });
+  }
 
   const updateContentType = function () {
-    const visible = $('#http_method').val() === 'POST';
-    toggleElem($('#content_type').parent().parent(), visible);
+    const visible = document.getElementById('http_method').value === 'POST';
+    toggleElem(document.getElementById('content_type').parentNode.parentNode, visible);
   };
   updateContentType();
-  $('#http_method').on('change', () => {
-    updateContentType();
-  });
+
+  document.getElementById('http_method').addEventListener('change', updateContentType);
 
   // Test delivery
-  $('#test-delivery').on('click', function () {
-    const $this = $(this);
-    $this.addClass('loading disabled');
-    $.post($this.data('link'), {
-      _csrf: csrfToken
-    }).done(
-      setTimeout(() => {
-        window.location.href = $this.data('redirect');
-      }, 5000)
-    );
+  document.getElementById('test-delivery')?.addEventListener('click', async function () {
+    this.classList.add('loading', 'disabled');
+    await POST(this.getAttribute('data-link'));
+    setTimeout(() => {
+      window.location.href = this.getAttribute('data-redirect');
+    }, 5000);
   });
 }

From b96fbb567c67b2e1580396cd5326003a0a8da799 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 17 Feb 2024 14:18:05 +0100
Subject: [PATCH 068/679] Enable markdownlint `no-trailing-punctuation` and
 `no-blanks-blockquote` (#29214)

Enable these two and fix issues.
---
 .markdownlint.yaml                                     | 2 --
 docs/content/administration/customizing-gitea.en-us.md | 2 +-
 docs/content/administration/mail-templates.en-us.md    | 4 ++--
 docs/content/administration/mail-templates.zh-cn.md    | 2 +-
 docs/content/contributing/guidelines-frontend.en-us.md | 2 +-
 docs/content/contributing/guidelines-frontend.zh-cn.md | 2 +-
 docs/content/development/api-usage.zh-cn.md            | 2 +-
 7 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index f740d1a4d6..b251ff796c 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -5,13 +5,11 @@ heading-increment: false
 line-length: {code_blocks: false, tables: false, stern: true, line_length: -1}
 no-alt-text: false
 no-bare-urls: false
-no-blanks-blockquote: false
 no-emphasis-as-heading: false
 no-empty-links: false
 no-hard-tabs: {code_blocks: false}
 no-inline-html: false
 no-space-in-code: false
 no-space-in-emphasis: false
-no-trailing-punctuation: false
 no-trailing-spaces: {br_spaces: 0}
 single-h1: false
diff --git a/docs/content/administration/customizing-gitea.en-us.md b/docs/content/administration/customizing-gitea.en-us.md
index d122fb4bfa..7efddb2824 100644
--- a/docs/content/administration/customizing-gitea.en-us.md
+++ b/docs/content/administration/customizing-gitea.en-us.md
@@ -284,7 +284,7 @@ syntax and shouldn't be touched without fully understanding these components.
 
 Google Analytics, Matomo (previously Piwik), and other analytics services can be added to Gitea. To add the tracking code, refer to the `Other additions to the page` section of this document, and add the JavaScript to the `$GITEA_CUSTOM/templates/custom/header.tmpl` file.
 
-## Customizing gitignores, labels, licenses, locales, and readmes.
+## Customizing gitignores, labels, licenses, locales, and readmes
 
 Place custom files in corresponding sub-folder under `custom/options`.
 
diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index 32b352da4b..05c41a6a02 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -222,7 +222,7 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages
         <a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>.
         </p>
         {{if not (eq .Body "")}}
-            <h3>Message content:</h3>
+            <h3>Message content</h3>
             <hr>
             {{.Body | Str2html}}
         {{end}}
@@ -245,7 +245,7 @@ This template produces something along these lines:
 
 > [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#).
 >
-> #### Message content:
+> #### Message content
 >
 > \_********************************\_********************************
 >
diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md
index 588f0b2ccb..4846f6f398 100644
--- a/docs/content/administration/mail-templates.zh-cn.md
+++ b/docs/content/administration/mail-templates.zh-cn.md
@@ -228,7 +228,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 
 > [@rhonda](#)(Rhonda Myers)更新了 [mike/stuff#38](#)。
 >
-> #### 消息内容:
+> #### 消息内容
 >
 > \_********************************\_********************************
 >
diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index edd89e1231..a33a38a6f9 100644
--- a/docs/content/contributing/guidelines-frontend.en-us.md
+++ b/docs/content/contributing/guidelines-frontend.en-us.md
@@ -34,7 +34,7 @@ The source files can be found in the following directories:
 
 We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)
 
-### Gitea specific guidelines:
+### Gitea specific guidelines
 
 1. Every feature (Fomantic-UI/jQuery module) should be put in separate files/directories.
 2. HTML ids and classes should use kebab-case, it's preferred to contain 2-3 feature related keywords.
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index 365144ee7c..43f72b4808 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -34,7 +34,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 
 我们推荐使用[Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)和[Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)。
 
-## Gitea 特定准则:
+## Gitea 特定准则
 
 1. 每个功能(Fomantic-UI/jQuery 模块)应放在单独的文件/目录中。
 2. HTML 的 id 和 class 应使用 kebab-case,最好包含2-3个与功能相关的关键词。
diff --git a/docs/content/development/api-usage.zh-cn.md b/docs/content/development/api-usage.zh-cn.md
index 96c1997294..d7aca16f7f 100644
--- a/docs/content/development/api-usage.zh-cn.md
+++ b/docs/content/development/api-usage.zh-cn.md
@@ -60,7 +60,7 @@ curl "http://localhost:4000/api/v1/repos/test1/test1/issues" \
 `/users/:name/tokens` 是一个特殊的接口,需要您使用 basic authentication 进行认证,具体原因在 issue 中
 [#3842](https://github.com/go-gitea/gitea/issues/3842#issuecomment-397743346) 有所提及,使用方法如下所示:
 
-### 使用 Basic authentication 认证:
+### 使用 Basic authentication 认证
 
 ```
 $ curl --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens

From aa6f88638fb827d5c5ed7506e5fc06dad92beea7 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 17 Feb 2024 15:42:52 +0200
Subject: [PATCH 069/679] Fix missing template for follow button in
 organization (#29215)

Leftover from https://github.com/go-gitea/gitea/pull/29005

# Before

![before](https://github.com/go-gitea/gitea/assets/20454870/24c74278-ccac-4dc6-bf26-713e90c07239)

# After

![after](https://github.com/go-gitea/gitea/assets/20454870/f91d503b-87d4-4c17-a56c-9c0a81fd9082)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 routers/web/user/profile.go        | 2 +-
 templates/org/follow_unfollow.tmpl | 7 +++++++
 templates/org/home.tmpl            | 8 +-------
 3 files changed, 9 insertions(+), 8 deletions(-)
 create mode 100644 templates/org/follow_unfollow.tmpl

diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index e7f133e981..37ce450530 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -29,7 +29,7 @@ import (
 
 const (
 	tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar"
-	tplFollowUnfollow   base.TplName = "shared/user/follow_unfollow"
+	tplFollowUnfollow   base.TplName = "org/follow_unfollow"
 )
 
 // OwnerProfile render profile page for a user or a organization (aka, repo owner)
diff --git a/templates/org/follow_unfollow.tmpl b/templates/org/follow_unfollow.tmpl
new file mode 100644
index 0000000000..b9a3bb77fe
--- /dev/null
+++ b/templates/org/follow_unfollow.tmpl
@@ -0,0 +1,7 @@
+<button class="ui basic button gt-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
+	{{if $.IsFollowing}}
+		{{ctx.Locale.Tr "user.unfollow"}}
+	{{else}}
+		{{ctx.Locale.Tr "user.follow"}}
+	{{end}}
+</button>
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index 322be3271d..81a76d3b4d 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -26,13 +26,7 @@
 			</a>
 			{{end}}
 			{{if .IsSigned}}
-				<button class="ui basic button gt-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
-					{{if $.IsFollowing}}
-						{{ctx.Locale.Tr "user.unfollow"}}
-					{{else}}
-						{{ctx.Locale.Tr "user.follow"}}
-					{{end}}
-				</button>
+				{{template "org/follow_unfollow" .}}
 			{{end}}
 		</div>
 	</div>

From 22b9c2c95c30e77ec444c629eb323325d2c04676 Mon Sep 17 00:00:00 2001
From: Jimmy Praet <jimmy.praet@telenet.be>
Date: Sat, 17 Feb 2024 15:07:56 +0100
Subject: [PATCH 070/679] Load outdated comments when (un)resolving
 conversation on PR timeline (#29203)

Relates to #28654, #29039 and #29050.

The "show outdated comments" flag should only apply to the file diff
view.
On the PR timeline, outdated comments are always shown.
So they should also be loaded when (un)resolving a conversation on the
timeline page.
---
 routers/web/repo/pull_review.go      | 3 ++-
 routers/web/repo/pull_review_test.go | 4 ++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 217f2dea6d..f84510b39d 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -156,7 +156,8 @@ func UpdateResolveConversation(ctx *context.Context) {
 func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) {
 	ctx.Data["PageIsPullFiles"] = origin == "diff"
 
-	comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool))
+	showOutdatedComments := origin == "timeline" || ctx.Data["ShowOutdatedComments"].(bool)
+	comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, showOutdatedComments)
 	if err != nil {
 		ctx.ServerError("FetchCodeCommentsByLine", err)
 		return
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
index 65019af40b..7e6594774a 100644
--- a/routers/web/repo/pull_review_test.go
+++ b/routers/web/repo/pull_review_test.go
@@ -68,9 +68,9 @@ func TestRenderConversation(t *testing.T) {
 		renderConversation(ctx, preparedComment, "timeline")
 		assert.Contains(t, resp.Body.String(), `<div id="code-comments-`)
 	})
-	run("timeline without outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+	run("timeline is not affected by ShowOutdatedComments=false", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
 		ctx.Data["ShowOutdatedComments"] = false
 		renderConversation(ctx, preparedComment, "timeline")
-		assert.Contains(t, resp.Body.String(), `conversation-not-existing`)
+		assert.Contains(t, resp.Body.String(), `<div id="code-comments-`)
 	})
 }

From 0157db84b13203877c098a258abeb387d59f3486 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 17 Feb 2024 16:32:43 +0200
Subject: [PATCH 071/679] Fix label `for` pointing to a `name` instead of `id`
 in webhook settings (#29209)

Here's the spec for the `for` attribute:
https://html.spec.whatwg.org/multipage/forms.html#attr-label-for

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 templates/repo/settings/webhook/settings.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index addf99d45a..3dfa094cf5 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -254,7 +254,7 @@
 <!-- Branch filter -->
 <div class="field">
 	<label for="branch_filter">{{ctx.Locale.Tr "repo.settings.branch_filter"}}</label>
-	<input name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
+	<input id="branch_filter" name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
 	<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" | Str2html}}</span>
 </div>
 

From 1d275c1748a75a01c270f5c306c5248808016aba Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 17 Feb 2024 17:01:25 +0200
Subject: [PATCH 072/679] Fix labels referencing the wrong ID in the user
 profile settings (#29199)

2 instances of `for` with a wrong value and 1 `for` that had a reference
to a `name` instead of `id`.

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 templates/user/settings/profile.tmpl | 12 ++++++------
 tests/integration/auth_ldap_test.go  |  2 +-
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 1f32aed0e8..d1c68656b6 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -22,8 +22,8 @@
 					<input id="full_name" name="full_name" value="{{.SignedUser.FullName}}" maxlength="100">
 				</div>
 				<div class="field {{if .Err_Email}}error{{end}}">
-					<label for="email">{{ctx.Locale.Tr "email"}}</label>
-					<p>{{.SignedUser.Email}}</p>
+					<label>{{ctx.Locale.Tr "email"}}</label>
+					<p id="signed-user-email">{{.SignedUser.Email}}</p>
 				</div>
 				<div class="field {{if .Err_Description}}error{{end}}">
 					<label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label>
@@ -42,11 +42,11 @@
 				<!-- private block -->
 
 				<div class="field" id="privacy-user-settings">
-					<label for="security-private"><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label>
+					<label><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label>
 				</div>
 
 				<div class="inline field {{if .Err_Visibility}}error{{end}}">
-					<span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span>
+					<span class="inline required field"><label>{{ctx.Locale.Tr "settings.visibility"}}</label></span>
 					<div class="ui selection type dropdown">
 						{{if .SignedUser.Visibility.IsPublic}}<input type="hidden" id="visibility" name="visibility" value="0">{{end}}
 						{{if .SignedUser.Visibility.IsLimited}}<input type="hidden" id="visibility" name="visibility" value="1">{{end}}
@@ -120,8 +120,8 @@
 				</div>
 
 				<div class="inline field gt-pl-4">
-					<label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
-					<input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
+					<label for="new-avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
+					<input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
 				</div>
 
 				<div class="field">
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 2d69dfcfd7..3a5fdb97a6 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -179,7 +179,7 @@ func TestLDAPUserSignin(t *testing.T) {
 
 	assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
 	assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
-	assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
+	assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text())
 }
 
 func TestLDAPAuthChange(t *testing.T) {

From 3da2c63354eb3804c7aec3c688b066b044f2c30e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 17 Feb 2024 19:51:35 +0200
Subject: [PATCH 073/679] Remove unneccesary `initUserAuthLinkAccountView` from
 "link account" page (#29217)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/user-auth.js | 28 ----------------------------
 web_src/js/index.js              |  3 +--
 2 files changed, 1 insertion(+), 30 deletions(-)

diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js
index af380dcfc7..60d186e699 100644
--- a/web_src/js/features/user-auth.js
+++ b/web_src/js/features/user-auth.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
 import {checkAppUrl} from './common-global.js';
 
 export function initUserAuthOauth2() {
@@ -21,30 +20,3 @@ export function initUserAuthOauth2() {
     });
   }
 }
-
-export function initUserAuthLinkAccountView() {
-  const $lnkUserPage = $('.page-content.user.link-account');
-  if ($lnkUserPage.length === 0) {
-    return false;
-  }
-
-  const $signinTab = $lnkUserPage.find('.item[data-tab="auth-link-signin-tab"]');
-  const $signUpTab = $lnkUserPage.find('.item[data-tab="auth-link-signup-tab"]');
-  const $signInView = $lnkUserPage.find('.tab[data-tab="auth-link-signin-tab"]');
-  const $signUpView = $lnkUserPage.find('.tab[data-tab="auth-link-signup-tab"]');
-
-  $signUpTab.on('click', () => {
-    $signinTab.removeClass('active');
-    $signInView.removeClass('active');
-    $signUpTab.addClass('active');
-    $signUpView.addClass('active');
-    return false;
-  });
-
-  $signinTab.on('click', () => {
-    $signUpTab.removeClass('active');
-    $signUpView.removeClass('active');
-    $signinTab.addClass('active');
-    $signInView.addClass('active');
-  });
-}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 078f9fc9df..117279c3c4 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -23,7 +23,7 @@ import {initFindFileInRepo} from './features/repo-findfile.js';
 import {initCommentContent, initMarkupContent} from './markup/content.js';
 import {initPdfViewer} from './render/pdf.js';
 
-import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js';
+import {initUserAuthOauth2} from './features/user-auth.js';
 import {
   initRepoIssueDue,
   initRepoIssueReferenceRepositorySearch,
@@ -178,7 +178,6 @@ onDomReady(() => {
   initCommitStatuses();
   initCaptcha();
 
-  initUserAuthLinkAccountView();
   initUserAuthOauth2();
   initUserAuthWebAuthn();
   initUserAuthWebAuthnRegister();

From 5e1bf3efe2ad3ba6cd30db187ca59b94c3fcdafa Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 17 Feb 2024 22:07:47 +0200
Subject: [PATCH 074/679] Remove jQuery from repo migrate page (#29219)

- Switched to plain JavaScript
- Tested the repo migrate functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/44ad134b-832e-44b8-8e77-7cc8603d95fe)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-migrate.js | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/web_src/js/features/repo-migrate.js b/web_src/js/features/repo-migrate.js
index cae28fdd1b..490e7df0e4 100644
--- a/web_src/js/features/repo-migrate.js
+++ b/web_src/js/features/repo-migrate.js
@@ -1,18 +1,17 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 import {GET, POST} from '../modules/fetch.js';
 
 const {appSubUrl} = window.config;
 
 export function initRepoMigrationStatusChecker() {
-  const $repoMigrating = $('#repo_migrating');
-  if (!$repoMigrating.length) return;
+  const repoMigrating = document.getElementById('repo_migrating');
+  if (!repoMigrating) return;
 
-  $('#repo_migrating_retry').on('click', doMigrationRetry);
+  document.getElementById('repo_migrating_retry').addEventListener('click', doMigrationRetry);
 
-  const task = $repoMigrating.attr('data-migrating-task-id');
+  const task = repoMigrating.getAttribute('data-migrating-task-id');
 
-  // returns true if the refresh still need to be called after a while
+  // returns true if the refresh still needs to be called after a while
   const refresh = async () => {
     const res = await GET(`${appSubUrl}/user/task/${task}`);
     if (res.status !== 200) return true; // continue to refresh if network error occurs
@@ -21,7 +20,7 @@ export function initRepoMigrationStatusChecker() {
 
     // for all status
     if (data.message) {
-      $('#repo_migrating_progress_message').text(data.message);
+      document.getElementById('repo_migrating_progress_message').textContent = data.message;
     }
 
     // TaskStatusFinished
@@ -37,7 +36,7 @@ export function initRepoMigrationStatusChecker() {
       showElem('#repo_migrating_retry');
       showElem('#repo_migrating_failed');
       showElem('#repo_migrating_failed_image');
-      $('#repo_migrating_failed_error').text(data.message);
+      document.getElementById('repo_migrating_failed_error').textContent = data.message;
       return false;
     }
 
@@ -59,6 +58,6 @@ export function initRepoMigrationStatusChecker() {
 }
 
 async function doMigrationRetry(e) {
-  await POST($(e.target).attr('data-migrating-task-retry-url'));
+  await POST(e.target.getAttribute('data-migrating-task-retry-url'));
   window.location.reload();
 }

From 658cbddbfbe219d5988fcbf308e0d8180176725f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 18 Feb 2024 04:48:10 +0800
Subject: [PATCH 075/679] Make submit event code work with both jQuery event
 and native event (#29223)

Partially related to #29200 and fix other potential bugs.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/common-issue-list.js | 2 +-
 web_src/js/features/repo-diff.js         | 2 +-
 web_src/js/utils/dom.js                  | 1 +
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/web_src/js/features/common-issue-list.js b/web_src/js/features/common-issue-list.js
index 317c11219b..8182f99f29 100644
--- a/web_src/js/features/common-issue-list.js
+++ b/web_src/js/features/common-issue-list.js
@@ -40,7 +40,7 @@ export function initCommonIssueListQuickGoto() {
   $form.on('submit', (e) => {
     // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
     let doQuickGoto = !isElemHidden($goto);
-    const submitter = submitEventSubmitter(e.originalEvent);
+    const submitter = submitEventSubmitter(e);
     if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false;
     if (!doQuickGoto) return;
 
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index eeb80e91b2..6d6f382613 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -58,7 +58,7 @@ function initRepoDiffConversationForm() {
       const formData = new FormData($form[0]);
 
       // if the form is submitted by a button, append the button's name and value to the form data
-      const submitter = submitEventSubmitter(e.originalEvent);
+      const submitter = submitEventSubmitter(e);
       const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit');
       if (isSubmittedByButton && submitter.name) {
         formData.append(submitter.name, submitter.value);
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index 4dc55a518a..fb6b751140 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -211,6 +211,7 @@ export function loadElem(el, src) {
 const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined';
 
 export function submitEventSubmitter(e) {
+  e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself
   return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter;
 }
 

From d73223bfc6fcabdfb4ca284729ccead5ba228728 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 18 Feb 2024 03:22:09 +0200
Subject: [PATCH 076/679] Remove jQuery from the repo release form (#29225)

- Switched to plain JavaScript
- Tested the repo release form functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/ede2072a-823d-418f-9890-a5a7445a1cc6)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/repo-release.js | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js
index 3338c2874b..2db8079009 100644
--- a/web_src/js/features/repo-release.js
+++ b/web_src/js/features/repo-release.js
@@ -1,19 +1,19 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 
 export function initRepoRelease() {
-  $(document).on('click', '.remove-rel-attach', function() {
-    const uuid = $(this).data('uuid');
-    const id = $(this).data('id');
-    $(`input[name='attachment-del-${uuid}']`).attr('value', true);
-    hideElem($(`#attachment-${id}`));
+  document.addEventListener('click', (e) => {
+    if (e.target.matches('.remove-rel-attach')) {
+      const uuid = e.target.getAttribute('data-uuid');
+      const id = e.target.getAttribute('data-id');
+      document.querySelector(`input[name='attachment-del-${uuid}']`).value = 'true';
+      hideElem(`#attachment-${id}`);
+    }
   });
 }
 
 export function initRepoReleaseNew() {
-  const $repoReleaseNew = $('.repository.new.release');
-  if (!$repoReleaseNew.length) return;
+  if (!document.querySelector('.repository.new.release')) return;
 
   initTagNameEditor();
   initRepoReleaseEditor();
@@ -45,9 +45,9 @@ function initTagNameEditor() {
 }
 
 function initRepoReleaseEditor() {
-  const $editor = $('.repository.new.release .combo-markdown-editor');
-  if ($editor.length === 0) {
+  const editor = document.querySelector('.repository.new.release .combo-markdown-editor');
+  if (!editor) {
     return;
   }
-  const _promise = initComboMarkdownEditor($editor);
+  initComboMarkdownEditor(editor);
 }

From a784ed3d6c6946fd9bf95f2e910f52f549326fe2 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 18 Feb 2024 09:48:59 +0800
Subject: [PATCH 077/679] Use "Safe" modifier for manually constructed safe
 HTML strings in templates (#29227)

Follow #29165. These HTML strings are safe to be rendered directly, to
avoid double-escaping.
---
 templates/admin/packages/list.tmpl              | 2 +-
 templates/admin/repo/list.tmpl                  | 2 +-
 templates/admin/stacktrace.tmpl                 | 2 +-
 templates/org/member/members.tmpl               | 4 ++--
 templates/org/team/members.tmpl                 | 2 +-
 templates/org/team/sidebar.tmpl                 | 2 +-
 templates/org/team/teams.tmpl                   | 2 +-
 templates/repo/commit_page.tmpl                 | 4 ++--
 templates/repo/issue/view_content/comments.tmpl | 4 ++--
 templates/repo/issue/view_content/pull.tmpl     | 2 +-
 templates/repo/settings/webhook/settings.tmpl   | 2 +-
 templates/user/settings/organization.tmpl       | 2 +-
 12 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 5cfd9ddefa..04f76748d0 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -88,7 +88,7 @@
 		{{ctx.Locale.Tr "packages.settings.delete"}}
 	</div>
 	<div class="content">
-		{{ctx.Locale.Tr "packages.settings.delete.notice" `<span class="name"></span>` `<span class="dataVersion"></span>` | Safe}}
+		{{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|Safe) (`<span class="dataVersion"></span>`|Safe)}}
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index fdba0734a2..c7a6ec7e4e 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -101,7 +101,7 @@
 	</div>
 	<div class="content">
 		<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
-		{{ctx.Locale.Tr "repo.settings.delete_notices_2" `<span class="name"></span>` | Safe}}<br>
+		{{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|Safe)}}<br>
 		{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}<br>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index 894e41f8d7..aa5e810cd7 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -39,7 +39,7 @@
 		{{ctx.Locale.Tr "admin.monitor.process.cancel"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" `<span class="name"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|Safe)}}</p>
 		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl
index e4ddb69805..03509ec93e 100644
--- a/templates/org/member/members.tmpl
+++ b/templates/org/member/members.tmpl
@@ -73,7 +73,7 @@
 		{{ctx.Locale.Tr "org.members.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
@@ -82,7 +82,7 @@
 		{{ctx.Locale.Tr "org.members.remove"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataOrganizationName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|Safe) (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index da63d82967..dd4ece1433 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -81,7 +81,7 @@
 		{{ctx.Locale.Tr "org.members.remove"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataTeamName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|Safe) (`<span class="dataTeamName"></span>`|Safe)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl
index 29e7cf7cdd..37550ab71f 100644
--- a/templates/org/team/sidebar.tmpl
+++ b/templates/org/team/sidebar.tmpl
@@ -88,7 +88,7 @@
 		{{ctx.Locale.Tr "org.teams.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|Safe)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl
index f4ceada2a7..b518d7d9d7 100644
--- a/templates/org/team/teams.tmpl
+++ b/templates/org/team/teams.tmpl
@@ -49,7 +49,7 @@
 		{{ctx.Locale.Tr "org.teams.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|Safe)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 01fa45babe..ce9fcecd8b 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -88,7 +88,7 @@
 												{{.CsrfTokenHtml}}
 												<div class="field">
 													<label>
-														{{ctx.Locale.Tr "repo.branch.new_branch_from" `<span class="text" id="modal-create-branch-from-span"></span>` | Safe}}
+														{{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|Safe)}}
 													</label>
 												</div>
 												<div class="required field">
@@ -113,7 +113,7 @@
 												<input type="hidden" name="create_tag" value="true">
 												<div class="field">
 													<label>
-														{{ctx.Locale.Tr "repo.tag.create_tag_from" `<span class="text" id="modal-create-tag-from-span"></span>` | Safe}}
+														{{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|Safe)}}
 													</label>
 												</div>
 												<div class="required field">
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index c1797ba77d..ed83377f5a 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -112,9 +112,9 @@
 					{{template "shared/user/authorlink" .Poster}}
 					{{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}}
 					{{if eq $.Issue.PullRequest.Status 3}}
-						{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID) | Safe) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape) | Safe) $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID) | Safe) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape) | Safe) $createdStr}}
 					{{end}}
 				</span>
 			</div>
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index f1ab53eb67..a28b849f98 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -38,7 +38,7 @@
 								{{ctx.Locale.Tr "repo.pulls.merged_success"}}
 							</h3>
 							<div class="merge-section-info">
-								{{ctx.Locale.Tr "repo.pulls.merged_info_text" (printf "<code>%s</code>" (.HeadTarget | Escape)) | Str2html}}
+								{{ctx.Locale.Tr "repo.pulls.merged_info_text" (printf "<code>%s</code>" (.HeadTarget | Escape) | Safe)}}
 							</div>
 						</div>
 						<div class="item-section-right">
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index 3dfa094cf5..8e2387067e 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -263,7 +263,7 @@
 	<label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label>
 	<input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}>
 	{{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}}
-		<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" "<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Str2html}}</span>
+		<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Safe)}}</span>
 	{{end}}
 </div>
 
diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl
index 8079521984..102ff2e95b 100644
--- a/templates/user/settings/organization.tmpl
+++ b/templates/user/settings/organization.tmpl
@@ -47,7 +47,7 @@
 		{{ctx.Locale.Tr "org.members.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>

From 31bb9f3247388b993c61a10190cfd512408ce57e Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 18 Feb 2024 17:52:02 +0800
Subject: [PATCH 078/679] Refactor more code in templates  (#29236)

Follow #29165.

* Introduce JSONTemplate to help to render JSON templates
* Introduce JSEscapeSafe for templates. Now only use `{{ ... |
JSEscape}}` instead of `{{ ... | JSEscape | Safe}}`
* Simplify "UserLocationMapURL" useage
---
 Makefile                                      |  4 ++--
 modules/context/context_response.go           | 14 ++++++++++++++
 modules/templates/helper.go                   |  6 +++++-
 modules/templates/helper_test.go              |  4 ++++
 routers/api/v1/api.go                         |  4 ++--
 routers/web/auth/oauth.go                     | 10 +---------
 routers/web/shared/user/header.go             |  4 +++-
 routers/web/swagger_json.go                   | 14 +-------------
 templates/shared/user/profile_big_avatar.tmpl |  5 ++---
 templates/swagger/v1_json.tmpl                |  8 ++++----
 templates/user/auth/oidc_wellknown.tmpl       | 14 +++++++-------
 11 files changed, 45 insertions(+), 42 deletions(-)

diff --git a/Makefile b/Makefile
index 3065d9e683..925fdcb946 100644
--- a/Makefile
+++ b/Makefile
@@ -164,8 +164,8 @@ ifdef DEPS_PLAYWRIGHT
 endif
 
 SWAGGER_SPEC := templates/swagger/v1_json.tmpl
-SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g
-SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g
+SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g
+SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g
 SWAGGER_EXCLUDE := code.gitea.io/sdk
 SWAGGER_NEWLINE_COMMAND := -e '$$a\'
 
diff --git a/modules/context/context_response.go b/modules/context/context_response.go
index d9102b77bd..829bca1f59 100644
--- a/modules/context/context_response.go
+++ b/modules/context/context_response.go
@@ -90,6 +90,20 @@ func (ctx *Context) HTML(status int, name base.TplName) {
 	}
 }
 
+// JSONTemplate renders the template as JSON response
+// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
+func (ctx *Context) JSONTemplate(tmpl base.TplName) {
+	t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
+	if err != nil {
+		ctx.ServerError("unable to find template", err)
+		return
+	}
+	ctx.Resp.Header().Set("Content-Type", "application/json")
+	if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
+		ctx.ServerError("unable to execute template", err)
+	}
+}
+
 // RenderToString renders the template content to a string
 func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
 	var buf strings.Builder
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 9ff5d8927f..6e42594b0b 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap {
 		"Safe":        Safe,
 		"Escape":      Escape,
 		"QueryEscape": url.QueryEscape,
-		"JSEscape":    template.JSEscapeString,
+		"JSEscape":    JSEscapeSafe,
 		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
 		"URLJoin":     util.URLJoin,
 		"DotEscape":   DotEscape,
@@ -211,6 +211,10 @@ func Escape(s any) template.HTML {
 	panic(fmt.Sprintf("unexpected type %T", s))
 }
 
+func JSEscapeSafe(s string) template.HTML {
+	return template.HTML(template.JSEscapeString(s))
+}
+
 func RenderEmojiPlain(s any) any {
 	switch v := s.(type) {
 	case string:
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index ec83e9ac33..739a92f34f 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -52,3 +52,7 @@ func TestSubjectBodySeparator(t *testing.T) {
 		"",
 		"Insuficient\n--\nSeparators")
 }
+
+func TestJSEscapeSafe(t *testing.T) {
+	assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`))
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index f3082e4fa0..3fafb96b8e 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -6,9 +6,9 @@
 //
 // This documentation describes the Gitea API.
 //
-//	Schemes: http, https
+//	Schemes: https, http
 //	BasePath: /api/v1
-//	Version: {{AppVer | JSEscape | Safe}}
+//	Version: {{AppVer | JSEscape}}
 //	License: MIT http://opensource.org/licenses/MIT
 //
 //	Consumes:
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 07140b6674..660fa8fe4e 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -579,16 +579,8 @@ func GrantApplicationOAuth(ctx *context.Context) {
 
 // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
 func OIDCWellKnown(ctx *context.Context) {
-	t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil)
-	if err != nil {
-		ctx.ServerError("unable to find template", err)
-		return
-	}
-	ctx.Resp.Header().Set("Content-Type", "application/json")
 	ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
-	if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
-		ctx.ServerError("unable to execute template", err)
-	}
+	ctx.JSONTemplate("user/auth/oidc_wellknown")
 }
 
 // OIDCKeys generates the JSON Web Key Set
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index a2c0abb47e..a6c66a2c70 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -4,6 +4,8 @@
 package user
 
 import (
+	"net/url"
+
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -36,7 +38,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 
 	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
-	ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL
+	ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
 
 	// Show OpenID URIs
 	openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go
index 493c97aa67..42e9dbe967 100644
--- a/routers/web/swagger_json.go
+++ b/routers/web/swagger_json.go
@@ -4,22 +4,10 @@
 package web
 
 import (
-	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 )
 
-// tplSwaggerV1Json swagger v1 json template
-const tplSwaggerV1Json base.TplName = "swagger/v1_json"
-
 // SwaggerV1Json render swagger v1 json
 func SwaggerV1Json(ctx *context.Context) {
-	t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil)
-	if err != nil {
-		ctx.ServerError("unable to find template", err)
-		return
-	}
-	ctx.Resp.Header().Set("Content-Type", "application/json")
-	if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
-		ctx.ServerError("unable to execute template", err)
-	}
+	ctx.JSONTemplate("swagger/v1_json")
 }
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 4fbc43f541..9ea8334881 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -31,9 +31,8 @@
 				<li>
 					{{svg "octicon-location"}}
 					<span class="gt-f1">{{.ContextUser.Location}}</span>
-					{{if .UserLocationMapURL}}
-						{{/* We presume that the UserLocationMapURL is safe, as it is provided by the site administrator. */}}
-						<a href="{{.UserLocationMapURL | Safe}}{{.ContextUser.Location | QueryEscape}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
+					{{if .ContextUserLocationMapURL}}
+						<a href="{{.ContextUserLocationMapURL}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
 							{{svg "octicon-link-external"}}
 						</a>
 					{{end}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index a881afaf0e..d26bed53aa 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -8,8 +8,8 @@
     "text/html"
   ],
   "schemes": [
-    "http",
-    "https"
+    "https",
+    "http"
   ],
   "swagger": "2.0",
   "info": {
@@ -19,9 +19,9 @@
       "name": "MIT",
       "url": "http://opensource.org/licenses/MIT"
     },
-    "version": "{{AppVer | JSEscape | Safe}}"
+    "version": "{{AppVer | JSEscape}}"
   },
-  "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1",
+  "basePath": "{{AppSubUrl | JSEscape}}/api/v1",
   "paths": {
     "/activitypub/user-id/{user-id}": {
       "get": {
diff --git a/templates/user/auth/oidc_wellknown.tmpl b/templates/user/auth/oidc_wellknown.tmpl
index 38e6900c38..54bb4a763d 100644
--- a/templates/user/auth/oidc_wellknown.tmpl
+++ b/templates/user/auth/oidc_wellknown.tmpl
@@ -1,16 +1,16 @@
 {
-    "issuer": "{{AppUrl | JSEscape | Safe}}",
-    "authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize",
-    "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
-    "jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
-    "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
-    "introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
+    "issuer": "{{AppUrl | JSEscape}}",
+    "authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize",
+    "token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token",
+    "jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys",
+    "userinfo_endpoint": "{{AppUrl | JSEscape}}login/oauth/userinfo",
+    "introspection_endpoint": "{{AppUrl | JSEscape}}login/oauth/introspect",
     "response_types_supported": [
         "code",
         "id_token"
     ],
     "id_token_signing_alg_values_supported": [
-        "{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}"
+        "{{.SigningKey.SigningMethod.Alg | JSEscape}}"
     ],
     "subject_types_supported": [
         "public"

From 7430eb9e7f04a2923cee1f144947cf5fcce39ef8 Mon Sep 17 00:00:00 2001
From: zhangnew <9146834+zhangnew@users.noreply.github.com>
Date: Sun, 18 Feb 2024 18:04:58 +0800
Subject: [PATCH 079/679] Update docs for actions variables (#29239)

the variables is supported, see
https://github.com/go-gitea/gitea/blob/a784ed3d6c6946fd9bf95f2e910f52f549326fe2/docs/content/usage/actions/act-runner.zh-cn.md?plain=1#L262-L289
---
 docs/content/usage/actions/comparison.zh-cn.md | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/docs/content/usage/actions/comparison.zh-cn.md b/docs/content/usage/actions/comparison.zh-cn.md
index dbe9ca007d..16b2181ba2 100644
--- a/docs/content/usage/actions/comparison.zh-cn.md
+++ b/docs/content/usage/actions/comparison.zh-cn.md
@@ -95,12 +95,6 @@ Gitea Actions目前不支持此功能,如果使用它,结果将始终为空
 
 ## 缺失的功能
 
-### 变量
-
-请参阅[变量](https://docs.github.com/zh/actions/learn-github-actions/variables)。
-
-目前变量功能正在开发中。
-
 ### 问题匹配器
 
 问题匹配器是一种扫描Actions输出以查找指定正则表达式模式并在用户界面中突出显示该信息的方法。

From 67adc5c1dc3470dab96053c2e77351f3a3f8062b Mon Sep 17 00:00:00 2001
From: FuXiaoHei <fuxiaohei@vip.qq.com>
Date: Sun, 18 Feb 2024 18:33:50 +0800
Subject: [PATCH 080/679] Artifact deletion in actions ui (#27172)

Add deletion link in runs view page.
Fix #26315


![image](https://github.com/go-gitea/gitea/assets/2142787/aa65a4ab-f434-4deb-b953-21e63c212033)

When click deletion button. It marks this artifact `need-delete`.

This artifact would be deleted when actions cleanup cron task.
---
 models/actions/artifact.go               | 22 ++++++++++
 options/locale/locale_en-US.ini          |  1 +
 routers/web/repo/actions/view.go         | 51 +++++++++++++++++++-----
 routers/web/web.go                       |  1 +
 services/actions/cleanup.go              | 38 +++++++++++++++++-
 templates/repo/actions/view.tmpl         |  1 +
 web_src/js/components/RepoActionView.vue | 15 ++++++-
 web_src/js/svg.js                        |  2 +
 8 files changed, 120 insertions(+), 11 deletions(-)

diff --git a/models/actions/artifact.go b/models/actions/artifact.go
index 5390f6288f..3d0a288e62 100644
--- a/models/actions/artifact.go
+++ b/models/actions/artifact.go
@@ -26,6 +26,8 @@ const (
 	ArtifactStatusUploadConfirmed                           // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed
 	ArtifactStatusUploadError                               // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored
 	ArtifactStatusExpired                                   // 4, ArtifactStatusExpired is the status of an artifact that is expired
+	ArtifactStatusPendingDeletion                           // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion
+	ArtifactStatusDeleted                                   // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
 )
 
 func init() {
@@ -147,8 +149,28 @@ func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) {
 		Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts)
 }
 
+// ListPendingDeleteArtifacts returns all artifacts in pending-delete status.
+// limit is the max number of artifacts to return.
+func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifact, error) {
+	arts := make([]*ActionArtifact, 0, limit)
+	return arts, db.GetEngine(ctx).
+		Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts)
+}
+
 // SetArtifactExpired sets an artifact to expired
 func SetArtifactExpired(ctx context.Context, artifactID int64) error {
 	_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)})
 	return err
 }
+
+// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
+func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
+	_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)})
+	return err
+}
+
+// SetArtifactDeleted sets an artifact to deleted
+func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
+	_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)})
+	return err
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 5f34bc4c1d..e46d6f38c0 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -123,6 +123,7 @@ pin = Pin
 unpin = Unpin
 
 artifacts = Artifacts
+confirm_delete_artifact = Are you sure you want to delete the artifact '%s' ?
 
 archived = Archived
 
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 59fb25b680..49387362b3 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -57,15 +57,16 @@ type ViewRequest struct {
 type ViewResponse struct {
 	State struct {
 		Run struct {
-			Link       string     `json:"link"`
-			Title      string     `json:"title"`
-			Status     string     `json:"status"`
-			CanCancel  bool       `json:"canCancel"`
-			CanApprove bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
-			CanRerun   bool       `json:"canRerun"`
-			Done       bool       `json:"done"`
-			Jobs       []*ViewJob `json:"jobs"`
-			Commit     ViewCommit `json:"commit"`
+			Link              string     `json:"link"`
+			Title             string     `json:"title"`
+			Status            string     `json:"status"`
+			CanCancel         bool       `json:"canCancel"`
+			CanApprove        bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
+			CanRerun          bool       `json:"canRerun"`
+			CanDeleteArtifact bool       `json:"canDeleteArtifact"`
+			Done              bool       `json:"done"`
+			Jobs              []*ViewJob `json:"jobs"`
+			Commit            ViewCommit `json:"commit"`
 		} `json:"run"`
 		CurrentJob struct {
 			Title  string         `json:"title"`
@@ -146,6 +147,7 @@ func ViewPost(ctx *context_module.Context) {
 	resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
+	resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.Done = run.Status.IsDone()
 	resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
 	resp.State.Run.Status = run.Status.String()
@@ -535,6 +537,29 @@ func ArtifactsView(ctx *context_module.Context) {
 	ctx.JSON(http.StatusOK, artifactsResponse)
 }
 
+func ArtifactsDeleteView(ctx *context_module.Context) {
+	if !ctx.Repo.CanWrite(unit.TypeActions) {
+		ctx.Error(http.StatusForbidden, "no permission")
+		return
+	}
+
+	runIndex := ctx.ParamsInt64("run")
+	artifactName := ctx.Params("artifact_name")
+
+	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+	if err != nil {
+		ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+			return errors.Is(err, util.ErrNotExist)
+		}, err)
+		return
+	}
+	if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+	ctx.JSON(http.StatusOK, struct{}{})
+}
+
 func ArtifactsDownloadView(ctx *context_module.Context) {
 	runIndex := ctx.ParamsInt64("run")
 	artifactName := ctx.Params("artifact_name")
@@ -562,6 +587,14 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
 		return
 	}
 
+	// if artifacts status is not uploaded-confirmed, treat it as not found
+	for _, art := range artifacts {
+		if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
+			ctx.Error(http.StatusNotFound, "artifact not found")
+			return
+		}
+	}
+
 	ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
 
 	writer := zip.NewWriter(ctx.Resp)
diff --git a/routers/web/web.go b/routers/web/web.go
index 0528b20328..864164972e 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1368,6 +1368,7 @@ func registerRoutes(m *web.Route) {
 				m.Post("/approve", reqRepoActionsWriter, actions.Approve)
 				m.Post("/artifacts", actions.ArtifactsView)
 				m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
+				m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
 				m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
 			})
 		}, reqRepoActionsReader, actions.MustEnableActions)
diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go
index 785eeb5838..59e2cc85de 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -20,8 +20,15 @@ func Cleanup(taskCtx context.Context, olderThan time.Duration) error {
 	return CleanupArtifacts(taskCtx)
 }
 
-// CleanupArtifacts removes expired artifacts and set records expired status
+// CleanupArtifacts removes expired add need-deleted artifacts and set records expired status
 func CleanupArtifacts(taskCtx context.Context) error {
+	if err := cleanExpiredArtifacts(taskCtx); err != nil {
+		return err
+	}
+	return cleanNeedDeleteArtifacts(taskCtx)
+}
+
+func cleanExpiredArtifacts(taskCtx context.Context) error {
 	artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx)
 	if err != nil {
 		return err
@@ -40,3 +47,32 @@ func CleanupArtifacts(taskCtx context.Context) error {
 	}
 	return nil
 }
+
+// deleteArtifactBatchSize is the batch size of deleting artifacts
+const deleteArtifactBatchSize = 100
+
+func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
+	for {
+		artifacts, err := actions.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize)
+		if err != nil {
+			return err
+		}
+		log.Info("Found %d artifacts pending deletion", len(artifacts))
+		for _, artifact := range artifacts {
+			if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
+				log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
+				continue
+			}
+			if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil {
+				log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err)
+				continue
+			}
+			log.Info("Artifact %d set deleted", artifact.ID)
+		}
+		if len(artifacts) < deleteArtifactBatchSize {
+			log.Debug("No more artifacts pending deletion")
+			break
+		}
+	}
+	return nil
+}
diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl
index 6b07e7000a..f8b106147b 100644
--- a/templates/repo/actions/view.tmpl
+++ b/templates/repo/actions/view.tmpl
@@ -19,6 +19,7 @@
 		data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}"
 		data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}"
 		data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}"
+		data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}"
 		data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}"
 		data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}"
 		data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}"
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 797869b78c..c4a7389bc5 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -5,7 +5,7 @@ import {createApp} from 'vue';
 import {toggleElem} from '../utils/dom.js';
 import {getCurrentLocale} from '../utils.js';
 import {renderAnsi} from '../render/ansi.js';
-import {POST} from '../modules/fetch.js';
+import {POST, DELETE} from '../modules/fetch.js';
 
 const sfc = {
   name: 'RepoActionView',
@@ -200,6 +200,12 @@ const sfc = {
       return await resp.json();
     },
 
+    async deleteArtifact(name) {
+      if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return;
+      await DELETE(`${this.run.link}/artifacts/${name}`);
+      await this.loadJob();
+    },
+
     async fetchJob() {
       const logCursors = this.currentJobStepsStates.map((it, idx) => {
         // cursor is used to indicate the last position of the logs
@@ -329,6 +335,8 @@ export function initRepositoryActionView() {
       cancel: el.getAttribute('data-locale-cancel'),
       rerun: el.getAttribute('data-locale-rerun'),
       artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
+      areYouSure: el.getAttribute('data-locale-are-you-sure'),
+      confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
       rerun_all: el.getAttribute('data-locale-rerun-all'),
       showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
       showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
@@ -404,6 +412,9 @@ export function initRepositoryActionView() {
               <a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
                 <SvgIcon name="octicon-file" class="ui text black job-artifacts-icon"/>{{ artifact.name }}
               </a>
+              <a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)" class="job-artifacts-delete">
+                <SvgIcon name="octicon-trash" class="ui text black job-artifacts-icon"/>
+              </a>
             </li>
           </ul>
         </div>
@@ -528,6 +539,8 @@ export function initRepositoryActionView() {
 .job-artifacts-item {
   margin: 5px 0;
   padding: 6px;
+  display: flex;
+  justify-content: space-between;
 }
 
 .job-artifacts-list {
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 084256587c..471b5136bd 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -67,6 +67,7 @@ import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethro
 import octiconSync from '../../public/assets/img/svg/octicon-sync.svg';
 import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
 import octiconTag from '../../public/assets/img/svg/octicon-tag.svg';
+import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg';
 import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
 import octiconX from '../../public/assets/img/svg/octicon-x.svg';
 import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg';
@@ -139,6 +140,7 @@ const svgs = {
   'octicon-sync': octiconSync,
   'octicon-table': octiconTable,
   'octicon-tag': octiconTag,
+  'octicon-trash': octiconTrash,
   'octicon-triangle-down': octiconTriangleDown,
   'octicon-x': octiconX,
   'octicon-x-circle-fill': octiconXCircleFill,

From 1a6e1cbada27db1e3327b0d7d331492c95e24759 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Sun, 18 Feb 2024 19:58:46 +0900
Subject: [PATCH 081/679] Implement some action notifier functions (#29173)

Fix #29166

Add support for the following activity types of `pull_request`
- assigned
- unassigned
- review_requested
- review_request_removed
- milestoned
- demilestoned
---
 modules/actions/github.go    |  4 +-
 modules/actions/workflows.go |  8 ++--
 services/actions/notifier.go | 76 +++++++++++++++++++++++++++++++-----
 3 files changed, 75 insertions(+), 13 deletions(-)

diff --git a/modules/actions/github.go b/modules/actions/github.go
index fafea4e11a..18917c5118 100644
--- a/modules/actions/github.go
+++ b/modules/actions/github.go
@@ -52,7 +52,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
 		case webhook_module.HookEventPullRequest,
 			webhook_module.HookEventPullRequestSync,
 			webhook_module.HookEventPullRequestAssign,
-			webhook_module.HookEventPullRequestLabel:
+			webhook_module.HookEventPullRequestLabel,
+			webhook_module.HookEventPullRequestReviewRequest,
+			webhook_module.HookEventPullRequestMilestone:
 			return true
 
 		default:
diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go
index a883f4181b..2db4a9296f 100644
--- a/modules/actions/workflows.go
+++ b/modules/actions/workflows.go
@@ -221,7 +221,9 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
 		webhook_module.HookEventPullRequest,
 		webhook_module.HookEventPullRequestSync,
 		webhook_module.HookEventPullRequestAssign,
-		webhook_module.HookEventPullRequestLabel:
+		webhook_module.HookEventPullRequestLabel,
+		webhook_module.HookEventPullRequestReviewRequest,
+		webhook_module.HookEventPullRequestMilestone:
 		return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt)
 
 	case // pull_request_review
@@ -397,13 +399,13 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 	} else {
 		// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
 		// Actions with the same name:
-		// opened, edited, closed, reopened, assigned, unassigned
+		// opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned
 		// Actions need to be converted:
 		// synchronized -> synchronize
 		// label_updated -> labeled
 		// label_cleared -> unlabeled
 		// Unsupported activity types:
-		// converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled
+		// converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued
 
 		action := prPayload.Action
 		switch action {
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 0b4fed5db1..093607f05c 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -101,11 +101,40 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
 		Notify(ctx)
 }
 
+// IssueChangeAssignee notifies assigned or unassigned to notifiers
+func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
+	ctx = withMethod(ctx, "IssueChangeAssignee")
+
+	var action api.HookIssueAction
+	if removed {
+		action = api.HookIssueUnassigned
+	} else {
+		action = api.HookIssueAssigned
+	}
+	notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestAssign, action)
+}
+
+// IssueChangeMilestone notifies assignee to notifiers
+func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
+	ctx = withMethod(ctx, "IssueChangeMilestone")
+
+	var action api.HookIssueAction
+	if issue.MilestoneID > 0 {
+		action = api.HookIssueMilestoned
+	} else {
+		action = api.HookIssueDemilestoned
+	}
+	notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestMilestone, action)
+}
+
 func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
 	_, _ []*issues_model.Label,
 ) {
 	ctx = withMethod(ctx, "IssueChangeLabels")
+	notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestLabel, api.HookIssueLabelUpdated)
+}
 
+func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {
 	var err error
 	if err = issue.LoadRepo(ctx); err != nil {
 		log.Error("LoadRepo: %v", err)
@@ -117,20 +146,15 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
 		return
 	}
 
-	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
 	if issue.IsPull {
 		if err = issue.LoadPullRequest(ctx); err != nil {
 			log.Error("loadPullRequest: %v", err)
 			return
 		}
-		if err = issue.PullRequest.LoadIssue(ctx); err != nil {
-			log.Error("LoadIssue: %v", err)
-			return
-		}
-		newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestLabel).
+		newNotifyInputFromIssue(issue, event).
 			WithDoer(doer).
 			WithPayload(&api.PullRequestPayload{
-				Action:      api.HookIssueLabelUpdated,
+				Action:      action,
 				Index:       issue.Index,
 				PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
 				Repository:  convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
@@ -140,10 +164,11 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
 			Notify(ctx)
 		return
 	}
-	newNotifyInputFromIssue(issue, webhook_module.HookEventIssueLabel).
+	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
+	newNotifyInputFromIssue(issue, event).
 		WithDoer(doer).
 		WithPayload(&api.IssuePayload{
-			Action:     api.HookIssueLabelUpdated,
+			Action:     action,
 			Index:      issue.Index,
 			Issue:      convert.ToAPIIssue(ctx, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
@@ -305,6 +330,39 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode
 		}).Notify(ctx)
 }
 
+func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
+	if !issue.IsPull {
+		log.Warn("PullRequestReviewRequest: issue is not a pull request: %v", issue.ID)
+		return
+	}
+
+	ctx = withMethod(ctx, "PullRequestReviewRequest")
+
+	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+	if err := issue.LoadPullRequest(ctx); err != nil {
+		log.Error("LoadPullRequest failed: %v", err)
+		return
+	}
+	var action api.HookIssueAction
+	if isRequest {
+		action = api.HookIssueReviewRequested
+	} else {
+		action = api.HookIssueReviewRequestRemoved
+	}
+	newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestReviewRequest).
+		WithDoer(doer).
+		WithPayload(&api.PullRequestPayload{
+			Action:            action,
+			Index:             issue.Index,
+			PullRequest:       convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+			RequestedReviewer: convert.ToUser(ctx, reviewer, nil),
+			Repository:        convert.ToRepo(ctx, issue.Repo, permission),
+			Sender:            convert.ToUser(ctx, doer, nil),
+		}).
+		WithPullRequest(issue.PullRequest).
+		Notify(ctx)
+}
+
 func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
 	ctx = withMethod(ctx, "MergePullRequest")
 

From 6093f507fe6f2d4802de8ec1ff5b04820e81571c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Nicas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Sun, 18 Feb 2024 12:47:50 +0100
Subject: [PATCH 082/679] Convert visibility to number (#29226)

Don't throw error while creating user (Fixes #29218)
---
 templates/admin/user/new.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl
index 81f70511d0..bcb53d8131 100644
--- a/templates/admin/user/new.tmpl
+++ b/templates/admin/user/new.tmpl
@@ -26,7 +26,7 @@
 				<div class="inline field {{if .Err_Visibility}}error{{end}}">
 					<span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span>
 					<div class="ui selection type dropdown">
-						<input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{.visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}">
+						<input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{printf "%d" .visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}">
 						<div class="text">
 							{{if .DefaultUserVisibilityMode.IsPublic}}{{ctx.Locale.Tr "settings.visibility.public"}}{{end}}
 							{{if .DefaultUserVisibilityMode.IsLimited}}{{ctx.Locale.Tr "settings.visibility.limited"}}{{end}}

From 4345cac52971c13debfe5e6f311aef3930fe2eed Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 18 Feb 2024 20:15:24 +0800
Subject: [PATCH 083/679] Improve TrHTML and add more tests (#29228)

Follow #29165.

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 modules/translation/i18n/i18n_test.go   | 66 +++++++++++++++++++++++++
 modules/translation/i18n/localestore.go |  8 +--
 2 files changed, 71 insertions(+), 3 deletions(-)

diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go
index ffe69a74df..b364992dfe 100644
--- a/modules/translation/i18n/i18n_test.go
+++ b/modules/translation/i18n/i18n_test.go
@@ -4,6 +4,7 @@
 package i18n
 
 import (
+	"html/template"
 	"strings"
 	"testing"
 
@@ -82,6 +83,71 @@ c=22
 	assert.Equal(t, "22", lang1.TrString("c"))
 }
 
+type stringerPointerReceiver struct {
+	s string
+}
+
+func (s *stringerPointerReceiver) String() string {
+	return s.s
+}
+
+type stringerStructReceiver struct {
+	s string
+}
+
+func (s stringerStructReceiver) String() string {
+	return s.s
+}
+
+type errorStructReceiver struct {
+	s string
+}
+
+func (e errorStructReceiver) Error() string {
+	return e.s
+}
+
+type errorPointerReceiver struct {
+	s string
+}
+
+func (e *errorPointerReceiver) Error() string {
+	return e.s
+}
+
+func TestLocaleWithTemplate(t *testing.T) {
+	ls := NewLocaleStore()
+	assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil))
+	lang1, _ := ls.Locale("lang1")
+
+	tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
+	tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`))
+
+	cases := []struct {
+		in   any
+		want string
+	}{
+		{"<str>", "<a>&lt;str&gt;</a>"},
+		{[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"},
+		{template.HTML("<html>"), "<a><html></a>"},
+		{stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{&lt;stringerPointerReceiver&gt;}</a>"},
+		{&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a>&lt;stringerPointerReceiver ptr&gt;</a>"},
+		{stringerStructReceiver{"<stringerStructReceiver>"}, "<a>&lt;stringerStructReceiver&gt;</a>"},
+		{&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a>&lt;stringerStructReceiver ptr&gt;</a>"},
+		{errorStructReceiver{"<errorStructReceiver>"}, "<a>&lt;errorStructReceiver&gt;</a>"},
+		{&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a>&lt;errorStructReceiver ptr&gt;</a>"},
+		{errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{&lt;errorPointerReceiver&gt;}</a>"},
+		{&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a>&lt;errorPointerReceiver ptr&gt;</a>"},
+	}
+
+	buf := &strings.Builder{}
+	for _, c := range cases {
+		buf.Reset()
+		assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in}))
+		assert.Equal(t, c.want, buf.String())
+	}
+}
+
 func TestLocaleStoreQuirks(t *testing.T) {
 	const nl = "\n"
 	q := func(q1, s string, q2 ...string) string {
diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go
index 69cc9fd91d..b422996984 100644
--- a/modules/translation/i18n/localestore.go
+++ b/modules/translation/i18n/localestore.go
@@ -133,12 +133,14 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
 	args := slices.Clone(trArgs)
 	for i, v := range args {
 		switch v := v.(type) {
+		case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
+			// for most basic types (including template.HTML which is safe), just do nothing and use it
 		case string:
-			args[i] = template.HTML(template.HTMLEscapeString(v))
+			args[i] = template.HTMLEscapeString(v)
 		case fmt.Stringer:
 			args[i] = template.HTMLEscapeString(v.String())
-		default: // int, float, include template.HTML
-			// do nothing, just use it
+		default:
+			args[i] = template.HTMLEscapeString(fmt.Sprint(v))
 		}
 	}
 	return template.HTML(l.TrString(trKey, args...))

From 68e1d17a5ffc200a8d270c12a199b5dde9c5290c Mon Sep 17 00:00:00 2001
From: FuXiaoHei <fuxiaohei@vip.qq.com>
Date: Sun, 18 Feb 2024 22:25:14 +0800
Subject: [PATCH 084/679] Expire artifacts before deleting them physically
 (#29241)

https://github.com/go-gitea/gitea/pull/27172#discussion_r1493735466

When cleanup artifacts, it removes storage first. If storage is not
exist (maybe delete manually), it gets error and continue loop. It makes
a dead loop if there are a lot pending but non-existing artifacts.

Now it updates db record at first to avoid keep a lot of pending status
artifacts.
---
 services/actions/cleanup.go | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go
index 59e2cc85de..5376c2624c 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -35,14 +35,14 @@ func cleanExpiredArtifacts(taskCtx context.Context) error {
 	}
 	log.Info("Found %d expired artifacts", len(artifacts))
 	for _, artifact := range artifacts {
-		if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
-			log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
-			continue
-		}
 		if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil {
 			log.Error("Cannot set artifact %d expired: %v", artifact.ID, err)
 			continue
 		}
+		if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
+			log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
+			continue
+		}
 		log.Info("Artifact %d set expired", artifact.ID)
 	}
 	return nil
@@ -59,14 +59,14 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
 		}
 		log.Info("Found %d artifacts pending deletion", len(artifacts))
 		for _, artifact := range artifacts {
-			if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
-				log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
-				continue
-			}
 			if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil {
 				log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err)
 				continue
 			}
+			if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
+				log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
+				continue
+			}
 			log.Info("Artifact %d set deleted", artifact.ID)
 		}
 		if len(artifacts) < deleteArtifactBatchSize {

From 39f8ab591c18a65cf783ecd17ddc1a5914ceff7a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 18 Feb 2024 15:51:21 +0100
Subject: [PATCH 085/679] Clean up diff header css and reduce global textarea
 min-height (#29232)

1. Tweak diff header and remove a numbe of unneeded CSS for it:

Before:
<img width="433" alt="Screenshot 2024-02-18 at 01 08 09"
src="https://github.com/go-gitea/gitea/assets/115237/d8b377c0-57bc-44d5-bb57-a582c7d4b3b4">

After:
<img width="463" alt="Screenshot 2024-02-18 at 01 07 56"
src="https://github.com/go-gitea/gitea/assets/115237/d08c17e7-5b86-4d07-81da-6371f4754325">

3. Reduce height of review textarea and also reduce fomantic's CSS from
12em to 8em. Now fits better on my screen:

<img width="1352" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/5c658d13-295e-4929-94da-13ade888020d">

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 templates/repo/commit_page.tmpl            |  2 +-
 templates/repo/diff/box.tmpl               |  2 +-
 web_src/css/base.css                       |  6 ++++++
 web_src/css/editor/combomarkdowneditor.css |  5 ++---
 web_src/css/repo.css                       | 18 +-----------------
 5 files changed, 11 insertions(+), 22 deletions(-)

diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index ce9fcecd8b..fbfaa19411 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -184,7 +184,7 @@
 				</div>
 		</div>
 		{{if .Commit.Signature}}
-			<div class="ui bottom attached message gt-text-left gt-df gt-ac gt-sb commit-header-row gt-fw {{$class}}">
+			<div class="ui bottom attached message gt-text-left gt-df gt-ac gt-sb commit-header-row gt-fw gt-mb-0 {{$class}}">
 				<div class="gt-df gt-ac">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index be7c7e80f2..5960decc06 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -1,7 +1,7 @@
 {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
 <div>
 	<div class="diff-detail-box diff-box">
-		<div class="gt-df gt-ac gt-fw">
+		<div class="gt-df gt-ac gt-fw gt-gap-3 gt-ml-1">
 			{{if $showFileTree}}
 				<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
 					{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 0d547f16ff..76ecfc9bf5 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -21,6 +21,7 @@
   --border-radius-circle: 50%;
   --opacity-disabled: 0.55;
   --height-loading: 16rem;
+  --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
   --tab-size: 4;
 }
 
@@ -492,6 +493,11 @@ ol.ui.list li,
   background: var(--color-active) !important;
 }
 
+.ui.form textarea:not([rows]) {
+  height: var(--min-height-textarea); /* override fomantic default 12em */
+  min-height: var(--min-height-textarea); /* override fomantic default 8em */
+}
+
 /* styles from removed fomantic transition module */
 .hidden.transition {
   visibility: hidden;
diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css
index 12ec1866a5..8a2f4ea416 100644
--- a/web_src/css/editor/combomarkdowneditor.css
+++ b/web_src/css/editor/combomarkdowneditor.css
@@ -37,13 +37,12 @@
 .combo-markdown-editor textarea.markdown-text-editor {
   display: block;
   width: 100%;
-  min-height: 200px;
-  max-height: calc(100vh - 200px);
+  max-height: calc(100vh - var(--min-height-textarea));
   resize: vertical;
 }
 
 .combo-markdown-editor .CodeMirror-scroll {
-  max-height: calc(100vh - 200px);
+  max-height: calc(100vh - var(--min-height-textarea));
 }
 
 /* use the same styles as markup/content.css */
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 610c3fcb55..31cff0ca15 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1498,12 +1498,6 @@
   background: var(--color-body);
 }
 
-@media (max-width: 991.98px) {
-  .repository .diff-detail-box {
-    flex-direction: row;
-  }
-}
-
 @media (max-width: 480px) {
   .repository .diff-detail-box {
     flex-wrap: wrap;
@@ -1528,7 +1522,7 @@
   color: var(--color-red);
 }
 
-@media (max-width: 991.98px) {
+@media (max-width: 800px) {
   .repository .diff-detail-box .diff-detail-stats {
     display: none !important;
   }
@@ -1538,7 +1532,6 @@
   display: flex;
   align-items: center;
   gap: 0.25em;
-  flex-wrap: wrap;
   justify-content: end;
 }
 
@@ -1548,15 +1541,6 @@
   margin-right: 0 !important;
 }
 
-@media (max-width: 480px) {
-  .repository .diff-detail-box .diff-detail-actions {
-    padding-top: 0.25rem;
-  }
-  .repository .diff-detail-box .diff-detail-actions .ui.button:not(.btn-submit) {
-    padding: 0 0.5rem;
-  }
-}
-
 .repository .diff-detail-box span.status {
   display: inline-block;
   width: 12px;

From c2a8aacae5242adbeb7bc1d4002492ae1cae47b2 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sun, 18 Feb 2024 23:16:34 +0800
Subject: [PATCH 086/679] Fix missed edit issues event for actions (#29237)

Fix #29213
---
 services/actions/notifier.go | 41 ++++++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 093607f05c..77848a3f58 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -55,6 +55,47 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
 	}).Notify(withMethod(ctx, "NewIssue"))
 }
 
+// IssueChangeContent notifies change content of issue
+func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
+	ctx = withMethod(ctx, "IssueChangeContent")
+
+	var err error
+	if err = issue.LoadRepo(ctx); err != nil {
+		log.Error("LoadRepo: %v", err)
+		return
+	}
+
+	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
+	if issue.IsPull {
+		if err = issue.LoadPullRequest(ctx); err != nil {
+			log.Error("loadPullRequest: %v", err)
+			return
+		}
+		newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequest).
+			WithDoer(doer).
+			WithPayload(&api.PullRequestPayload{
+				Action:      api.HookIssueEdited,
+				Index:       issue.Index,
+				PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+				Repository:  convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
+				Sender:      convert.ToUser(ctx, doer, nil),
+			}).
+			WithPullRequest(issue.PullRequest).
+			Notify(ctx)
+		return
+	}
+	newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).
+		WithDoer(doer).
+		WithPayload(&api.IssuePayload{
+			Action:     api.HookIssueEdited,
+			Index:      issue.Index,
+			Issue:      convert.ToAPIIssue(ctx, issue),
+			Repository: convert.ToRepo(ctx, issue.Repo, permission),
+			Sender:     convert.ToUser(ctx, doer, nil),
+		}).
+		Notify(ctx)
+}
+
 // IssueChangeStatus notifies close or reopen issue to notifiers
 func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) {
 	ctx = withMethod(ctx, "IssueChangeStatus")

From 8be198cdef0a486f417663b1fd6878458d7e5d92 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 19 Feb 2024 01:39:04 +0800
Subject: [PATCH 087/679] Use general token signing secret (#29205)

Use a clearly defined "signing secret" for token signing.
---
 modules/base/tool.go                         |  2 +-
 modules/context/context.go                   |  3 +-
 modules/setting/lfs.go                       | 21 ++++++------
 modules/setting/oauth2.go                    | 36 ++++++++++++++++----
 modules/setting/oauth2_test.go               | 34 ++++++++++++++++++
 services/actions/auth.go                     |  4 +--
 services/actions/auth_test.go                |  2 +-
 services/auth/source/oauth2/jwtsigningkey.go |  9 +----
 services/packages/auth.go                    |  4 +--
 9 files changed, 82 insertions(+), 33 deletions(-)
 create mode 100644 modules/setting/oauth2_test.go

diff --git a/modules/base/tool.go b/modules/base/tool.go
index e9f4dfa279..19fb2c451f 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -115,7 +115,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf any) string {
 
 	// create sha1 encode string
 	sh := sha1.New()
-	_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, setting.SecretKey, startStr, endStr, minutes)))
+	_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
 	encoded := hex.EncodeToString(sh.Sum(nil))
 
 	code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
diff --git a/modules/context/context.go b/modules/context/context.go
index 4d367b3242..66732eaa8a 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -6,6 +6,7 @@ package context
 
 import (
 	"context"
+	"encoding/hex"
 	"fmt"
 	"html/template"
 	"io"
@@ -124,7 +125,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
 func Contexter() func(next http.Handler) http.Handler {
 	rnd := templates.HTMLRenderer()
 	csrfOpts := CsrfOptions{
-		Secret:         setting.SecretKey,
+		Secret:         hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
 		Cookie:         setting.CSRFCookieName,
 		SetCookie:      true,
 		Secure:         setting.SessionConfig.Secure,
diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go
index 22a75f6008..2034ef782c 100644
--- a/modules/setting/lfs.go
+++ b/modules/setting/lfs.go
@@ -12,12 +12,11 @@ import (
 
 // LFS represents the configuration for Git LFS
 var LFS = struct {
-	StartServer     bool          `ini:"LFS_START_SERVER"`
-	JWTSecretBase64 string        `ini:"LFS_JWT_SECRET"`
-	JWTSecretBytes  []byte        `ini:"-"`
-	HTTPAuthExpiry  time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
-	MaxFileSize     int64         `ini:"LFS_MAX_FILE_SIZE"`
-	LocksPagingNum  int           `ini:"LFS_LOCKS_PAGING_NUM"`
+	StartServer    bool          `ini:"LFS_START_SERVER"`
+	JWTSecretBytes []byte        `ini:"-"`
+	HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
+	MaxFileSize    int64         `ini:"LFS_MAX_FILE_SIZE"`
+	LocksPagingNum int           `ini:"LFS_LOCKS_PAGING_NUM"`
 
 	Storage *Storage
 }{}
@@ -59,10 +58,10 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
 		return nil
 	}
 
-	LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
-	LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(LFS.JWTSecretBase64)
+	jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
+	LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
 	if err != nil {
-		LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
+		LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
 		if err != nil {
 			return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
 		}
@@ -72,8 +71,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
 		if err != nil {
 			return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
 		}
-		rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
-		saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
+		rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
+		saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
 		if err := saveCfg.Save(); err != nil {
 			return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
 		}
diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index e16e167024..4d3bfd3eb6 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -6,6 +6,7 @@ package setting
 import (
 	"math"
 	"path/filepath"
+	"sync/atomic"
 
 	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/log"
@@ -96,7 +97,6 @@ var OAuth2 = struct {
 	RefreshTokenExpirationTime int64
 	InvalidateRefreshTokens    bool
 	JWTSigningAlgorithm        string `ini:"JWT_SIGNING_ALGORITHM"`
-	JWTSecretBase64            string `ini:"JWT_SECRET"`
 	JWTSigningPrivateKeyFile   string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
 	MaxTokenLength             int
 	DefaultApplications        []string
@@ -128,28 +128,50 @@ func loadOAuth2From(rootCfg ConfigProvider) {
 		return
 	}
 
-	OAuth2.JWTSecretBase64 = loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
+	jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
 
 	if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
 		OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
 	}
 
 	if InstallLock {
-		if _, err := generate.DecodeJwtSecretBase64(OAuth2.JWTSecretBase64); err != nil {
-			_, OAuth2.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
+		jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
+		if err != nil {
+			jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
 			if err != nil {
 				log.Fatal("error generating JWT secret: %v", err)
 			}
-
 			saveCfg, err := rootCfg.PrepareSaving()
 			if err != nil {
 				log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
 			}
-			rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
-			saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
+			rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
+			saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
 			if err := saveCfg.Save(); err != nil {
 				log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
 			}
 		}
+		generalSigningSecret.Store(&jwtSecretBytes)
 	}
 }
+
+// generalSigningSecret is used as container for a []byte value
+// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
+var generalSigningSecret atomic.Pointer[[]byte]
+
+func GetGeneralTokenSigningSecret() []byte {
+	old := generalSigningSecret.Load()
+	if old == nil || len(*old) == 0 {
+		jwtSecret, _, err := generate.NewJwtSecretWithBase64()
+		if err != nil {
+			log.Fatal("Unable to generate general JWT secret: %s", err.Error())
+		}
+		if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
+			// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
+			log.Warn("OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
+			return jwtSecret
+		}
+		return *generalSigningSecret.Load()
+	}
+	return *old
+}
diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go
new file mode 100644
index 0000000000..d822198619
--- /dev/null
+++ b/modules/setting/oauth2_test.go
@@ -0,0 +1,34 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/modules/generate"
+	"code.gitea.io/gitea/modules/test"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetGeneralSigningSecret(t *testing.T) {
+	// when there is no general signing secret, it should be generated, and keep the same value
+	assert.Nil(t, generalSigningSecret.Load())
+	s1 := GetGeneralTokenSigningSecret()
+	assert.NotNil(t, s1)
+	s2 := GetGeneralTokenSigningSecret()
+	assert.Equal(t, s1, s2)
+
+	// the config value should always override any pre-generated value
+	cfg, _ := NewConfigProviderFromData(`
+[oauth2]
+JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+`)
+	defer test.MockVariableValue(&InstallLock, true)()
+	loadOAuth2From(cfg)
+	actual := GetGeneralTokenSigningSecret()
+	expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
+	assert.Len(t, actual, 32)
+	assert.EqualValues(t, expected, actual)
+}
diff --git a/services/actions/auth.go b/services/actions/auth.go
index 53e68f0b71..e0f9a9015d 100644
--- a/services/actions/auth.go
+++ b/services/actions/auth.go
@@ -38,7 +38,7 @@ func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
 	}
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 
-	tokenString, err := token.SignedString([]byte(setting.SecretKey))
+	tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
 	if err != nil {
 		return "", err
 	}
@@ -62,7 +62,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
 		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
 			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
 		}
-		return []byte(setting.SecretKey), nil
+		return setting.GetGeneralTokenSigningSecret(), nil
 	})
 	if err != nil {
 		return 0, err
diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go
index f6288ccd5a..1f62f17f52 100644
--- a/services/actions/auth_test.go
+++ b/services/actions/auth_test.go
@@ -20,7 +20,7 @@ func TestCreateAuthorizationToken(t *testing.T) {
 	assert.NotEqual(t, "", token)
 	claims := jwt.MapClaims{}
 	_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
-		return []byte(setting.SecretKey), nil
+		return setting.GetGeneralTokenSigningSecret(), nil
 	})
 	assert.Nil(t, err)
 	scp, ok := claims["scp"]
diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
index 2afe557b0d..070fffe60f 100644
--- a/services/auth/source/oauth2/jwtsigningkey.go
+++ b/services/auth/source/oauth2/jwtsigningkey.go
@@ -18,7 +18,6 @@ import (
 	"path/filepath"
 	"strings"
 
-	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -301,7 +300,7 @@ func InitSigningKey() error {
 	case "HS384":
 		fallthrough
 	case "HS512":
-		key, err = loadSymmetricKey()
+		key = setting.GetGeneralTokenSigningSecret()
 	case "RS256":
 		fallthrough
 	case "RS384":
@@ -334,12 +333,6 @@ func InitSigningKey() error {
 	return nil
 }
 
-// loadSymmetricKey checks if the configured secret is valid.
-// If it is not valid, it will return an error.
-func loadSymmetricKey() (any, error) {
-	return generate.DecodeJwtSecretBase64(setting.OAuth2.JWTSecretBase64)
-}
-
 // loadOrCreateAsymmetricKey checks if the configured private key exists.
 // If it does not exist a new random key gets generated and saved on the configured path.
 func loadOrCreateAsymmetricKey() (any, error) {
diff --git a/services/packages/auth.go b/services/packages/auth.go
index 2f78b26f50..8263c28bed 100644
--- a/services/packages/auth.go
+++ b/services/packages/auth.go
@@ -33,7 +33,7 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
 	}
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 
-	tokenString, err := token.SignedString([]byte(setting.SecretKey))
+	tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
 	if err != nil {
 		return "", err
 	}
@@ -57,7 +57,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
 		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
 			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
 		}
-		return []byte(setting.SecretKey), nil
+		return setting.GetGeneralTokenSigningSecret(), nil
 	})
 	if err != nil {
 		return 0, err

From 20f6a7c484d9dbf249d8e1dafa9a8c0a2e12127e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= <sahin@sahinakkaya.dev>
Date: Mon, 19 Feb 2024 00:02:07 +0300
Subject: [PATCH 088/679] De-duplicate contributor graph translations (#29247)

---
 options/locale/locale_en-US.ini  | 11 +++++++----
 templates/repo/contributors.tmpl |  8 ++++----
 2 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index e46d6f38c0..d033039dd3 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1985,10 +1985,7 @@ contributors.contribution_type.filter_label = Contribution type:
 contributors.contribution_type.commits = Commits
 contributors.contribution_type.additions = Additions
 contributors.contribution_type.deletions = Deletions
-contributors.loading_title = Loading contributions...
-contributors.loading_title_failed = Could not load contributions
-contributors.loading_info = This might take a bit…
-contributors.component_failed_to_load = An unexpected error happened.
+contributors.what = contributions
 
 search = Search
 search.search_repo = Search repository
@@ -2593,6 +2590,12 @@ error.csv.too_large = Can't render this file because it is too large.
 error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d.
 error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
 
+[graphs]
+component_loading = Loading %s...
+component_loading_failed = Could not load %s
+component_loading_info = This might take a bit…
+component_failed_to_load = An unexpected error happened.
+
 [org]
 org_name_holder = Organization Name
 org_full_name_holder = Organization Full Name
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl
index 49a251c1f9..3bd343197b 100644
--- a/templates/repo/contributors.tmpl
+++ b/templates/repo/contributors.tmpl
@@ -4,10 +4,10 @@
 		data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}"
 		data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}"
 		data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}"
-		data-locale-loading-title="{{ctx.Locale.Tr "repo.contributors.loading_title"}}"
-		data-locale-loading-title-failed="{{ctx.Locale.Tr "repo.contributors.loading_title_failed"}}"
-		data-locale-loading-info="{{ctx.Locale.Tr "repo.contributors.loading_info"}}"
-		data-locale-component-failed-to-load="{{ctx.Locale.Tr "repo.contributors.component_failed_to_load"}}"
+		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "repo.contributors.what")}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "repo.contributors.what")}}"
+		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
+		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
 	>
 	</div>
 {{end}}

From f04e71f9bc05d4930e1eff0b69ceb0e890528e30 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 19 Feb 2024 00:24:35 +0000
Subject: [PATCH 089/679] [skip ci] Updated licenses and gitignores

---
 options/license/Brian-Gladman-2-Clause        | 17 ++++++++++
 options/license/CMU-Mach-nodoc                | 11 +++++++
 options/license/GNOME-examples-exception      |  1 +
 options/license/Gmsh-exception                | 16 +++++++++
 options/license/HPND-Fenneberg-Livingston     | 13 ++++++++
 options/license/HPND-INRIA-IMAG               |  9 +++++
 options/license/Mackerras-3-Clause            | 25 ++++++++++++++
 .../license/Mackerras-3-Clause-acknowledgment | 25 ++++++++++++++
 options/license/OpenVision                    | 33 +++++++++++++++++++
 options/license/Sun-PPP                       | 13 ++++++++
 options/license/UMich-Merit                   | 19 +++++++++++
 options/license/bcrypt-Solar-Designer         | 11 +++++++
 options/license/gtkbook                       |  6 ++++
 options/license/softSurfer                    |  6 ++++
 14 files changed, 205 insertions(+)
 create mode 100644 options/license/Brian-Gladman-2-Clause
 create mode 100644 options/license/CMU-Mach-nodoc
 create mode 100644 options/license/GNOME-examples-exception
 create mode 100644 options/license/Gmsh-exception
 create mode 100644 options/license/HPND-Fenneberg-Livingston
 create mode 100644 options/license/HPND-INRIA-IMAG
 create mode 100644 options/license/Mackerras-3-Clause
 create mode 100644 options/license/Mackerras-3-Clause-acknowledgment
 create mode 100644 options/license/OpenVision
 create mode 100644 options/license/Sun-PPP
 create mode 100644 options/license/UMich-Merit
 create mode 100644 options/license/bcrypt-Solar-Designer
 create mode 100644 options/license/gtkbook
 create mode 100644 options/license/softSurfer

diff --git a/options/license/Brian-Gladman-2-Clause b/options/license/Brian-Gladman-2-Clause
new file mode 100644
index 0000000000..7276f63e9e
--- /dev/null
+++ b/options/license/Brian-Gladman-2-Clause
@@ -0,0 +1,17 @@
+Copyright (C) 1998-2013, Brian Gladman, Worcester, UK. All
+   rights reserved.
+
+The redistribution and use of this software (with or without
+changes) is allowed without the payment of fees or royalties
+provided that:
+
+   source code distributions include the above copyright notice,
+   this list of conditions and the following disclaimer;
+
+   binary distributions include the above copyright notice, this
+   list of conditions and the following disclaimer in their
+   documentation.
+
+This software is provided 'as is' with no explicit or implied
+warranties in respect of its operation, including, but not limited
+to, correctness and fitness for purpose.
diff --git a/options/license/CMU-Mach-nodoc b/options/license/CMU-Mach-nodoc
new file mode 100644
index 0000000000..c81d74fee7
--- /dev/null
+++ b/options/license/CMU-Mach-nodoc
@@ -0,0 +1,11 @@
+Copyright (C) 2002 Naval Research Laboratory (NRL/CCS)
+
+Permission to use, copy, modify and distribute this software and
+its documentation is hereby granted, provided that both the
+copyright notice and this permission notice appear in all copies of
+the software, derivative works or modified versions, and any
+portions thereof.
+
+NRL ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS" CONDITION AND
+DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY DAMAGES WHATSOEVER
+RESULTING FROM THE USE OF THIS SOFTWARE.
diff --git a/options/license/GNOME-examples-exception b/options/license/GNOME-examples-exception
new file mode 100644
index 0000000000..0f0cd53b50
--- /dev/null
+++ b/options/license/GNOME-examples-exception
@@ -0,0 +1 @@
+As a special exception, the copyright holders give you permission to copy, modify, and distribute the example code contained in this document under the terms of your choosing, without restriction.
diff --git a/options/license/Gmsh-exception b/options/license/Gmsh-exception
new file mode 100644
index 0000000000..6d28f704e4
--- /dev/null
+++ b/options/license/Gmsh-exception
@@ -0,0 +1,16 @@
+The copyright holders of Gmsh give you permission to combine Gmsh
+  with code included in the standard release of Netgen (from Joachim
+  Sch"oberl), METIS (from George Karypis at the University of
+  Minnesota), OpenCASCADE (from Open CASCADE S.A.S) and ParaView
+  (from Kitware, Inc.) under their respective licenses. You may copy
+  and distribute such a system following the terms of the GNU GPL for
+  Gmsh and the licenses of the other code concerned, provided that
+  you include the source code of that other code when and as the GNU
+  GPL requires distribution of source code.
+
+  Note that people who make modified versions of Gmsh are not
+  obligated to grant this special exception for their modified
+  versions; it is their choice whether to do so. The GNU General
+  Public License gives permission to release a modified version
+  without this exception; this exception also makes it possible to
+  release a modified version which carries forward this exception.
diff --git a/options/license/HPND-Fenneberg-Livingston b/options/license/HPND-Fenneberg-Livingston
new file mode 100644
index 0000000000..aaf524f3aa
--- /dev/null
+++ b/options/license/HPND-Fenneberg-Livingston
@@ -0,0 +1,13 @@
+Copyright (C) 1995,1996,1997,1998 Lars Fenneberg <lf@elemental.net>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose and without fee is hereby granted, provided that this copyright and
+permission notice appear on all copies and supporting documentation, the
+name of Lars Fenneberg not be used in advertising or publicity pertaining to
+distribution of the program without specific prior permission, and notice be
+given in supporting documentation that copying and distribution is by
+permission of Lars Fenneberg.
+
+Lars Fenneberg makes no representations about the suitability of this
+software for any purpose.  It is provided "as is" without express or implied
+warranty.
diff --git a/options/license/HPND-INRIA-IMAG b/options/license/HPND-INRIA-IMAG
new file mode 100644
index 0000000000..87d09d92cb
--- /dev/null
+++ b/options/license/HPND-INRIA-IMAG
@@ -0,0 +1,9 @@
+This software is available with usual "research" terms with
+the aim of retain credits of the software. Permission to use,
+copy, modify and distribute this software for any purpose and
+without fee is hereby granted, provided that the above copyright
+notice and this permission notice appear in all copies, and
+the name of INRIA, IMAG, or any contributor not be used in
+advertising or publicity pertaining to this material without
+the prior explicit permission. The software is provided "as
+is" without any warranties, support or liabilities of any kind.
diff --git a/options/license/Mackerras-3-Clause b/options/license/Mackerras-3-Clause
new file mode 100644
index 0000000000..6467f0c98e
--- /dev/null
+++ b/options/license/Mackerras-3-Clause
@@ -0,0 +1,25 @@
+Copyright (c) 1995 Eric Rosenquist.  All rights reserved.
+  
+   Redistribution and use in source and binary forms, with or without
+   modification, are permitted provided that the following conditions
+   are met:
+  
+   1. Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+  
+   2. Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in
+      the documentation and/or other materials provided with the
+      distribution.
+  
+   3. The name(s) of the authors of this software must not be used to
+      endorse or promote products derived from this software without
+      prior written permission.
+  
+   THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+   THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+   AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+   SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+   AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+   OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/options/license/Mackerras-3-Clause-acknowledgment b/options/license/Mackerras-3-Clause-acknowledgment
new file mode 100644
index 0000000000..5f0187add7
--- /dev/null
+++ b/options/license/Mackerras-3-Clause-acknowledgment
@@ -0,0 +1,25 @@
+Copyright (c) 1993-2002 Paul Mackerras. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 
+ 1. Redistributions of source code must retain the above copyright
+     notice, this list of conditions and the following disclaimer.
+ 
+2. The name(s) of the authors of this software must not be used to
+   endorse or promote products derived from this software without
+   prior written permission.
+
+3. Redistributions of any form whatsoever must retain the following
+   acknowledgment:
+   "This product includes software developed by Paul Mackerras
+   <paulus@ozlabs.org>".
+
+THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/options/license/OpenVision b/options/license/OpenVision
new file mode 100644
index 0000000000..983505389e
--- /dev/null
+++ b/options/license/OpenVision
@@ -0,0 +1,33 @@
+Copyright, OpenVision Technologies, Inc., 1993-1996, All Rights
+Reserved
+
+WARNING:  Retrieving the OpenVision Kerberos Administration system
+source code, as described below, indicates your acceptance of the
+following terms.  If you do not agree to the following terms, do
+not retrieve the OpenVision Kerberos administration system.
+
+You may freely use and distribute the Source Code and Object Code
+compiled from it, with or without modification, but this Source
+Code is provided to you "AS IS" EXCLUSIVE OF ANY WARRANTY,
+INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY OR
+FITNESS FOR A PARTICULAR PURPOSE, OR ANY OTHER WARRANTY, WHETHER
+EXPRESS OR IMPLIED. IN NO EVENT WILL OPENVISION HAVE ANY LIABILITY
+FOR ANY LOST PROFITS, LOSS OF DATA OR COSTS OF PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES, OR FOR ANY SPECIAL, INDIRECT, OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT, INCLUDING,
+WITHOUT LIMITATION, THOSE RESULTING FROM THE USE OF THE SOURCE
+CODE, OR THE FAILURE OF THE SOURCE CODE TO PERFORM, OR FOR ANY
+OTHER REASON.
+
+OpenVision retains all copyrights in the donated Source Code.
+OpenVision also retains copyright to derivative works of the Source
+Code, whether created by OpenVision or by a third party. The
+OpenVision copyright notice must be preserved if derivative works
+are made based on the donated Source Code.
+
+OpenVision Technologies, Inc. has donated this Kerberos
+Administration system to MIT for inclusion in the standard Kerberos
+5 distribution. This donation underscores our commitment to
+continuing Kerberos technology development and our gratitude for
+the valuable work which has been performed by MIT and the Kerberos
+community.
diff --git a/options/license/Sun-PPP b/options/license/Sun-PPP
new file mode 100644
index 0000000000..5f94a13437
--- /dev/null
+++ b/options/license/Sun-PPP
@@ -0,0 +1,13 @@
+Copyright (c) 2001 by Sun Microsystems, Inc.
+All rights reserved.
+
+Non-exclusive rights to redistribute, modify, translate, and use
+this software in source and binary forms, in whole or in part, is
+hereby granted, provided that the above copyright notice is
+duplicated in any source form, and that neither the name of the
+copyright holder nor the author is used to endorse or promote
+products derived from this software.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/options/license/UMich-Merit b/options/license/UMich-Merit
new file mode 100644
index 0000000000..93e304b90e
--- /dev/null
+++ b/options/license/UMich-Merit
@@ -0,0 +1,19 @@
+[C] The Regents of the University of Michigan and Merit Network, Inc. 1992,
+1993, 1994, 1995 All Rights Reserved
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted, provided
+that the above copyright notice and this permission notice appear in all
+copies of the software and derivative works or modified versions thereof,
+and that both the copyright notice and this permission and disclaimer
+notice appear in supporting documentation.
+
+THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE REGENTS OF THE
+UNIVERSITY OF MICHIGAN AND MERIT NETWORK, INC. DO NOT WARRANT THAT THE
+FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET LICENSEE'S REQUIREMENTS OR
+THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE.  The Regents of the
+University of Michigan and Merit Network, Inc. shall not be liable for any
+special, indirect, incidental or consequential damages with respect to any
+claim by Licensee or any third party arising from use of the software.
diff --git a/options/license/bcrypt-Solar-Designer b/options/license/bcrypt-Solar-Designer
new file mode 100644
index 0000000000..8cb05017fc
--- /dev/null
+++ b/options/license/bcrypt-Solar-Designer
@@ -0,0 +1,11 @@
+Written by Solar Designer <solar at openwall.com> in 1998-2014.
+No copyright is claimed, and the software is hereby placed in the public
+domain.  In case this attempt to disclaim copyright and place the software
+in the public domain is deemed null and void, then the software is
+Copyright (c) 1998-2014 Solar Designer and it is hereby released to the
+general public under the following terms:
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted.
+
+There's ABSOLUTELY NO WARRANTY, express or implied.
diff --git a/options/license/gtkbook b/options/license/gtkbook
new file mode 100644
index 0000000000..91215e80d6
--- /dev/null
+++ b/options/license/gtkbook
@@ -0,0 +1,6 @@
+Copyright 2005 Syd Logan, All Rights Reserved
+
+This code is distributed without warranty. You are free to use
+this code for any purpose, however, if this code is republished or
+redistributed in its original form, as hardcopy or electronically,
+then you must include this copyright notice along with the code.
diff --git a/options/license/softSurfer b/options/license/softSurfer
new file mode 100644
index 0000000000..1bbc88c34c
--- /dev/null
+++ b/options/license/softSurfer
@@ -0,0 +1,6 @@
+Copyright 2001, softSurfer (www.softsurfer.com)
+This code may be freely used and modified for any purpose                                                                                                                                
+providing that this copyright notice is included with it.
+SoftSurfer makes no warranty for this code, and cannot be held
+liable for any real or imagined damage resulting from its use.
+Users of this code must verify correctness for their application.

From 5e72526da4e915791f03af056890e16821bde052 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 19 Feb 2024 03:23:06 +0100
Subject: [PATCH 090/679] Downscale pasted PNG images based on metadata
 (#29123)

Some images like MacOS screenshots contain
[pHYs](http://www.libpng.org/pub/png/book/chapter11.html#png.ch11.div.8)
data which we can use to downscale uploaded images so they render in the
same dppx ratio in which they were taken.

Before:

<img width="584" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/50979e3a-5d5a-40dc-a0a4-36eb6e28f14a">

After:

<img width="329" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/0690902a-f2fe-4c6b-97b3-6fdd67c21bad">
---
 web_src/js/features/comp/ImagePaste.js | 20 +++++++++--
 web_src/js/utils/image.js              | 47 ++++++++++++++++++++++++++
 web_src/js/utils/image.test.js         | 29 ++++++++++++++++
 3 files changed, 93 insertions(+), 3 deletions(-)
 create mode 100644 web_src/js/utils/image.js
 create mode 100644 web_src/js/utils/image.test.js

diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js
index 27abcfe56f..444ab89150 100644
--- a/web_src/js/features/comp/ImagePaste.js
+++ b/web_src/js/features/comp/ImagePaste.js
@@ -1,5 +1,7 @@
 import $ from 'jquery';
+import {htmlEscape} from 'escape-goat';
 import {POST} from '../../modules/fetch.js';
+import {imageInfo} from '../../utils/image.js';
 
 async function uploadFile(file, uploadUrl) {
   const formData = new FormData();
@@ -109,10 +111,22 @@ const uploadClipboardImage = async (editor, dropzone, e) => {
 
     const placeholder = `![${name}](uploading ...)`;
     editor.insertPlaceholder(placeholder);
-    const data = await uploadFile(img, uploadUrl);
-    editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`);
 
-    const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid);
+    const {uuid} = await uploadFile(img, uploadUrl);
+    const {width, dppx} = await imageInfo(img);
+
+    const url = `/attachments/${uuid}`;
+    let text;
+    if (width > 0 && dppx > 1) {
+      // Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
+      // method to change image size in Markdown that is supported by all implementations.
+      text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
+    } else {
+      text = `![${name}](${url})`;
+    }
+    editor.replacePlaceholder(placeholder, text);
+
+    const $input = $(`<input name="files" type="hidden">`).attr('id', uuid).val(uuid);
     $files.append($input);
   }
 };
diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js
new file mode 100644
index 0000000000..ed5d98e35a
--- /dev/null
+++ b/web_src/js/utils/image.js
@@ -0,0 +1,47 @@
+export async function pngChunks(blob) {
+  const uint8arr = new Uint8Array(await blob.arrayBuffer());
+  const chunks = [];
+  if (uint8arr.length < 12) return chunks;
+  const view = new DataView(uint8arr.buffer);
+  if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
+
+  const decoder = new TextDecoder();
+  let index = 8;
+  while (index < uint8arr.length) {
+    const len = view.getUint32(index);
+    chunks.push({
+      name: decoder.decode(uint8arr.slice(index + 4, index + 8)),
+      data: uint8arr.slice(index + 8, index + 8 + len),
+    });
+    index += len + 12;
+  }
+
+  return chunks;
+}
+
+// decode a image and try to obtain width and dppx. If will never throw but instead
+// return default values.
+export async function imageInfo(blob) {
+  let width = 0; // 0 means no width could be determined
+  let dppx = 1; // 1 dot per pixel for non-HiDPI screens
+
+  if (blob.type === 'image/png') { // only png is supported currently
+    try {
+      for (const {name, data} of await pngChunks(blob)) {
+        const view = new DataView(data.buffer);
+        if (name === 'IHDR' && data?.length) {
+          // extract width from mandatory IHDR chunk
+          width = view.getUint32(0);
+        } else if (name === 'pHYs' && data?.length) {
+          // extract dppx from optional pHYs chunk, assuming pixels are square
+          const unit = view.getUint8(8);
+          if (unit === 1) {
+            dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx
+          }
+        }
+      }
+    } catch {}
+  }
+
+  return {width, dppx};
+}
diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js
new file mode 100644
index 0000000000..ba4758250c
--- /dev/null
+++ b/web_src/js/utils/image.test.js
@@ -0,0 +1,29 @@
+import {pngChunks, imageInfo} from './image.js';
+
+const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==';
+const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==';
+const pngEmpty = 'data:image/png;base64,';
+
+async function dataUriToBlob(datauri) {
+  return await (await globalThis.fetch(datauri)).blob();
+}
+
+test('pngChunks', async () => {
+  expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([
+    {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])},
+    {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])},
+    {name: 'IEND', data: new Uint8Array([])},
+  ]);
+  expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([
+    {name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])},
+    {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])},
+    {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])},
+  ]);
+  expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]);
+});
+
+test('imageInfo', async () => {
+  expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1});
+  expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2});
+  expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1});
+});

From 0ea8de2d0729e1e1d0ea9de1e59fbcb673e87fd2 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Mon, 19 Feb 2024 17:31:36 +0800
Subject: [PATCH 091/679] Do not use lower tag names to find releases/tags
 (#29261)

Fix #26090, see
https://github.com/go-gitea/gitea/issues/26090#issuecomment-1952013206

Since `TagName` stores the original tag name and `LowerTagName` stores
the lower tag name, it doesn't make sense to use lowercase tags as
`TagNames` in `FindReleasesOptions`.


https://github.com/go-gitea/gitea/blob/5e72526da4e915791f03af056890e16821bde052/services/repository/push.go#L396-L397

While the only other usage looks correct:


https://github.com/go-gitea/gitea/blob/5e72526da4e915791f03af056890e16821bde052/routers/web/repo/repo.go#L416
---
 services/repository/push.go | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/services/repository/push.go b/services/repository/push.go
index bedcf6f252..c76025b6a7 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -321,14 +321,9 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
 		return nil
 	}
 
-	lowerTags := make([]string, 0, len(tags))
-	for _, tag := range tags {
-		lowerTags = append(lowerTags, strings.ToLower(tag))
-	}
-
 	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		RepoID:   repo.ID,
-		TagNames: lowerTags,
+		TagNames: tags,
 	})
 	if err != nil {
 		return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
@@ -338,6 +333,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
 		relMap[rel.LowerTagName] = rel
 	}
 
+	lowerTags := make([]string, 0, len(tags))
+	for _, tag := range tags {
+		lowerTags = append(lowerTags, strings.ToLower(tag))
+	}
+
 	newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap))
 
 	emailToUser := make(map[string]*user_model.User)

From a11ccc9fcd61fb25ffb1c37b87a0df4ee9efd84e Mon Sep 17 00:00:00 2001
From: Markus Amshove <scm@amshove.dev>
Date: Mon, 19 Feb 2024 10:57:08 +0100
Subject: [PATCH 092/679] Disallow merge when required checked are missing
 (#29143)

fixes #21892

This PR disallows merging a PR when not all commit status contexts
configured in the branch protection are met.

Previously, the PR was happy to merge when one commit status was
successful and the other contexts weren't reported.

Any feedback is welcome, first time Go :-)
I'm also not sure if the changes in the template break something else

Given the following branch protection:


![branch_protection](https://github.com/go-gitea/gitea/assets/2401875/f871b4e4-138b-435a-b496-f9ad432e3dec)

This was shown before the change:


![before](https://github.com/go-gitea/gitea/assets/2401875/60424ff0-ee09-4fa0-856e-64e6e3fb0612)

With the change, it is now shown as this:


![after](https://github.com/go-gitea/gitea/assets/2401875/4e464142-efb1-4889-8166-eb3be26c8f3d)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/web/repo/pull.go                    | 30 +++++++++++++++++++++
 services/pull/commit_status.go              |  4 +++
 templates/repo/issue/view_content/pull.tmpl |  1 +
 templates/repo/pulls/status.tmpl            | 10 ++++++-
 4 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 365d9bf258..7052467e64 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -652,6 +652,24 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 	}
 
 	if pb != nil && pb.EnableStatusCheck {
+
+		var missingRequiredChecks []string
+		for _, requiredContext := range pb.StatusCheckContexts {
+			contextFound := false
+			matchesRequiredContext := createRequiredContextMatcher(requiredContext)
+			for _, presentStatus := range commitStatuses {
+				if matchesRequiredContext(presentStatus.Context) {
+					contextFound = true
+					break
+				}
+			}
+
+			if !contextFound {
+				missingRequiredChecks = append(missingRequiredChecks, requiredContext)
+			}
+		}
+		ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
+
 		ctx.Data["is_context_required"] = func(context string) bool {
 			for _, c := range pb.StatusCheckContexts {
 				if c == context {
@@ -720,6 +738,18 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 	return compareInfo
 }
 
+func createRequiredContextMatcher(requiredContext string) func(string) bool {
+	if gp, err := glob.Compile(requiredContext); err == nil {
+		return func(contextToCheck string) bool {
+			return gp.Match(contextToCheck)
+		}
+	}
+
+	return func(contextToCheck string) bool {
+		return requiredContext == contextToCheck
+	}
+}
+
 type pullCommitList struct {
 	Commits             []pull_service.CommitInfo `json:"commits"`
 	LastReviewCommitSha string                    `json:"last_review_commit_sha"`
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index b73816c7eb..3282f4f379 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -51,6 +51,10 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
 		}
 	}
 
+	if matchedCount != len(requiredContexts) {
+		return structs.CommitStatusPending
+	}
+
 	if matchedCount == 0 {
 		status := git_model.CalcCommitStatus(commitStatuses)
 		if status != nil {
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index a28b849f98..e86deb8915 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -24,6 +24,7 @@
 		{{template "repo/pulls/status" (dict
 			"CommitStatus" .LatestCommitStatus
 			"CommitStatuses" .LatestCommitStatuses
+			"MissingRequiredChecks" .MissingRequiredChecks
 			"ShowHideChecks" true
 			"is_context_required" .is_context_required
 		)}}
diff --git a/templates/repo/pulls/status.tmpl b/templates/repo/pulls/status.tmpl
index ae508b8fa4..e8636ba1b8 100644
--- a/templates/repo/pulls/status.tmpl
+++ b/templates/repo/pulls/status.tmpl
@@ -2,6 +2,7 @@
 Template Attributes:
 * CommitStatus: summary of all commit status state
 * CommitStatuses: all commit status elements
+* MissingRequiredChecks: commit check contexts that are required by branch protection but not present
 * ShowHideChecks: whether use a button to show/hide the checks
 * is_context_required: Used in pull request commit status check table
 */}}
@@ -9,7 +10,7 @@ Template Attributes:
 {{if .CommitStatus}}
 <div class="commit-status-panel">
 	<div class="ui top attached header commit-status-header">
-		{{if eq .CommitStatus.State "pending"}}
+		{{if or (eq .CommitStatus.State "pending") (.MissingRequiredChecks)}}
 			{{ctx.Locale.Tr "repo.pulls.status_checking"}}
 		{{else if eq .CommitStatus.State "success"}}
 			{{ctx.Locale.Tr "repo.pulls.status_checks_success"}}
@@ -46,6 +47,13 @@ Template Attributes:
 				</div>
 			</div>
 		{{end}}
+		{{range .MissingRequiredChecks}}
+			<div class="commit-status-item">
+				{{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}}
+				<div class="status-context gt-ellipsis">{{.}}</div>
+				<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
+			</div>
+		{{end}}
 	</div>
 </div>
 {{end}}

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

Fixes #29082

![grafik](https://github.com/go-gitea/gitea/assets/1666336/bb2ccde1-ee99-459d-9e74-0fb8ea79e8b3)
---
 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 fdb247d413..b920ffb6dd 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>

From 740c6a226c4df26432641018fbfd9186977d573f Mon Sep 17 00:00:00 2001
From: Johan Van de Wauw <johan@fluves.com>
Date: Mon, 19 Feb 2024 11:51:58 +0100
Subject: [PATCH 094/679] Fix c/p error in inline documentation (#29148)

Fix small copy/paste error in inline documentation

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/auth/source/db/source.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go
index 50eae27439..bb2270cbd6 100644
--- a/services/auth/source/db/source.go
+++ b/services/auth/source/db/source.go
@@ -18,7 +18,7 @@ func (source *Source) FromDB(bs []byte) error {
 	return nil
 }
 
-// ToDB exports an SMTPConfig to a serialized format.
+// ToDB exports the config to a byte slice to be saved into database (this method is just dummy and does nothing for DB source)
 func (source *Source) ToDB() ([]byte, error) {
 	return nil, nil
 }

From 567a68a0bf78c8d70f08c8ab948fdbb455225aa9 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 19 Feb 2024 19:25:58 +0800
Subject: [PATCH 095/679] Remove DataRaceCheck (#29258)

Since #26254, it started using `{{ctx.Locale.Tr ...}}`

Now the `ctx` seems stable enough, so the check could be removed.
---
 modules/context/context_template.go | 14 --------------
 templates/base/footer.tmpl          |  1 -
 templates/base/head.tmpl            |  1 -
 3 files changed, 16 deletions(-)

diff --git a/modules/context/context_template.go b/modules/context/context_template.go
index ba90fc170a..7878d409ca 100644
--- a/modules/context/context_template.go
+++ b/modules/context/context_template.go
@@ -5,10 +5,7 @@ package context
 
 import (
 	"context"
-	"errors"
 	"time"
-
-	"code.gitea.io/gitea/modules/log"
 )
 
 var _ context.Context = TemplateContext(nil)
@@ -36,14 +33,3 @@ func (c TemplateContext) Err() error {
 func (c TemplateContext) Value(key any) any {
 	return c.parentContext().Value(key)
 }
-
-// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context
-// as the current template's rendering context (request context), to help to find data race issues as early as possible.
-// When the code is proven to be correct and stable, this function should be removed.
-func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) {
-	if c.parentContext() != dataCtx {
-		log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2))
-		return "", errors.New("parent context mismatch")
-	}
-	return "", nil
-}
diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
index d65a3626a4..fed426a469 100644
--- a/templates/base/footer.tmpl
+++ b/templates/base/footer.tmpl
@@ -16,6 +16,5 @@
 	<script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + '. Please make sure the asset files can be accessed.')"></script>
 
 	{{template "custom/footer" .}}
-	{{ctx.DataRaceCheck $.Context}}
 </body>
 </html>
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index e910bb0cd9..d7e28474e7 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -30,7 +30,6 @@
 	{{template "custom/header" .}}
 </head>
 <body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
-	{{ctx.DataRaceCheck $.Context}}
 	{{template "custom/body_outer_pre" .}}
 
 	<div class="full height">

From 39a77d92d9677b0a0049cb8696960d6d2ac052d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= <sahin@sahinakkaya.dev>
Date: Mon, 19 Feb 2024 15:47:38 +0300
Subject: [PATCH 096/679] Deduplicate translations for contributors graph
 (#29256)

I have implemented three graph pages
([contributors](https://github.com/go-gitea/gitea/pull/27882), [code
frequency](https://github.com/go-gitea/gitea/pull/29191) and [recent
commits](https://github.com/go-gitea/gitea/pull/29210)) and they have
all same page title as the tab name so I decided to use same
translations for them. This PR is for contributors graph. Other PR's
have their own respective commits.
---
 options/locale/locale_en-US.ini  | 3 +--
 routers/web/repo/contributors.go | 2 +-
 templates/repo/contributors.tmpl | 4 ++--
 3 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index d033039dd3..574e99e654 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1980,12 +1980,10 @@ activity.git_stats_and_deletions = and
 activity.git_stats_deletion_1 = %d deletion
 activity.git_stats_deletion_n = %d deletions
 
-contributors = Contributors
 contributors.contribution_type.filter_label = Contribution type:
 contributors.contribution_type.commits = Commits
 contributors.contribution_type.additions = Additions
 contributors.contribution_type.deletions = Deletions
-contributors.what = contributions
 
 search = Search
 search.search_repo = Search repository
@@ -2595,6 +2593,7 @@ component_loading = Loading %s...
 component_loading_failed = Could not load %s
 component_loading_info = This might take a bit…
 component_failed_to_load = An unexpected error happened.
+contributors.what = contributions
 
 [org]
 org_name_holder = Organization Name
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
index f7dedc0b34..bcfef7580a 100644
--- a/routers/web/repo/contributors.go
+++ b/routers/web/repo/contributors.go
@@ -18,7 +18,7 @@ const (
 
 // Contributors render the page to show repository contributors graph
 func Contributors(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("repo.contributors")
+	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors")
 
 	ctx.Data["PageIsActivity"] = true
 	ctx.Data["PageIsContributors"] = true
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl
index 3bd343197b..4a258e5b70 100644
--- a/templates/repo/contributors.tmpl
+++ b/templates/repo/contributors.tmpl
@@ -4,8 +4,8 @@
 		data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}"
 		data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}"
 		data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}"
-		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "repo.contributors.what")}}"
-		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "repo.contributors.what")}}"
+		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.contributors.what")}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.contributors.what")}}"
 		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
 		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
 	>

From 217d71c48a10265e08b95cc961656b921f61f9ff Mon Sep 17 00:00:00 2001
From: 6543 <m.huber@kithara.com>
Date: Mon, 19 Feb 2024 14:42:18 +0100
Subject: [PATCH 097/679] Workaround to clean up old reviews on creating a new
 one (#28554)

close  #28542

blocks  #28544

---
*Sponsored by Kithara Software GmbH*
---
 models/issues/review.go                   |  40 ++++++-
 models/unittest/unit_tests.go             |   8 +-
 tests/integration/api_pull_review_test.go | 126 ++++++++++++++++++++++
 3 files changed, 165 insertions(+), 9 deletions(-)

diff --git a/models/issues/review.go b/models/issues/review.go
index 3aa9d3e2a8..fc110630e0 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -292,8 +292,14 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
 
 // CreateReview creates a new review based on opts
 func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
+	ctx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	defer committer.Close()
+	sess := db.GetEngine(ctx)
+
 	review := &Review{
-		Type:         opts.Type,
 		Issue:        opts.Issue,
 		IssueID:      opts.Issue.ID,
 		Reviewer:     opts.Reviewer,
@@ -303,15 +309,39 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
 		CommitID:     opts.CommitID,
 		Stale:        opts.Stale,
 	}
+
 	if opts.Reviewer != nil {
+		review.Type = opts.Type
 		review.ReviewerID = opts.Reviewer.ID
-	} else {
-		if review.Type != ReviewTypeRequest {
-			review.Type = ReviewTypeRequest
+
+		reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID}
+		// make sure user review requests are cleared
+		if opts.Type != ReviewTypePending {
+			if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil {
+				return nil, err
+			}
 		}
+		// make sure if the created review gets dismissed no old review surface
+		// other types can be ignored, as they don't affect branch protection
+		if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject {
+			if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))).
+				Cols("dismissed").Update(&Review{Dismissed: true}); err != nil {
+				return nil, err
+			}
+		}
+
+	} else if opts.ReviewerTeam != nil {
+		review.Type = ReviewTypeRequest
 		review.ReviewerTeamID = opts.ReviewerTeam.ID
+
+	} else {
+		return nil, fmt.Errorf("provide either reviewer or reviewer team")
 	}
-	return review, db.Insert(ctx, review)
+
+	if _, err := sess.Insert(review); err != nil {
+		return nil, err
+	}
+	return review, committer.Commit()
 }
 
 // GetCurrentReview returns the current pending review of reviewer for given issue
diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go
index d47bceea1e..75898436fc 100644
--- a/models/unittest/unit_tests.go
+++ b/models/unittest/unit_tests.go
@@ -131,8 +131,8 @@ func AssertSuccessfulInsert(t assert.TestingT, beans ...any) {
 }
 
 // AssertCount assert the count of a bean
-func AssertCount(t assert.TestingT, bean, expected any) {
-	assert.EqualValues(t, expected, GetCount(t, bean))
+func AssertCount(t assert.TestingT, bean, expected any) bool {
+	return assert.EqualValues(t, expected, GetCount(t, bean))
 }
 
 // AssertInt64InRange assert value is in range [low, high]
@@ -150,7 +150,7 @@ func GetCountByCond(t assert.TestingT, tableName string, cond builder.Cond) int6
 }
 
 // AssertCountByCond test the count of database entries matching bean
-func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) {
-	assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
+func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) bool {
+	return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
 		"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
 }
diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go
index daa136b21e..ab6d33cd5b 100644
--- a/tests/integration/api_pull_review_test.go
+++ b/tests/integration/api_pull_review_test.go
@@ -13,11 +13,14 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
+	issue_service "code.gitea.io/gitea/services/issue"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
+	"xorm.io/builder"
 )
 
 func TestAPIPullReview(t *testing.T) {
@@ -314,3 +317,126 @@ func TestAPIPullReviewRequest(t *testing.T) {
 		AddTokenAuth(token)
 	MakeRequest(t, req, http.StatusNoContent)
 }
+
+func TestAPIPullReviewStayDismissed(t *testing.T) {
+	// This test against issue https://github.com/go-gitea/gitea/issues/28542
+	// where old reviews surface after a review request got dismissed.
+	defer tests.PrepareTestEnv(t)()
+	pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+	assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	session2 := loginUser(t, user2.LoginName)
+	token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
+	user8 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
+	session8 := loginUser(t, user8.LoginName)
+	token8 := getTokenForLoggedInUser(t, session8, auth_model.AccessTokenScopeWriteRepository)
+
+	// user2 request user8
+	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{user8.LoginName},
+	}).AddTokenAuth(token2)
+	MakeRequest(t, req, http.StatusCreated)
+
+	reviewsCountCheck(t,
+		"check we have only one review request",
+		pullIssue.ID, user8.ID, 0, 1, 1, false)
+
+	// user2 request user8 again, it is expected to be ignored
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{user8.LoginName},
+	}).AddTokenAuth(token2)
+	MakeRequest(t, req, http.StatusCreated)
+
+	reviewsCountCheck(t,
+		"check we have only one review request, even after re-request it again",
+		pullIssue.ID, user8.ID, 0, 1, 1, false)
+
+	// user8 reviews it as accept
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
+		Event: "APPROVED",
+		Body:  "lgtm",
+	}).AddTokenAuth(token8)
+	MakeRequest(t, req, http.StatusOK)
+
+	reviewsCountCheck(t,
+		"check we have one valid approval",
+		pullIssue.ID, user8.ID, 0, 0, 1, true)
+
+	// emulate of auto-dismiss lgtm on a protected branch that where a pull just got an update
+	_, err := db.GetEngine(db.DefaultContext).Where("issue_id = ? AND reviewer_id = ?", pullIssue.ID, user8.ID).
+		Cols("dismissed").Update(&issues_model.Review{Dismissed: true})
+	assert.NoError(t, err)
+
+	// user2 request user8 again
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{user8.LoginName},
+	}).AddTokenAuth(token2)
+	MakeRequest(t, req, http.StatusCreated)
+
+	reviewsCountCheck(t,
+		"check we have no valid approval and one review request",
+		pullIssue.ID, user8.ID, 1, 1, 2, false)
+
+	// user8 dismiss review
+	_, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user8, user8, false)
+	assert.NoError(t, err)
+
+	reviewsCountCheck(t,
+		"check new review request is now dismissed",
+		pullIssue.ID, user8.ID, 1, 0, 1, false)
+
+	// add a new valid approval
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
+		Event: "APPROVED",
+		Body:  "lgtm",
+	}).AddTokenAuth(token8)
+	MakeRequest(t, req, http.StatusOK)
+
+	reviewsCountCheck(t,
+		"check that old reviews requests are deleted",
+		pullIssue.ID, user8.ID, 1, 0, 2, true)
+
+	// now add a change request witch should dismiss the approval
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
+		Event: "REQUEST_CHANGES",
+		Body:  "please change XYZ",
+	}).AddTokenAuth(token8)
+	MakeRequest(t, req, http.StatusOK)
+
+	reviewsCountCheck(t,
+		"check that old reviews are dismissed",
+		pullIssue.ID, user8.ID, 2, 0, 3, false)
+}
+
+func reviewsCountCheck(t *testing.T, name string, issueID, reviewerID int64, expectedDismissed, expectedRequested, expectedTotal int, expectApproval bool) {
+	t.Run(name, func(t *testing.T) {
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+			"dismissed":   true,
+		}, expectedDismissed)
+
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+		}, expectedTotal)
+
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+			"type":        issues_model.ReviewTypeRequest,
+		}, expectedRequested)
+
+		approvalCount := 0
+		if expectApproval {
+			approvalCount = 1
+		}
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+			"type":        issues_model.ReviewTypeApprove,
+			"dismissed":   false,
+		}, approvalCount)
+	})
+}

From 35d5e4aea4bb02a0b4c7b38ecb2acf612151e891 Mon Sep 17 00:00:00 2001
From: vincent <pulltheflower@163.com>
Date: Mon, 19 Feb 2024 22:50:03 +0800
Subject: [PATCH 098/679] Fix content size does not match error when uploading
 lfs file (#29259)

![image](https://github.com/go-gitea/gitea/assets/38434877/cd726b4d-4771-4547-8aee-ae4e4b56b1d1)
When we update an lfs file by API
`api/v1/repos/{owner}/{repo}/contents/{filepath}`, there will show an
error

```json
{
  "message": "Put \"http://localhost:9000/gitea/lfs/38/92/05904d6c7bb83fc676513911226f2be25bf1465616bb9b29587100ab1414\": readfrom tcp [::1]:57300->[::1]:9000: content size does not match",
  "url": "http://localhost:3000/api/swagger"
}
```

The reason of this error is
https://github.com/go-gitea/gitea/blob/main/services/repository/files/update.go,
in this file, the `file.ContentReader` been used twice. So when use
`file.ContentReader` in the second time, the `i` of this Reader has been
updated to the length of the content. it will return 0 and an `io.EOF`
error when we try to read cotent from this Reader.
---
 routers/api/v1/repo/file.go         | 2 +-
 services/repository/files/update.go | 6 +++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 370e4753f3..317213c946 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -408,7 +408,7 @@ func canReadFiles(r *context.Repository) bool {
 	return r.Permission.CanRead(unit.TypeCode)
 }
 
-func base64Reader(s string) (io.Reader, error) {
+func base64Reader(s string) (io.ReadSeeker, error) {
 	b, err := base64.StdEncoding.DecodeString(s)
 	if err != nil {
 		return nil, err
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index f223daf3a9..4f7178184b 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -40,7 +40,7 @@ type ChangeRepoFile struct {
 	Operation     string
 	TreePath      string
 	FromTreePath  string
-	ContentReader io.Reader
+	ContentReader io.ReadSeeker
 	SHA           string
 	Options       *RepoFileOptions
 }
@@ -448,6 +448,10 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
 			return err
 		}
 		if !exist {
+			_, err := file.ContentReader.Seek(0, io.SeekStart)
+			if err != nil {
+				return err
+			}
 			if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil {
 				if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
 					return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)

From 100031f5f143a15c79ebbe1b77c86091e3b6d489 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 20 Feb 2024 00:34:35 +0200
Subject: [PATCH 099/679] Remove jQuery from the repo migration form (#29229)

- Switched to plain JavaScript
- Tested the repo migration form functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/3496ec05-48a7-449e-8cdd-f8372ba0d589)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-migration.js | 72 +++++++++++++++------------
 1 file changed, 39 insertions(+), 33 deletions(-)

diff --git a/web_src/js/features/repo-migration.js b/web_src/js/features/repo-migration.js
index 3bd0e6d72c..59e282e4e7 100644
--- a/web_src/js/features/repo-migration.js
+++ b/web_src/js/features/repo-migration.js
@@ -1,38 +1,42 @@
-import $ from 'jquery';
 import {hideElem, showElem, toggleElem} from '../utils/dom.js';
 
-const $service = $('#service_type');
-const $user = $('#auth_username');
-const $pass = $('#auth_password');
-const $token = $('#auth_token');
-const $mirror = $('#mirror');
-const $lfs = $('#lfs');
-const $lfsSettings = $('#lfs_settings');
-const $lfsEndpoint = $('#lfs_endpoint');
-const $items = $('#migrate_items').find('input[type=checkbox]');
+const service = document.getElementById('service_type');
+const user = document.getElementById('auth_username');
+const pass = document.getElementById('auth_password');
+const token = document.getElementById('auth_token');
+const mirror = document.getElementById('mirror');
+const lfs = document.getElementById('lfs');
+const lfsSettings = document.getElementById('lfs_settings');
+const lfsEndpoint = document.getElementById('lfs_endpoint');
+const items = document.querySelectorAll('#migrate_items input[type=checkbox]');
 
 export function initRepoMigration() {
   checkAuth();
   setLFSSettingsVisibility();
 
-  $user.on('input', () => {checkItems(false)});
-  $pass.on('input', () => {checkItems(false)});
-  $token.on('input', () => {checkItems(true)});
-  $mirror.on('change', () => {checkItems(true)});
-  $('#lfs_settings_show').on('click', () => { showElem($lfsEndpoint); return false });
-  $lfs.on('change', setLFSSettingsVisibility);
+  user?.addEventListener('input', () => {checkItems(false)});
+  pass?.addEventListener('input', () => {checkItems(false)});
+  token?.addEventListener('input', () => {checkItems(true)});
+  mirror?.addEventListener('change', () => {checkItems(true)});
+  document.getElementById('lfs_settings_show')?.addEventListener('click', (e) => {
+    e.preventDefault();
+    e.stopPropagation();
+    showElem(lfsEndpoint);
+  });
+  lfs?.addEventListener('change', setLFSSettingsVisibility);
 
-  const $cloneAddr = $('#clone_addr');
-  $cloneAddr.on('change', () => {
-    const $repoName = $('#repo_name');
-    if ($cloneAddr.val().length > 0 && $repoName.val().length === 0) { // Only modify if repo_name input is blank
-      $repoName.val($cloneAddr.val().match(/^(.*\/)?((.+?)(\.git)?)$/)[3]);
+  const cloneAddr = document.getElementById('clone_addr');
+  cloneAddr?.addEventListener('change', () => {
+    const repoName = document.getElementById('repo_name');
+    if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank
+      repoName.value = cloneAddr.value.match(/^(.*\/)?((.+?)(\.git)?)$/)[3];
     }
   });
 }
 
 function checkAuth() {
-  const serviceType = $service.val();
+  if (!service) return;
+  const serviceType = Number(service.value);
 
   checkItems(serviceType !== 1);
 }
@@ -40,24 +44,26 @@ function checkAuth() {
 function checkItems(tokenAuth) {
   let enableItems;
   if (tokenAuth) {
-    enableItems = $token.val() !== '';
+    enableItems = token?.value !== '';
   } else {
-    enableItems = $user.val() !== '' || $pass.val() !== '';
+    enableItems = user?.value !== '' || pass?.value !== '';
   }
-  if (enableItems && $service.val() > 1) {
-    if ($mirror.is(':checked')) {
-      $items.not('[name="wiki"]').attr('disabled', true);
-      $items.filter('[name="wiki"]').attr('disabled', false);
+  if (enableItems && Number(service?.value) > 1) {
+    if (mirror?.checked) {
+      for (const item of items) {
+        item.disabled = item.name !== 'wiki';
+      }
       return;
     }
-    $items.attr('disabled', false);
+    for (const item of items) item.disabled = false;
   } else {
-    $items.attr('disabled', true);
+    for (const item of items) item.disabled = true;
   }
 }
 
 function setLFSSettingsVisibility() {
-  const visible = $lfs.is(':checked');
-  toggleElem($lfsSettings, visible);
-  hideElem($lfsEndpoint);
+  if (!lfs) return;
+  const visible = lfs.checked;
+  toggleElem(lfsSettings, visible);
+  hideElem(lfsEndpoint);
 }

From d9268369473965fce1464325d9f4b15ed9d38046 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 20 Feb 2024 00:23:17 +0000
Subject: [PATCH 100/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_lv-LV.ini | 46 ++++++++++++++++++++++++++++++---
 1 file changed, 43 insertions(+), 3 deletions(-)

diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index e275b02ba0..d85cdb24a4 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -109,6 +109,7 @@ loading=Notiek ielāde…
 
 error=Kļūda
 error404=Lapa, ko vēlaties atvērt, <strong>neeksistē</strong> vai arī <strong>Jums nav tiesības</strong> to aplūkot.
+go_back=Atgriezties
 
 never=Nekad
 unknown=Nezināms
@@ -589,6 +590,8 @@ user_bio=Biogrāfija
 disabled_public_activity=Šis lietotājs ir atslēdzies iespēju aplūkot tā aktivitāti.
 email_visibility.limited=E-pasta adrese ir redzama visiem autentificētajiem lietotājiem
 email_visibility.private=E-pasta adrese ir redzama tikai administratoriem
+show_on_map=Rādīt šo vietu kartē
+settings=Lietotāja iestatījumi
 
 form.name_reserved=Lietotājvārdu "%s" nedrīkst izmantot.
 form.name_pattern_not_allowed=Lietotājvārds "%s" nav atļauts.
@@ -610,9 +613,12 @@ delete=Dzēst kontu
 twofa=Divfaktoru autentifikācija
 account_link=Saistītie konti
 organization=Organizācijas
+uid=UID
 webauthn=Drošības atslēgas
 
 public_profile=Publiskais profils
+biography_placeholder=Pastāsti mums mazliet par sevi! (Var izmantot Markdown)
+location_placeholder=Kopīgot savu aptuveno atrašanās vietu ar citiem
 password_username_disabled=Ne-lokāliem lietotājiem nav atļauts mainīt savu lietotāja vārdu. Sazinieties ar sistēmas administratoru, lai uzzinātu sīkāk.
 full_name=Pilns vārds
 website=Mājas lapa
@@ -696,6 +702,7 @@ add_email_success=Jūsu jaunā e-pasta adrese tika veiksmīgi pievienota.
 email_preference_set_success=E-pasta izvēle tika veiksmīgi saglabāta.
 add_openid_success=Jūsu jaunā OpenID adrese tika veiksmīgi pievienota.
 keep_email_private=Paslēpt e-pasta adresi
+keep_email_private_popup=Šis profilā paslēps e-pasta adresi, kā arī tad, kad tiks veikts izmaiņu pieprasījums vai tīmekļa saskarnē labota datne. Aizgādātie iesūtījumi netiks pārveidoti. Revīzijās jāizmanto %s, lai sasaistītu tos ar kontu.
 openid_desc=Jūsu OpenID adreses ļauj autorizēties, izmantojot, Jūsu izvēlēto pakalpojumu sniedzēju.
 
 manage_ssh_keys=Pārvaldīt SSH atslēgas
@@ -822,6 +829,7 @@ authorized_oauth2_applications=Autorizētās OAuth2 lietotnes
 revoke_key=Atsaukt
 revoke_oauth2_grant=Atsaukt piekļuvi
 revoke_oauth2_grant_description=Atsaucot piekļuvi šai trešas puses lietotnei tiks liegta piekļuve Jūsu datiem. Vai turpināt?
+revoke_oauth2_grant_success=Piekļuve veiksmīgi atsaukta.
 
 twofa_desc=Divfaktoru autentifikācija uzlabo konta drošību.
 twofa_is_enrolled=Kontam ir <strong>ieslēgta</strong> divfaktoru autentifikācija.
@@ -874,6 +882,7 @@ visibility=Lietotāja redzamība
 visibility.public=Publisks
 visibility.public_tooltip=Redzams ikvienam
 visibility.limited=Ierobežota
+visibility.limited_tooltip=Redzams tikai autentificētiem lietotājiem
 visibility.private=Privāts
 
 [repo]
@@ -888,6 +897,7 @@ template_helper=Padarīt repozitoriju par sagatavi
 template_description=Sagatavju repozitoriji tiek izmantoti, lai balstoties uz tiem veidotu jaunus repozitorijus saglabājot direktoriju un failu struktūru.
 visibility=Redzamība
 visibility_description=Tikai organizācijas īpašnieks vai tās biedri, kam ir tiesības, varēs piekļūt šim repozitorijam.
+visibility_helper=Padarīt repozitoriju privātu
 visibility_helper_forced=Jūsu sistēmas administrators ir noteicis, ka visiem no jauna izveidotajiem repozitorijiem ir jābūt privātiem.
 visibility_fork_helper=(Šīs vērtības maiņa ietekmēs arī visus atdalītos repozitorijus.)
 clone_helper=Nepieciešama palīdzība klonēšanā? Apmeklē <a target="_blank" rel="noopener noreferrer" href="%s">palīdzības</a> sadaļu.
@@ -896,6 +906,7 @@ fork_from=Atdalīt no
 already_forked=Repozitorijs %s jau ir atdalīts
 fork_to_different_account=Atdalīt uz citu kontu
 fork_visibility_helper=Atdalītam repozitorijam nav iespējams mainīt tā redzamību.
+all_branches=Visi atzari
 use_template=Izmantot šo sagatavi
 clone_in_vsc=Atvērt VS Code
 download_zip=Lejupielādēt ZIP
@@ -923,7 +934,8 @@ trust_model_helper_committer=Revīzijas iesūtītāja: Uzticēties parakstiem, k
 trust_model_helper_collaborator_committer=Līdzstrādnieka un revīzijas iesūtītāja: Uzticēties līdzstrādnieku parakstiem, kas atbilst revīzijas iesūtītājam
 trust_model_helper_default=Noklusētais: Izmantojiet šī servera noklusēto uzticamības modeli
 create_repo=Izveidot repozitoriju
-default_branch=Noklusējuma atzars
+default_branch=Noklusētais atzars
+default_branch_label=noklusējuma
 default_branch_helper=Noklusētais atzars nosaka pamata atzaru uz kuru tiks veidoti izmaiņu pieprasījumi un koda revīziju iesūtīšana.
 mirror_prune=Izmest
 mirror_prune_desc=Izdzēst visas ārējās atsauces, kas ārējā repozitorijā vairs neeksistē
@@ -959,6 +971,7 @@ delete_preexisting_success=Dzēst nepārņemtos failus direktorijā %s
 blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas
 author_search_tooltip=Tiks attēloti ne vairāk kā 30 lietotāji
 
+tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s
 
 transfer.accept=Apstiprināt īpašnieka maiņu
 transfer.accept_desc=`Mainīt īpašnieku uz "%s"`
@@ -1117,6 +1130,9 @@ commit_graph.select=Izvēlieties atzarus
 commit_graph.hide_pr_refs=Paslēpt izmaiņu pieprasījumus
 commit_graph.monochrome=Melnbalts
 commit_graph.color=Krāsa
+commit.contained_in=Šī revīzija ir iekļauta:
+commit.contained_in_default_branch=Šī revīzija ir daļa no noklusētā atzara
+commit.load_referencing_branches_and_tags=Ielādēt atzarus un tagus, kas atsaucas uz šo revīziju
 blame=Vainot
 download_file=Lejupielādēt failu
 normal_view=Parastais skats
@@ -1209,6 +1225,7 @@ commits.signed_by_untrusted_user=Parakstījis neuzticams lietotājs
 commits.signed_by_untrusted_user_unmatched=Parakstījis neuzticams lietotājs, kas neatbilst izmaiņu autoram
 commits.gpg_key_id=GPG atslēgas ID
 commits.ssh_key_fingerprint=SSH atslēgas identificējošā zīmju virkne
+commits.view_path=Skatīt šajā vēstures punktā
 
 commit.operations=Darbības
 commit.revert=Atgriezt
@@ -1219,7 +1236,7 @@ commit.cherry-pick-header=Izlasīt: %s
 commit.cherry-pick-content=Norādiet atzaru uz kuru izlasīt:
 
 commitstatus.error=Kļūda
-commitstatus.failure=Neveiksmīgs
+commitstatus.failure=Kļūme
 commitstatus.pending=Nav iesūtīts
 commitstatus.success=Pabeigts
 
@@ -1417,6 +1434,7 @@ issues.ref_from=`no %[1]s`
 issues.author=Autors
 issues.role.owner=Īpašnieks
 issues.role.member=Biedri
+issues.role.contributor_helper=Šis lietotājs repozitorijā ir iepriekš veicis labojumus.
 issues.re_request_review=Pieprasīt atkārtotu recenziju
 issues.is_stale=Šajā izmaiņu pieprasījumā ir notikušas izmaiņās, kopš veicāt tā recenziju
 issues.remove_request_review=Noņemt recenzijas pieprasījumu
@@ -1602,6 +1620,11 @@ pulls.switch_comparison_type=Mainīt salīdzināšanas tipu
 pulls.switch_head_and_base=Mainīt galvas un pamata atzarus
 pulls.filter_branch=Filtrēt atzarus
 pulls.no_results=Nekas netika atrasts.
+pulls.show_all_commits=Rādīt visas revīzijas
+pulls.showing_only_single_commit=Rāda tikai revīzijas %[1]s izmaiņas
+pulls.showing_specified_commit_range=Rāda tikai izmaiņas starp %[1]s..%[2]s
+pulls.select_commit_hold_shift_for_range=Atlasīt revīziju. Jātur Shift + klikšķis, lai atlasītu vairākas
+pulls.filter_changes_by_commit=Atlasīt pēc revīzijas
 pulls.nothing_to_compare=Nav ko salīdzināt, jo bāzes un salīdzināmie atzari ir vienādi.
 pulls.nothing_to_compare_and_allow_empty_pr=Šie atzari ir vienādi. Izveidotais izmaiņu pieprasījums būs tukšs.
 pulls.has_pull_request=`Izmaiņu pieprasījums starp šiem atzariem jau eksistē: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1705,6 +1728,7 @@ pulls.delete.title=Dzēst šo izmaiņu pieprasījumu?
 pulls.delete.text=Vai patiešām vēlaties dzēst šo izmaiņu pieprasījumu? (Neatgriezeniski tiks izdzēsts viss saturs. Apsveriet iespēju to aizvērt, ja vēlaties informāciju saglabāt vēsturei)
 
 
+pull.deleted_branch=(izdzēsts):%s
 
 milestones.new=Jauns atskaites punkts
 milestones.closed=Aizvērts %s
@@ -1733,7 +1757,17 @@ milestones.filter_sort.most_complete=Visvairāk pabeigtais
 milestones.filter_sort.most_issues=Visvairāk problēmu
 milestones.filter_sort.least_issues=Vismazāk problēmu
 
+signing.will_sign=Šī revīzija tiks parakstīta ar atslēgu "%s".
 signing.wont_sign.error=Notika kļūda pārbaudot vai revīzija var tikt parakstīta.
+signing.wont_sign.nokey=Nav pieejamas atslēgas, ar ko parakstīt šo revīziju.
+signing.wont_sign.never=Revīzijas nekad netiek parakstītas.
+signing.wont_sign.always=Revīzijas vienmēr tiek parakstītas.
+signing.wont_sign.pubkey=Revīzija netiks parakstīta, jo kontam nav piesaistīta publiskā atslēga.
+signing.wont_sign.twofa=Jābūt iespējotai divfaktoru autentifikācijai, lai parakstītu revīzijas.
+signing.wont_sign.parentsigned=Revīzija netiks parakstīta, jo nav parakstīta vecāka revīzija.
+signing.wont_sign.basesigned=Sapludināšanas revīzija netiks parakstīta, jo pamata revīzija nav parakstīta.
+signing.wont_sign.headsigned=Sapludināšanas revīzija netiks parakstīta, jo galvenā revīzija nav parakstīta.
+signing.wont_sign.commitssigned=Sapludināšana netiks parakstīta, jo visas saistītās revīzijas nav parakstītas.
 signing.wont_sign.not_signed_in=Jūs neesat pieteicies.
 
 ext_wiki=Piekļuve ārējai vikivietnei
@@ -2174,6 +2208,7 @@ settings.dismiss_stale_approvals_desc=Kad tiek iesūtītas jaunas revīzijas, ka
 settings.require_signed_commits=Pieprasīt parakstītas revīzijas
 settings.require_signed_commits_desc=Noraidīt iesūtītās izmaiņas šim atzaram, ja tās nav parakstītas vai nav iespējams pārbaudīt.
 settings.protect_branch_name_pattern=Aizsargātā zara šablons
+settings.protect_branch_name_pattern_desc=Aizsargāto atzaru nosaukumu šabloni. Šablonu pierakstu skatīt <a href="https://github.com/gobwas/glob">dokumentācijā</a>. Piemēri: main, release/**
 settings.protect_patterns=Šabloni
 settings.protect_protected_file_patterns=Aizsargāto failu šablons (vairākus var norādīt atdalot ar semikolu ';'):
 settings.protect_protected_file_patterns_desc=Aizsargātie faili, ko nevar mainīt, pat ja lietotājam ir tiesības veidot jaunus, labot vai dzēst failus šajā atzarā. Vairākus šablons ir iespējams norādīt atdalot tos ar semikolu (';'). Sīkāka informācija par šabloniem pieejama <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentācijā. Piemēram, <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2210,6 +2245,7 @@ settings.tags.protection.allowed.teams=Atļauts komandām
 settings.tags.protection.allowed.noone=Nevienam
 settings.tags.protection.create=Aizsargāt tagus
 settings.tags.protection.none=Nav uzstādīta tagu aizsargāšana.
+settings.tags.protection.pattern.description=Var izmantot vienkāršu nosaukumu vai glob šablonu, vai regulāro izteiksmi, lai atbilstu vairākiem tagiem. Vairāk ir lasāms <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">aizsargāto tagu šablonu dokumentācijā</a>.
 settings.bot_token=Bota pilnvara
 settings.chat_id=Tērzēšanas ID
 settings.matrix.homeserver_url=Mājas servera URL
@@ -2380,13 +2416,14 @@ branch.default_deletion_failed=Atzars "%s" ir noklusētais atzars un to nevar dz
 branch.restore=`Atjaunot atzaru "%s"`
 branch.download=`Lejupielādēt atzaru "%s"`
 branch.rename=`Pārsaukt atzaru "%s"`
+branch.search=Meklēt atzarā
 branch.included_desc=Šis atzars ir daļa no noklusēta atzara
 branch.included=Iekļauts
 branch.create_new_branch=Izveidot jaunu atzaru no atzara:
 branch.confirm_create_branch=Izveidot atzaru
 branch.warning_rename_default_branch=Tiks pārsaukts noklusētais atzars.
 branch.rename_branch_to=Pārsaukt "%s" uz:
-branch.confirm_rename_branch=Pārsaukt atzaru
+branch.confirm_rename_branch=Pārdēvēt atzaru
 branch.create_branch_operation=Izveidot atzaru
 branch.new_branch=Izveidot jaunu atzaru
 branch.new_branch_from=`Izveidot jaunu atzaru no "%s"`
@@ -2622,6 +2659,7 @@ dashboard.gc_lfs=Veikt atkritumu uzkopšanas darbus LFS meta objektiem
 dashboard.stop_zombie_tasks=Apturēt zombija uzdevumus
 dashboard.stop_endless_tasks=Apturēt nepārtrauktus uzdevumus
 dashboard.cancel_abandoned_jobs=Atcelt pamestus darbus
+dashboard.sync_branch.started=Sākta atzaru sinhronizācija
 
 users.user_manage_panel=Lietotāju kontu pārvaldība
 users.new_account=Izveidot lietotāja kontu
@@ -3330,10 +3368,12 @@ runs.all_workflows=Visas darbaplūsmas
 runs.commit=Revīzija
 runs.invalid_workflow_helper=Darbaplūsmas konfigurācijas fails ir kļūdains. Pārbaudiet konfiugrācijas failu: %s
 runs.status=Statuss
+runs.empty_commit_message=(tukšs revīzijas ziņojums)
 
 
 need_approval_desc=Nepieciešams apstiprinājums, lai izpildītu izmaiņu pieprasījumu darbaplūsmas no atdalītiem repozitorijiem.
 
+variables.id_not_exist=Mainīgais ar identifikatoru %d neeksistē.
 
 [projects]
 type-1.display_name=Individuālais projekts

From e4e5d76932e9d5ba1f8c63213aefae1493012a81 Mon Sep 17 00:00:00 2001
From: Rafael Heard <rafael.heard@gmail.com>
Date: Mon, 19 Feb 2024 20:01:48 -0500
Subject: [PATCH 101/679] Left align the input labels for the link account page
 (#29255)

In a previous [PR](https://github.com/go-gitea/gitea/pull/28753) we
moved the labels to be above the inputs. The PR ensures that the
alignment is also on both tabs of the link account page
(`/user/link_account`).

Before
<img width="1094" alt="before"
src="https://github.com/go-gitea/gitea/assets/6152817/ac1e86bd-c4d6-4e45-87d1-87bb8a736149">

After
<img width="1094" alt="after"
src="https://github.com/go-gitea/gitea/assets/6152817/1b5fc109-f4d2-43ee-b924-0a9e53a0e391">

---------

Co-authored-by: rafh <rafaelheard@gmail.com>
---
 web_src/css/form.css | 2 --
 1 file changed, 2 deletions(-)

diff --git a/web_src/css/form.css b/web_src/css/form.css
index c0de4978dd..a5288c9309 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -242,7 +242,6 @@ textarea:focus,
 .user.activate form,
 .user.forgot.password form,
 .user.reset.password form,
-.user.link-account form,
 .user.signup form {
   margin: auto;
   width: 700px !important;
@@ -277,7 +276,6 @@ textarea:focus,
   .user.activate form .inline.field > label,
   .user.forgot.password form .inline.field > label,
   .user.reset.password form .inline.field > label,
-  .user.link-account form .inline.field > label,
   .user.signup form .inline.field > label {
     text-align: right;
     width: 250px !important;

From 8c21bc0d51ab22c0d05d8ce2ea8bc80d6f893800 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Tue, 20 Feb 2024 09:39:44 +0800
Subject: [PATCH 102/679] Do not show delete button when time tracker is
 disabled (#29257)

Fix #29233

The delete button of time logs won't be shown when the time tracker is disabled.

![image](https://github.com/go-gitea/gitea/assets/15528715/5cc4e0c9-d2f9-4b8f-a2f5-fe202b94c191)
---
 templates/repo/issue/view_content/comments_delete_time.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl
index 7c01bb4228..95121b0dc7 100644
--- a/templates/repo/issue/view_content/comments_delete_time.tmpl
+++ b/templates/repo/issue/view_content/comments_delete_time.tmpl
@@ -1,4 +1,4 @@
-{{if .comment.Time}} {{/* compatibility with time comments made before v1.14 */}}
+{{if and .comment.Time (.ctxData.Repository.IsTimetrackerEnabled ctx)}} {{/* compatibility with time comments made before v1.14 */}}
 	{{if (not .comment.Time.Deleted)}}
 		{{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}}
 			<span class="gt-float-right">

From ade1110e8b7d94dc142a259854e2b73845eab8b9 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 20 Feb 2024 12:37:37 +0200
Subject: [PATCH 103/679] Remove jQuery from repo wiki creation page (#29271)

- Switched to plain JavaScript
- Tested the wiki creation form functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/2dfc95fd-40cc-4ffb-9ae6-50f798fddd67)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 .../js/features/comp/ComboMarkdownEditor.js   | 16 +++---
 web_src/js/features/repo-diff.js              |  4 +-
 web_src/js/features/repo-wiki.js              | 56 ++++++++++---------
 web_src/js/utils/dom.js                       | 12 ++++
 4 files changed, 51 insertions(+), 37 deletions(-)

diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index d486c5830a..d209f11ab2 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -2,7 +2,7 @@ import '@github/markdown-toolbar-element';
 import '@github/text-expander-element';
 import $ from 'jquery';
 import {attachTribute} from '../tribute.js';
-import {hideElem, showElem, autosize} from '../../utils/dom.js';
+import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js';
 import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
 import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
 import {renderPreviewPanelContent} from '../repo-editor.js';
@@ -14,17 +14,17 @@ let elementIdCounter = 0;
 
 /**
  * validate if the given textarea is non-empty.
- * @param {jQuery} $textarea
+ * @param {HTMLElement} textarea - The textarea element to be validated.
  * @returns {boolean} returns true if validation succeeded.
  */
-export function validateTextareaNonEmpty($textarea) {
+export function validateTextareaNonEmpty(textarea) {
   // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation.
   // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert.
-  if (!$textarea.val()) {
-    if ($textarea.is(':visible')) {
-      $textarea.prop('required', true);
-      const $form = $textarea.parents('form');
-      $form[0]?.reportValidity();
+  if (!textarea.value) {
+    if (isElemVisible(textarea)) {
+      textarea.required = true;
+      const form = textarea.closest('form');
+      form?.reportValidity();
     } else {
       // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places.
       showErrorToast('Require non-empty content');
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 6d6f382613..5c73bf4bbc 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -47,8 +47,8 @@ function initRepoDiffConversationForm() {
     e.preventDefault();
 
     const $form = $(e.target);
-    const $textArea = $form.find('textarea');
-    if (!validateTextareaNonEmpty($textArea)) {
+    const textArea = e.target.querySelector('textarea');
+    if (!validateTextareaNonEmpty(textArea)) {
       return;
     }
 
diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js
index 58036fde37..d51bf35c81 100644
--- a/web_src/js/features/repo-wiki.js
+++ b/web_src/js/features/repo-wiki.js
@@ -1,50 +1,51 @@
-import $ from 'jquery';
 import {initMarkupContent} from '../markup/content.js';
 import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {fomanticMobileScreen} from '../modules/fomantic.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
 
 async function initRepoWikiFormEditor() {
-  const $editArea = $('.repository.wiki .combo-markdown-editor textarea');
-  if (!$editArea.length) return;
+  const editArea = document.querySelector('.repository.wiki .combo-markdown-editor textarea');
+  if (!editArea) return;
 
-  const $form = $('.repository.wiki.new .ui.form');
-  const $editorContainer = $form.find('.combo-markdown-editor');
+  const form = document.querySelector('.repository.wiki.new .ui.form');
+  const editorContainer = form.querySelector('.combo-markdown-editor');
   let editor;
 
   let renderRequesting = false;
   let lastContent;
-  const renderEasyMDEPreview = function () {
+  const renderEasyMDEPreview = async function () {
     if (renderRequesting) return;
 
-    const $previewFull = $editorContainer.find('.EasyMDEContainer .editor-preview-active');
-    const $previewSide = $editorContainer.find('.EasyMDEContainer .editor-preview-active-side');
-    const $previewTarget = $previewSide.length ? $previewSide : $previewFull;
-    const newContent = $editArea.val();
-    if (editor && $previewTarget.length && lastContent !== newContent) {
+    const previewFull = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active');
+    const previewSide = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active-side');
+    const previewTarget = previewSide || previewFull;
+    const newContent = editArea.value;
+    if (editor && previewTarget && lastContent !== newContent) {
       renderRequesting = true;
-      $.post(editor.previewUrl, {
-        _csrf: csrfToken,
-        mode: editor.previewMode,
-        context: editor.previewContext,
-        text: newContent,
-        wiki: editor.previewWiki,
-      }).done((data) => {
+      const formData = new FormData();
+      formData.append('mode', editor.previewMode);
+      formData.append('context', editor.previewContext);
+      formData.append('text', newContent);
+      formData.append('wiki', editor.previewWiki);
+      try {
+        const response = await POST(editor.previewUrl, {data: formData});
+        const data = await response.text();
         lastContent = newContent;
-        $previewTarget.html(`<div class="markup ui segment">${data}</div>`);
+        previewTarget.innerHTML = `<div class="markup ui segment">${data}</div>`;
         initMarkupContent();
-      }).always(() => {
+      } catch (error) {
+        console.error('Error rendering preview:', error);
+      } finally {
         renderRequesting = false;
         setTimeout(renderEasyMDEPreview, 1000);
-      });
+      }
     } else {
       setTimeout(renderEasyMDEPreview, 1000);
     }
   };
   renderEasyMDEPreview();
 
-  editor = await initComboMarkdownEditor($editorContainer, {
+  editor = await initComboMarkdownEditor(editorContainer, {
     useScene: 'wiki',
     // EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
     // And another benefit is that we only need to write the style once for both editors.
@@ -64,9 +65,10 @@ async function initRepoWikiFormEditor() {
     },
   });
 
-  $form.on('submit', () => {
-    if (!validateTextareaNonEmpty($editArea)) {
-      return false;
+  form.addEventListener('submit', (e) => {
+    if (!validateTextareaNonEmpty(editArea)) {
+      e.preventDefault();
+      e.stopPropagation();
     }
   });
 }
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index fb6b751140..ca24650f76 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -227,3 +227,15 @@ export function initSubmitEventPolyfill() {
   document.body.addEventListener('click', submitEventPolyfillListener);
   document.body.addEventListener('focus', submitEventPolyfillListener);
 }
+
+/**
+ * Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
+ * Note: This function doesn't account for all possible visibility scenarios.
+ * @param {HTMLElement} element The element to check.
+ * @returns {boolean} True if the element is visible.
+ */
+export function isElemVisible(element) {
+  if (!element) return false;
+
+  return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
+}

From 3f73eabb666bd68af7a5317eaa2f97be52f35a26 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 20 Feb 2024 21:12:47 +0100
Subject: [PATCH 104/679] Explained where create issue/PR template (#29035)
 (#29266)

For some user (as me), documentation lack of precision about where to
store issue/pr template.

I propose an enhancement about this point. With bold exergue and
precision about server itself.

I've found some user with same interrogation as :
https://forum.gitea.com/t/issue-template-directory/3328

---------

Co-authored-by: Km <cam.lafit@azerttyu.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 docs/content/usage/issue-pull-request-templates.en-us.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/docs/content/usage/issue-pull-request-templates.en-us.md b/docs/content/usage/issue-pull-request-templates.en-us.md
index 34475e3465..b031b262fb 100644
--- a/docs/content/usage/issue-pull-request-templates.en-us.md
+++ b/docs/content/usage/issue-pull-request-templates.en-us.md
@@ -19,9 +19,10 @@ menu:
 
 Some projects have a standard list of questions that users need to answer
 when creating an issue or pull request. Gitea supports adding templates to the
-main branch of the repository so that they can autopopulate the form when users are
+**default branch of the repository** so that they can autopopulate the form when users are
 creating issues and pull requests. This will cut down on the initial back and forth
 of getting some clarifying details.
+It is currently not possible to provide generic issue/pull-request templates globally.
 
 Additionally, the New Issue page URL can be suffixed with `?title=Issue+Title&body=Issue+Text` and the form will be populated with those strings. Those strings will be used instead of the template if there is one.
 

From a5c570c1e02302212a5d8f7cf7d91f24ab0578d5 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 21 Feb 2024 01:05:17 +0100
Subject: [PATCH 105/679] Remove jQuery .map() and enable eslint rules for it
 (#29272)

- Use case in `repo-commit` was tested until the point where the POST
request was sent with the same payload.
- Use case in `repo-legacy` was tested completely with comment editing.
- `jquery/no-fade` was disabled as well to stay in sync with
`no-jquery/no-fade`, had no violations.
---
 .eslintrc.yaml                     |  6 +++---
 web_src/js/features/repo-commit.js | 18 ++++++++----------
 web_src/js/features/repo-legacy.js |  9 +++------
 3 files changed, 14 insertions(+), 19 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index ab9c218849..e9991c02ba 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -296,7 +296,7 @@ rules:
   jquery/no-delegate: [2]
   jquery/no-each: [0]
   jquery/no-extend: [2]
-  jquery/no-fade: [0]
+  jquery/no-fade: [2]
   jquery/no-filter: [0]
   jquery/no-find: [0]
   jquery/no-global-eval: [2]
@@ -309,7 +309,7 @@ rules:
   jquery/no-is-function: [2]
   jquery/no-is: [0]
   jquery/no-load: [2]
-  jquery/no-map: [0]
+  jquery/no-map: [2]
   jquery/no-merge: [2]
   jquery/no-param: [2]
   jquery/no-parent: [0]
@@ -451,7 +451,7 @@ rules:
   no-jquery/no-load: [2]
   no-jquery/no-map-collection: [0]
   no-jquery/no-map-util: [2]
-  no-jquery/no-map: [0]
+  no-jquery/no-map: [2]
   no-jquery/no-merge: [2]
   no-jquery/no-node-name: [2]
   no-jquery/no-noop: [2]
diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js
index 76b34d2077..fc70ba41e4 100644
--- a/web_src/js/features/repo-commit.js
+++ b/web_src/js/features/repo-commit.js
@@ -14,17 +14,15 @@ export function initRepoEllipsisButton() {
 }
 
 export function initRepoCommitLastCommitLoader() {
+  const notReadyEls = document.querySelectorAll('table#repo-files-table tr.notready');
+  if (!notReadyEls.length) return;
+
   const entryMap = {};
-
-  const entries = $('table#repo-files-table tr.notready')
-    .map((_, v) => {
-      entryMap[$(v).attr('data-entryname')] = $(v);
-      return $(v).attr('data-entryname');
-    })
-    .get();
-
-  if (entries.length === 0) {
-    return;
+  const entries = [];
+  for (const el of notReadyEls) {
+    const entryname = el.getAttribute('data-entryname');
+    entryMap[entryname] = $(el);
+    entries.push(entryname);
   }
 
   const lastCommitLoaderURL = $('table#repo-files-table').data('lastCommitLoaderUrl');
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index ce1bff11a2..10ad836797 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -398,17 +398,14 @@ async function onEditContent(event) {
     }
   };
 
-  const saveAndRefresh = (dz, $dropzone) => {
+  const saveAndRefresh = (dz) => {
     showElem($renderContent);
     hideElem($editContentZone);
-    const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
-      return $(this).val();
-    }).get();
     $.post($editContentZone.attr('data-update-url'), {
       _csrf: csrfToken,
       content: comboMarkdownEditor.value(),
       context: $editContentZone.attr('data-context'),
-      files: $attachments,
+      files: dz.files.map((file) => file.uuid),
     }, (data) => {
       if (!data.content) {
         $renderContent.html($('#no-content').html());
@@ -452,7 +449,7 @@ async function onEditContent(event) {
     });
     $editContentZone.find('.save.button').on('click', (e) => {
       e.preventDefault();
-      saveAndRefresh(dz, $dropzone);
+      saveAndRefresh(dz);
     });
   } else {
     comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));

From 69dbfbe4e52845a807302a15e8d79d183acf683b Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Wed, 21 Feb 2024 00:23:41 +0000
Subject: [PATCH 106/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_lv-LV.ini | 233 +++++++++++++++++++++++++++++---
 1 file changed, 212 insertions(+), 21 deletions(-)

diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index d85cdb24a4..d4a8740f79 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -17,10 +17,11 @@ template=Sagatave
 language=Valoda
 notifications=Paziņojumi
 active_stopwatch=Aktīvā laika uzskaite
+tracked_time_summary=Izsekojamā laika apkopojums, kas ir balstīts uz pieteikumu saraksta atlasi
 create_new=Izveidot…
 user_profile_and_more=Profils un iestatījumi…
 signed_in_as=Pieteicies kā
-enable_javascript=Šai lapas darbībai ir nepieciešams JavaScript.
+enable_javascript=Šai tīmekļvietnei ir nepieciešams JavaScript.
 toc=Satura rādītājs
 licenses=Licences
 return_to_gitea=Atgriezties Gitea
@@ -40,12 +41,12 @@ webauthn_sign_in=Nospiediet pogu uz drošības atslēgas. Ja tai nav pogas, izņ
 webauthn_press_button=Nospiediet drošības atslēgas pogu…
 webauthn_use_twofa=Izmantot divfaktoru kodu no tālruņa
 webauthn_error=Nevar nolasīt drošības atslēgu.
-webauthn_unsupported_browser=Jūsu pārlūkprogramma neatbalsta WebAuthn standartu.
+webauthn_unsupported_browser=Jūsu pārlūks neatbalsta WebAuthn standartu.
 webauthn_error_unknown=Notikusi nezināma kļūda. Atkārtojiet darbību vēlreiz.
-webauthn_error_insecure=WebAuthn atbalsta tikai drošus savienojumus ar serveri
-webauthn_error_unable_to_process=Serveris nevar apstrādāt Jūsu pieprasījumu.
+webauthn_error_insecure=`WebAuthn atbalsta tikai drošus savienojumus. Pārbaudīšanai ar HTTP var izmantot izcelsmi "localhost" vai "127.0.0.1"`
+webauthn_error_unable_to_process=Serveris nevarēja apstrādāt pieprasījumu.
 webauthn_error_duplicated=Drošības atslēga nav atļauta šim pieprasījumam. Pārliecinieties, ka šī atslēga jau nav reģistrēta.
-webauthn_error_empty=Norādiet atslēgas nosaukumu.
+webauthn_error_empty=Jānorāda šīs atslēgas nosaukums.
 webauthn_error_timeout=Iestājusies noildze, mēģinot, nolasīt atslēgu. Pārlādējiet lapu un mēģiniet vēlreiz.
 webauthn_reload=Pārlādēt
 
@@ -60,11 +61,11 @@ new_org=Jauna organizācija
 new_project=Jauns projekts
 new_project_column=Jauna kolonna
 manage_org=Pārvaldīt organizācijas
-admin_panel=Lapas administrēšana
+admin_panel=Vietnes administrēšana
 account_settings=Konta iestatījumi
 settings=Iestatījumi
 your_profile=Profils
-your_starred=Atzīmēts ar zvaigznīti
+your_starred=Pievienots izlasē
 your_settings=Iestatījumi
 
 all=Visi
@@ -90,9 +91,11 @@ remove=Noņemt
 remove_all=Noņemt visus
 remove_label_str=`Noņemt ierakstu "%s"`
 edit=Labot
+view=Skatīt
 
 enabled=Iespējots
 disabled=Atspējots
+locked=Slēgts
 
 copy=Kopēt
 copy_url=Kopēt saiti
@@ -131,6 +134,7 @@ concept_user_organization=Organizācija
 show_timestamps=Rādīt laika zīmogus
 show_log_seconds=Rādīt sekundes
 show_full_screen=Atvērt pilnā logā
+download_logs=Lejupielādēt žurnālus
 
 confirm_delete_selected=Apstiprināt, lai izdzēstu visus atlasītos vienumus?
 
@@ -171,6 +175,7 @@ string.desc=Z - A
 
 [error]
 occurred=Radusies kļūda
+report_message=Ja ir pārliecība, ka šī ir Gitea nepilnība, lūgums pārbaudīt <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a>, vai tā jau nav zināma, vai izveidot jaunu pieteikumu, ja nepieciešams.
 missing_csrf=Kļūdains pieprasījums: netika iesūtīta drošības pilnvara
 invalid_csrf=Kļūdains pieprasījums: iesūtīta kļūdaina drošības pilnvara
 not_found=Pieprasītie dati netika atrasti.
@@ -179,6 +184,7 @@ network_error=Tīkla kļūda
 [startpage]
 app_desc=Viegli uzstādāms Git serviss
 install=Vienkārši instalējams
+install_desc=Vienkārši <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">jāpalaiž izpildāmais fails</a> vajadzīgajai platformai, jāizmanto <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, vai jāiegūst <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">pakotne</a>.
 platform=Pieejama dažādām platformām
 platform_desc=Gitea iespējams uzstādīt jebkur, kam <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> var nokompilēt: Windows, macOS, Linux, ARM utt. Izvēlies to, kas tev patīk!
 lightweight=Viegla
@@ -223,6 +229,7 @@ repo_path_helper=Git repozitoriji tiks glabāti šajā direktorijā.
 lfs_path=Git LFS glabāšanas vieta
 lfs_path_helper=Faili, kas pievienoti Git LFS, tiks glabāti šajā direktorijā. Atstājiet tukšu, lai atspējotu.
 run_user=Izpildes lietotājs
+run_user_helper=Operētājsistēms lietotājs, ar kuru tiks palaists Gitea. Jāņem vērā, ka šim lietotājam ir jābūt piekļuvei repozitorija atrašanās vietai.
 domain=Servera domēns
 domain_helper=Domēns vai servera adrese.
 ssh_port=SSH servera ports
@@ -294,6 +301,8 @@ invalid_password_algorithm=Kļūdaina paroles jaucējfunkcija
 password_algorithm_helper=Norādiet paroles jaucējalgoritmu. Algoritmi atšķirās pēc prasībām pret resursiem un stipruma. Argon2 algoritms ir drošs, bet tam nepieciešams daudz operatīvās atmiņas, līdz ar ko tas var nebūt piemērots sistēmām ar maz pieejamajiem resursiem.
 enable_update_checker=Iespējot jaunu versiju paziņojumus
 enable_update_checker_helper=Periodiski pārbaudīt jaunu version pieejamību, izgūstot datus no gitea.io.
+env_config_keys=Vides konfigurācija
+env_config_keys_prompt=Šie vides mainīgie tiks pielietoti arī konfigurācijas failā:
 
 [home]
 uname_holder=Lietotājvārds vai e-pasts
@@ -352,9 +361,11 @@ disable_register_prompt=Reģistrācija ir atspējota. Lūdzu, sazinieties ar vie
 disable_register_mail=Reģistrācijas e-pasta apstiprināšana ir atspējota.
 manual_activation_only=Sazinieties ar lapas administratoru, lai pabeigtu konta aktivizāciju.
 remember_me=Atcerēties šo ierīci
+remember_me.compromised=Pieteikšanās pilnvara vairs nav derīga, kas var norādīt uz ļaunprātīgām darbībām kontā. Lūgums pārbaudīt, vai kontā nav neparastu darbību.
 forgot_password_title=Aizmirsu paroli
 forgot_password=Aizmirsi paroli?
 sign_up_now=Nepieciešams konts? Reģistrējies tagad.
+sign_up_successful=Konts tika veiksmīgi izveidots. Laipni lūdzam!
 confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz <b>%s</b>, pārbaudies savu e-pasta kontu tuvāko %s laikā, lai pabeigtu reģistrācijas procesu.
 must_change_password=Mainīt paroli
 allow_password_change=Pieprasīt lietotājam mainīt paroli (ieteicams)
@@ -370,6 +381,7 @@ email_not_associate=Šī e-pasta adrese nav saistīta ar nevienu kontu.
 send_reset_mail=Nosūtīt paroles atjaunošanas e-pastu
 reset_password=Paroles atjaunošana
 invalid_code=Jūsu apstiprināšanas kodam ir beidzies derīguma termiņš vai arī tas ir nepareizs.
+invalid_code_forgot_password=Apliecinājuma kods ir nederīgs vai tā derīgums ir beidzies. Nospiediet <a href="%s">šeit</a>, lai uzsāktu jaunu sesiju.
 invalid_password=Jūsu parole neatbilst parolei, kas tika ievadīta veidojot so kontu.
 reset_password_helper=Atjaunot paroli
 reset_password_wrong_user=Jūs esat pieteicies kā %s, bet konta atkopšanas saite ir paredzēta lietotājam %s
@@ -397,6 +409,7 @@ openid_connect_title=Pievienoties jau esošam kontam
 openid_connect_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam.
 openid_register_title=Izveidot jaunu kontu
 openid_register_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam.
+openid_signin_desc=Jāievada OpenID URI. Piemēram, anna.openid.example.org vai https://openid.example.org/anna.
 disable_forgot_password_mail=Konta atjaunošana ir atspējota, jo nav uzstādīti e-pasta servera iestatījumi. Sazinieties ar lapas administratoru.
 disable_forgot_password_mail_admin=Kontu atjaunošana ir pieejama tikai, ja ir veikta e-pasta servera iestatījumu konfigurēšana. Norādiet e-pasta servera iestatījumus, lai iespējotu kontu atjaunošanu.
 email_domain_blacklisted=Nav atļauts reģistrēties ar šādu e-pasta adresi.
@@ -406,7 +419,9 @@ authorize_application_created_by=Šo lietotni izveidoja %s.
 authorize_application_description=Ja piešķirsiet tiesības, tā varēs piekļūt un mainīt Jūsu konta informāciju, ieskaitot privātos repozitorijus un organizācijas.
 authorize_title=Autorizēt "%s" piekļuvi jūsu kontam?
 authorization_failed=Autorizācija neizdevās
+authorization_failed_desc=Autentifikācija neizdevās, jo tika veikts kļūdains pieprasījums. Sazinieties ar lietojumprogrammas, ar kuru mēģinājāt autentificēties, uzturētāju.
 sspi_auth_failed=SSPI autentifikācija neizdevās
+password_pwned=Izvēlētā parole ir <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">nozagto paroļu sarakstā</a>, kas iepriekš ir atklāts publiskās datu noplūdēs. Lūgums mēģināt vēlreiz ar citu paroli un apsvērt to nomainīt arī citur.
 password_pwned_err=Neizdevās pabeigt pieprasījumu uz HaveIBeenPwned
 
 [mail]
@@ -421,6 +436,7 @@ activate_account.text_1=Sveiki <b>%[1]s</b>, esat reģistrējies %[2]s!
 activate_account.text_2=Nospiediet uz saites, lai aktivizētu savu kontu lapā <b>%s</b>:
 
 activate_email=Apstipriniet savu e-pasta adresi
+activate_email.title=%s, apstipriniet savu e-pasta adresi
 activate_email.text=Nospiediet uz saites, lai apstiprinātu savu e-pasta adresi lapā <b>%s</b>:
 
 register_notify=Laipni lūdzam Gitea
@@ -619,6 +635,7 @@ webauthn=Drošības atslēgas
 public_profile=Publiskais profils
 biography_placeholder=Pastāsti mums mazliet par sevi! (Var izmantot Markdown)
 location_placeholder=Kopīgot savu aptuveno atrašanās vietu ar citiem
+profile_desc=Norādīt, kā profils tiek attēlots citiem lietotājiem. Primārā e-pasta adrese tiks izmantota paziņojumiem, paroles atjaunošanai un Git tīmekļa darbībām.
 password_username_disabled=Ne-lokāliem lietotājiem nav atļauts mainīt savu lietotāja vārdu. Sazinieties ar sistēmas administratoru, lai uzzinātu sīkāk.
 full_name=Pilns vārds
 website=Mājas lapa
@@ -630,6 +647,8 @@ update_language_not_found=Valoda "%s" nav pieejama.
 update_language_success=Valoda tika nomainīta.
 update_profile_success=Jūsu profila informācija tika saglabāta.
 change_username=Lietotājvārds mainīts.
+change_username_prompt=Piezīme: lietotājvārda mainīšana maina arī konta URL.
+change_username_redirect_prompt=Iepriekšējais lietotājvārds tiks pārvirzīts, kamēr neviens cits to neizmanto.
 continue=Turpināt
 cancel=Atcelt
 language=Valoda
@@ -654,6 +673,7 @@ comment_type_group_project=Projektus
 comment_type_group_issue_ref=Problēmu atsauces
 saved_successfully=Iestatījumi tika veiksmīgi saglabati.
 privacy=Privātums
+keep_activity_private=Profila lapā paslēpt notikumus
 keep_activity_private_popup=Savu aktivitāti redzēsiet tikai Jūs un administratori
 
 lookup_avatar_by_mail=Meklēt profila bildes pēc e-pasta
@@ -663,12 +683,14 @@ choose_new_avatar=Izvēlēties jaunu profila attēlu
 update_avatar=Saglabāt profila bildi
 delete_current_avatar=Dzēst pašreizējo profila bildi
 uploaded_avatar_not_a_image=Augšupielādētais fails nav attēls.
+uploaded_avatar_is_too_big=Augšupielādētā faila izmērs (%d KiB) pārsniedz pieļaujamo izmēru (%d KiB).
 update_avatar_success=Profila attēls tika saglabāts.
 update_user_avatar_success=Lietotāja profila attēls tika atjaunots.
 
 change_password=Mainīt paroli
 old_password=Pašreizējā parole
 new_password=Jauna parole
+retype_new_password=Apstiprināt jauno paroli
 password_incorrect=Ievadīta nepareiza pašreizējā parole.
 change_password_success=Parole tika veiksmīgi nomainīta. Tagad varat pieteikties ar jauno paroli.
 password_change_disabled=Ārējie konti nevar mainīt paroli, izmantojot, Gitea saskarni.
@@ -677,6 +699,7 @@ emails=E-pasta adreses
 manage_emails=Pārvaldīt e-pasta adreses
 manage_themes=Izvēlieties noklusējuma motīvu
 manage_openid=Pārvaldīt OpenID adreses
+email_desc=Primārā e-pasta adrese tiks izmantota paziņojumiem, paroļu atjaunošanai un, ja tā nav paslēpta, Git tīmekļa darbībām.
 theme_desc=Šis būs noklusējuma motīvs visiem lietotājiem.
 primary=Primārā
 activated=Aktivizēts
@@ -684,6 +707,7 @@ requires_activation=Nepieciešams aktivizēt
 primary_email=Uzstādīt kā primāro
 activate_email=Nosūtīt aktivizācijas e-pastu
 activations_pending=Gaida aktivizāciju
+can_not_add_email_activations_pending=Ir nepabeigta aktivizācija. Pēc dažām minūtēm mēģiniet vēlreiz, ja ir vēlme pievienot jaunu e-pasta adresi.
 delete_email=Noņemt
 email_deletion=Dzēst e-pasta adresi
 email_deletion_desc=E-pasta adrese un ar to saistītā informācija tiks dzēsta no šī konta. Git revīzijas ar šo e-pasta adresi netiks mainītas. Vai turpināt?
@@ -783,6 +807,7 @@ ssh_externally_managed=Šim lietotājam SSH atslēga tiek pāvaldīta attālinā
 manage_social=Pārvaldīt piesaistītos sociālos kontus
 social_desc=Šie sociālo tīklu konti var tikt izmantoti, lai pieteiktos. Pārliecinieties, ka visi ir atpazīstami.
 unbind=Atsaistīt
+unbind_success=Sociālā tīkla konts tika veiksmīgi noņemts.
 
 manage_access_token=Pārvaldīt piekļuves pilnvaras
 generate_new_token=Izveidot jaunu pilnvaru
@@ -802,7 +827,9 @@ permissions_public_only=Tikai publiskie
 permissions_access_all=Visi (publiskie, privātie un ierobežotie)
 select_permissions=Norādiet tiesības
 permission_no_access=Nav piekļuves
-permission_read=Izlasītie
+permission_read=Skatīšanās
+permission_write=Skatīšanās un raksīšanas
+access_token_desc=Atzīmētie pilnvaras apgabali ierobežo autentifikāciju tikai atbilstošiem <a %s>API</a> izsaukumiem. Sīkāka informācija pieejama <a %s>dokumentācijā</a>.
 at_least_one_permission=Nepieciešams norādīt vismaz vienu tiesību, lai izveidotu pilnvaru
 permissions_list=Tiesības:
 
@@ -814,6 +841,8 @@ remove_oauth2_application_desc=Noņemot OAuth2 lietotni, tiks noņemta piekļuve
 remove_oauth2_application_success=Lietotne tika dzēsta.
 create_oauth2_application=Izveidot jaunu OAuth2 lietotni
 create_oauth2_application_button=Izveidot lietotni
+create_oauth2_application_success=Ir veiksmīgi izveidota jauna OAuth2 lietotne.
+update_oauth2_application_success=Ir veiksmīgi atjaunota OAuth2 lietotne.
 oauth2_application_name=Lietotnes nosaukums
 oauth2_confidential_client=Konfidenciāls klients. Norādiet lietotēm, kas glabā noslēpumu slepenībā, piemēram, tīmekļa lietotnēm. Nenorādiet instalējamām lietotnēm, tai skaitā darbavirsmas vai mobilajām lietotnēm.
 oauth2_redirect_uris=Pārsūtīšanas URI. Norādiet katru URI savā rindā.
@@ -822,20 +851,26 @@ oauth2_client_id=Klienta ID
 oauth2_client_secret=Klienta noslēpums
 oauth2_regenerate_secret=Pārģenerēt noslēpumus
 oauth2_regenerate_secret_hint=Pazaudēts noslēpums?
+oauth2_client_secret_hint=Pēc šīs lapas pamešanas vai atsvaidzināšanas noslēpums vairs netiks parādīts. Lūgums pārliecināties, ka tas ir saglabāts.
 oauth2_application_edit=Labot
 oauth2_application_create_description=OAuth2 lietotnes ļauj trešas puses lietotnēm piekļūt lietotāja kontiem šajā instancē.
+oauth2_application_remove_description=OAuth2 lietotnes noņemšana liegs tai piekļūt pilnvarotiem lietotāju kontiem šajā instancē. Vai turpināt?
+oauth2_application_locked=Gitea sāknēšanas brīdī reģistrē dažas OAuth2 lietotnes, ja tas ir iespējots konfigurācijā. Lai novērstu negaidītu uzvedību, tās nevar ne labot, ne noņemt. Lūgums vērsties OAuth2 dokumentācijā pēc vairāk informācijas.
 
 authorized_oauth2_applications=Autorizētās OAuth2 lietotnes
+authorized_oauth2_applications_description=Ir ļauta piekļuve savam Gitea kontam šīm trešo pušu lietotnēm. Lūgums atsaukt piekļuvi lietotnēm, kas vairs nav nepieciešamas.
 revoke_key=Atsaukt
 revoke_oauth2_grant=Atsaukt piekļuvi
 revoke_oauth2_grant_description=Atsaucot piekļuvi šai trešas puses lietotnei tiks liegta piekļuve Jūsu datiem. Vai turpināt?
 revoke_oauth2_grant_success=Piekļuve veiksmīgi atsaukta.
 
 twofa_desc=Divfaktoru autentifikācija uzlabo konta drošību.
+twofa_recovery_tip=Ja ierīce tiek pazaudēta, iespējams izmantot vienreiz izmantojamo atkopšanas atslēgu, lai atgūtu piekļuvi savam kontam.
 twofa_is_enrolled=Kontam ir <strong>ieslēgta</strong> divfaktoru autentifikācija.
 twofa_not_enrolled=Kontam šobrīd nav ieslēgta divfaktoru autentifikācija.
 twofa_disable=Atslēgt divfaktoru autentifikāciju
 twofa_scratch_token_regenerate=Ģenerēt jaunu vienreizējo kodu
+twofa_scratch_token_regenerated=Vienreizējā pilnvara tagad ir %s. Tā ir jāglabā drošā vietā, tā vairs nekad netiks rādīta.
 twofa_enroll=Ieslēgt divfaktoru autentifikāciju
 twofa_disable_note=Nepieciešamības gadījumā divfaktoru autentifikāciju ir iespējams atslēgt.
 twofa_disable_desc=Atslēdzot divfaktoru autentifikāciju, konts vairs nebūs tik drošs. Vai turpināt?
@@ -853,6 +888,8 @@ webauthn_register_key=Pievienot drošības atslēgu
 webauthn_nickname=Segvārds
 webauthn_delete_key=Noņemt drošības atslēgu
 webauthn_delete_key_desc=Noņemot drošības atslēgu ar to vairs nebūs iespējams pieteikties. Vai turpināt?
+webauthn_key_loss_warning=Ja tiek pazaudētas drošības atslēgas, tiks zaudēta piekļuve kontam.
+webauthn_alternative_tip=Ir vēlams uzstādīt papildu autentifikācijas veidu.
 
 manage_account_links=Pārvaldīt saistītos kontus
 manage_account_links_desc=Šādi ārējie konti ir piesaistīti Jūsu Gitea kontam.
@@ -862,8 +899,10 @@ remove_account_link=Noņemt saistīto kontu
 remove_account_link_desc=Noņemot saistīto kontu, tam tiks liegta piekļuve Jūsu Gitea kontam. Vai turpināt?
 remove_account_link_success=Saistītais konts tika noņemts.
 
+hooks.desc=Pievienot tīmekļa āķus, kas izpildīsies <strong>visos repozitorijos</strong>, kas jums pieder.
 
 orgs_none=Jūs neesat nevienas organizācijas biedrs.
+repos_none=Jums nepieder neviens repozitorijs.
 
 delete_account=Dzēst savu kontu
 delete_prompt=Šī darbība pilnībā izdzēsīs Jūsu kontu, kā arī tā ir <strong>NEATGRIEZENISKA</strong>.
@@ -884,8 +923,10 @@ visibility.public_tooltip=Redzams ikvienam
 visibility.limited=Ierobežota
 visibility.limited_tooltip=Redzams tikai autentificētiem lietotājiem
 visibility.private=Privāts
+visibility.private_tooltip=Redzams tikai organizāciju, kurām esi pievienojies, dalībniekiem
 
 [repo]
+new_repo_helper=Repozitorijs satur visus projekta failus, tajā skaitā izmaiņu vēsturi. Jau tiek glabāts kaut kur citur? <a href="%s">Pārnest repozitoriju.</a>
 owner=Īpašnieks
 owner_helper=Ņemot vērā maksimālā repozitoriju skaita ierobežojumu, ne visas organizācijas var tikt parādītas sarakstā.
 repo_name=Repozitorija nosaukums
@@ -906,7 +947,9 @@ fork_from=Atdalīt no
 already_forked=Repozitorijs %s jau ir atdalīts
 fork_to_different_account=Atdalīt uz citu kontu
 fork_visibility_helper=Atdalītam repozitorijam nav iespējams mainīt tā redzamību.
+fork_branch=Atzars, ko klonēt atdalītajā repozitorijā
 all_branches=Visi atzari
+fork_no_valid_owners=Šim repozitorijam nevar izveidot atdalītu repozitoriju, jo tam nav spēkā esošu īpašnieku.
 use_template=Izmantot šo sagatavi
 clone_in_vsc=Atvērt VS Code
 download_zip=Lejupielādēt ZIP
@@ -944,6 +987,8 @@ mirror_interval_invalid=Nekorekts spoguļošanas intervāls.
 mirror_sync_on_commit=Sinhronizēt, kad revīzijas tiek iesūtītas
 mirror_address=Spoguļa adrese
 mirror_address_desc=Pieslēgšanās rekvizītus norādiet autorizācijas sadaļā.
+mirror_address_url_invalid=Norādītais URL ir nederīgs. Visas URL daļas ir jānorāda pareizi.
+mirror_address_protocol_invalid=Norādītais URL ir nederīgs. Var spoguļot tikai no http(s):// vai git:// adresēm.
 mirror_lfs=Lielu failu glabātuve (LFS)
 mirror_lfs_desc=Aktivizēt LFS datu spoguļošanu.
 mirror_lfs_endpoint=LFS galapunkts
@@ -954,7 +999,7 @@ mirror_password_blank_placeholder=(nav uzstādīts)
 mirror_password_help=Nomainiet lietotāju, lai izdzēstu saglabāto paroli.
 watchers=Novērotāji
 stargazers=Zvaigžņdevēji
-stars_remove_warning=Tiks noņemtas visas atzīmētās zvaigznes šim repozitorijam.
+stars_remove_warning=Šis repozitorijs tiks noņemts no visām izlasēm.
 forks=Atdalītie repozitoriji
 reactions_more=un vēl %d
 unit_disabled=Administrators ir atspējojies šo repozitorija sadaļu.
@@ -969,14 +1014,20 @@ delete_preexisting=Dzēst jau eksistējošos failus
 delete_preexisting_content=Dzēst failus direktorijā %s
 delete_preexisting_success=Dzēst nepārņemtos failus direktorijā %s
 blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas
+blame.ignore_revs=Neņem vērā izmaiņas no <a href="%s">.git-blame-ignore-revs</a>. Nospiediet <a href="%s">šeit, lai to apietu</a> un redzētu visu izmaiņu skatu.
+blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no <a href="%s">.git-blam-ignore-revs</a>.
 author_search_tooltip=Tiks attēloti ne vairāk kā 30 lietotāji
 
 tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s
+tree_path_not_found_branch=Atzarā %[2]s nepastāv ceļš %[1]s
+tree_path_not_found_tag=Tagā %[2]s nepastāv ceļš %[1]s
 
 transfer.accept=Apstiprināt īpašnieka maiņu
 transfer.accept_desc=`Mainīt īpašnieku uz "%s"`
 transfer.reject=Noraidīt īpašnieka maiņu
 transfer.reject_desc=`Atcelt īpašnieka maiņu uz "%s"`
+transfer.no_permission_to_accept=Nav atļaujas pieņemt šo pārsūtīšanu.
+transfer.no_permission_to_reject=Nav atļaujas noraidīt šo pārsūtīšanu.
 
 desc.private=Privāts
 desc.public=Publisks
@@ -995,6 +1046,8 @@ template.issue_labels=Problēmu etiķetes
 template.one_item=Norādiet vismaz vienu sagataves vienību
 template.invalid=Norādiet sagataves repozitoriju
 
+archive.title=Šis repozitorijs ir arhivēts. Ir iespējams aplūkot tā failus un to konēt, bet nav iespējams iesūtīt izmaiņas, kā arī izveidot jaunas problēmas vai izmaiņu pieprasījumus.
+archive.title_date=Šis repozitorijs tika arhivēts %s. Ir iespējams aplūkot tā failus un to konēt, bet nav iespējams iesūtīt izmaiņas, kā arī izveidot jaunas problēmas vai izmaiņu pieprasījumus.
 archive.issue.nocomment=Repozitorijs ir arhivēts. Problēmām nevar pievienot jaunus komentārus.
 archive.pull.nocomment=Repozitorijs ir arhivēts. Izmaiņu pieprasījumiem nevar pievienot jaunus komentārus.
 
@@ -1011,6 +1064,7 @@ migrate_options_lfs=Migrēt LFS failus
 migrate_options_lfs_endpoint.label=LFS galapunkts
 migrate_options_lfs_endpoint.description=Migrācija mēģinās izmantot attālināto URL, lai <a target="_blank" rel="noopener noreferrer" href="%s">noteiktu LFS serveri</a>. Var norādīt arī citu galapunktu, ja repozitorija LFS dati ir izvietoti citā vietā.
 migrate_options_lfs_endpoint.description.local=Iespējams norādīt arī servera ceļu.
+migrate_options_lfs_endpoint.placeholder=Ja nav norādīts, galamērķis tiks atvasināts no klonēšanas URL
 migrate_items=Vienības, ko pārņemt
 migrate_items_wiki=Vikivietni
 migrate_items_milestones=Atskaites punktus
@@ -1061,11 +1115,11 @@ generated_from=ģenerēts no
 fork_from_self=Nav iespējams atdalīt repozitoriju, kuram esat īpašnieks.
 fork_guest_user=Piesakieties, lai atdalītu repozitoriju.
 watch_guest_user=Piesakieties, lai sekotu šim repozitorijam.
-star_guest_user=Piesakieties, lai atzīmētu šo repozitoriju ar zvaigznīti.
+star_guest_user=Piesakieties, lai pievienotu šo repozitoriju izlasei.
 unwatch=Nevērot
 watch=Vērot
 unstar=Noņemt zvaigznīti
-star=Pievienot zvaigznīti
+star=Pievienot izlasei
 fork=Atdalīts
 download_archive=Lejupielādēt repozitoriju
 more_operations=Vairāk darbību
@@ -1113,6 +1167,10 @@ file_view_rendered=Skatīt rezultātu
 file_view_raw=Rādīt neapstrādātu
 file_permalink=Patstāvīgā saite
 file_too_large=Šis fails ir par lielu, lai to parādītu.
+invisible_runes_header=`Šīs fails satur neredzamus unikoda simbolus`
+invisible_runes_description=`Šis fails satur neredzamus unikoda simbolus, kas ir neatšķirami cilvēkiem, bet dators tās var atstrādāt atšķirīgi. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.`
+ambiguous_runes_header=`Šis fails satur neviennozīmīgus unikoda simbolus`
+ambiguous_runes_description=`Šis fails satur unikoda simbolus, kas var tikt sajauktas ar citām rakstzīmēm. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.`
 invisible_runes_line=`Šī līnija satur neredzamus unikoda simbolus`
 ambiguous_runes_line=`Šī līnija satur neviennozīmīgus unikoda simbolus`
 ambiguous_character=`%[1]c [U+%04[1]X] var tikt sajaukts ar %[2]c [U+%04[2]X]`
@@ -1125,6 +1183,7 @@ video_not_supported_in_browser=Jūsu pārlūks neatbalsta HTML5 video.
 audio_not_supported_in_browser=Jūsu pārlūks neatbalsta HTML5 audio.
 stored_lfs=Saglabāts Git LFS
 symbolic_link=Simboliska saite
+executable_file=Izpildāmais fails
 commit_graph=Revīziju grafs
 commit_graph.select=Izvēlieties atzarus
 commit_graph.hide_pr_refs=Paslēpt izmaiņu pieprasījumus
@@ -1353,14 +1412,15 @@ issues.delete_branch_at=`izdzēsa atzaru <b>%s</b> %s`
 issues.filter_label=Etiķete
 issues.filter_label_exclude=`Izmantojiet <code>alt</code> + <code>peles klikšķis vai enter</code>, lai neiekļautu etiķeti`
 issues.filter_label_no_select=Visas etiķetes
+issues.filter_label_select_no_label=Nav etiķetes
 issues.filter_milestone=Atskaites punkts
 issues.filter_milestone_all=Visi atskaites punkti
 issues.filter_milestone_none=Nav atskaites punkta
 issues.filter_milestone_open=Atvērtie atskaites punkti
 issues.filter_milestone_closed=Aizvērtie atskaites punkti
-issues.filter_project=Projektus
+issues.filter_project=Projekts
 issues.filter_project_all=Visi projekti
-issues.filter_project_none=Nav projektu
+issues.filter_project_none=Nav projekta
 issues.filter_assignee=Atbildīgais
 issues.filter_assginee_no_select=Visi atbildīgie
 issues.filter_assginee_no_assignee=Nav atbildīgā
@@ -1386,6 +1446,7 @@ issues.filter_sort.moststars=Visvairāk atzīmētie
 issues.filter_sort.feweststars=Vismazāk atzīmētie
 issues.filter_sort.mostforks=Visvairāk atdalītie
 issues.filter_sort.fewestforks=Vismazāk atdalītie
+issues.keyword_search_unavailable=Meklēšana pēc atslēgvārda pašreiz nav pieejama. Lūgums sazināties ar vietnes administratoru.
 issues.action_open=Atvērt
 issues.action_close=Aizvērt
 issues.action_label=Etiķete
@@ -1406,6 +1467,7 @@ issues.next=Nākamā
 issues.open_title=Atvērta
 issues.closed_title=Slēgta
 issues.draft_title=Melnraksts
+issues.num_comments_1=%d komentārs
 issues.num_comments=%d komentāri
 issues.commented_at=`komentēja <a href="#%s">%s</a>`
 issues.delete_comment_confirm=Vai patiešām vēlaties dzēst šo komentāru?
@@ -1414,6 +1476,7 @@ issues.context.quote_reply=Atbildēt citējot
 issues.context.reference_issue=Atsaukties uz šo jaunā problēmā
 issues.context.edit=Labot
 issues.context.delete=Dzēst
+issues.no_content=Nav sniegts apraksts.
 issues.close=Slēgt problēmu
 issues.comment_pull_merged_at=saplidināta revīzija %[1]s atzarā %[2]s %[3]s
 issues.comment_manually_pull_merged_at=manuāli saplidināta revīzija %[1]s atzarā %[2]s %[3]s
@@ -1432,8 +1495,16 @@ issues.ref_closed_from=`<a href="%[3]s">aizvēra problēmu %[4]s</a> <a id="%[1]
 issues.ref_reopened_from=`<a href="%[3]s">atkārtoti atvēra problēmu %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`no %[1]s`
 issues.author=Autors
+issues.author_helper=Šis lietotājs ir autors.
 issues.role.owner=Īpašnieks
-issues.role.member=Biedri
+issues.role.owner_helper=Šis lietotājs ir šī repozitorija īpašnieks.
+issues.role.member=Dalībnieks
+issues.role.member_helper=Šis lietotājs ir organizācijas, kurai pieder šis repozitorijs, dalībnieks.
+issues.role.collaborator=Līdzstrādnieks
+issues.role.collaborator_helper=Šis lietotājs ir uzaicināts līdzdarboties repozitorijā.
+issues.role.first_time_contributor=Pirmreizējs līdzradītājs
+issues.role.first_time_contributor_helper=Šis ir pirmais šī lietotāja ieguldījums šājā repozitorijā.
+issues.role.contributor=Līdzradītājs
 issues.role.contributor_helper=Šis lietotājs repozitorijā ir iepriekš veicis labojumus.
 issues.re_request_review=Pieprasīt atkārtotu recenziju
 issues.is_stale=Šajā izmaiņu pieprasījumā ir notikušas izmaiņās, kopš veicāt tā recenziju
@@ -1449,6 +1520,9 @@ issues.label_title=Etiķetes nosaukums
 issues.label_description=Etiķetes apraksts
 issues.label_color=Etiķetes krāsa
 issues.label_exclusive=Ekskluzīvs
+issues.label_archive=Arhīvēt etiķeti
+issues.label_archived_filter=Rādīt arhivētās etiķetes
+issues.label_archive_tooltip=Arhivētās etiķetes pēc noklusējuma netiek iekļautas ieteikumos, kad meklē pēc nosaukuma.
 issues.label_exclusive_desc=Nosauciet etiķeti <code>grupa/nosaukums</code>, lai grupētu etiķētes un varētu norādīt tās kā ekskluzīvas ar citām <code>grupa/</code> etiķetēm.
 issues.label_exclusive_warning=Jebkura konfliktējoša ekskluzīvas grupas etiķete tiks noņemta, labojot pieteikumu vai izmaiņu pietikumu etiķetes.
 issues.label_count=%d etiķetes
@@ -1503,6 +1577,7 @@ issues.tracking_already_started=`Jau ir uzsākta laika uzskaite par <a href="%s"
 issues.stop_tracking=Apturēt taimeri
 issues.stop_tracking_history=` beidza strādāt %s`
 issues.cancel_tracking=Atmest
+issues.cancel_tracking_history=`atcēla laika uzskaiti %s`
 issues.add_time=Manuāli pievienot laiku
 issues.del_time=Dzēst šo laika žurnāla ierakstu
 issues.add_time_short=Pievienot laiku
@@ -1526,6 +1601,7 @@ issues.due_date_form=dd.mm.yyyy
 issues.due_date_form_add=Pievienot izpildes termiņu
 issues.due_date_form_edit=Labot
 issues.due_date_form_remove=Noņemt
+issues.due_date_not_writer=Ir nepieciešama rakstīšanas piekļuve šim repozitorijam, lai varētu mainīt problēmas plānoto izpildes datumu.
 issues.due_date_not_set=Izpildes termiņš nav uzstādīts.
 issues.due_date_added=pievienoja izpildes termiņu %s %s
 issues.due_date_modified=mainīja termiņa datumu no %[2]s uz %[1]s %[3]s
@@ -1581,6 +1657,9 @@ issues.review.pending.tooltip=Šis komentārs nav redzams citiem lietotājiem. L
 issues.review.review=Recenzija
 issues.review.reviewers=Recenzenti
 issues.review.outdated=Novecojis
+issues.review.outdated_description=Saturs ir mainījies kopš šī komentāra pievienošanas
+issues.review.option.show_outdated_comments=Rādīt novecojušus komentārus
+issues.review.option.hide_outdated_comments=Paslēpt novecojušus komentārus
 issues.review.show_outdated=Rādīt novecojušu
 issues.review.hide_outdated=Paslēpt novecojušu
 issues.review.show_resolved=Rādīt atrisināto
@@ -1621,9 +1700,11 @@ pulls.switch_head_and_base=Mainīt galvas un pamata atzarus
 pulls.filter_branch=Filtrēt atzarus
 pulls.no_results=Nekas netika atrasts.
 pulls.show_all_commits=Rādīt visas revīzijas
+pulls.show_changes_since_your_last_review=Rādīt izmaiņas kopš Tavas pēdējās recenzijas
 pulls.showing_only_single_commit=Rāda tikai revīzijas %[1]s izmaiņas
 pulls.showing_specified_commit_range=Rāda tikai izmaiņas starp %[1]s..%[2]s
 pulls.select_commit_hold_shift_for_range=Atlasīt revīziju. Jātur Shift + klikšķis, lai atlasītu vairākas
+pulls.review_only_possible_for_full_diff=Recenzēšana ir iespējama tikai tad, kad tiek apskatīts pilns salīdzinājums
 pulls.filter_changes_by_commit=Atlasīt pēc revīzijas
 pulls.nothing_to_compare=Nav ko salīdzināt, jo bāzes un salīdzināmie atzari ir vienādi.
 pulls.nothing_to_compare_and_allow_empty_pr=Šie atzari ir vienādi. Izveidotais izmaiņu pieprasījums būs tukšs.
@@ -1656,6 +1737,12 @@ pulls.is_empty=Mērķa atzars jau satur šī atzara izmaiņas. Šī revīzija b
 pulls.required_status_check_failed=Dažas no pārbaudēm nebija veiksmīgas.
 pulls.required_status_check_missing=Trūkst dažu obligāto pārbaužu.
 pulls.required_status_check_administrator=Kā administrators Jūs varat sapludināt šo izmaiņu pieprasījumu.
+pulls.blocked_by_approvals=Šim izmaiņu pieprasījumam vēl nav pietiekami daudz apstiprinājumu. Nodrošināti %d no %d apstiprinājumiem.
+pulls.blocked_by_rejection=Šim izmaiņu pieprasījumam oficiālais recenzents ir pieprasījis labojumus.
+pulls.blocked_by_official_review_requests=Šim izmaiņu pieprasījumam ir oficiāli recenzijas pieprasījumi.
+pulls.blocked_by_outdated_branch=Šis izmaiņu pieprasījums ir bloķēts, jo tas ir novecojis.
+pulls.blocked_by_changed_protected_files_1=Šis izmaiņu pieprasījums ir bloķēts, jo tas izmaina aizsargāto failu:
+pulls.blocked_by_changed_protected_files_n=Šis izmaiņu pieprasījums ir bloķēts, jo tas izmaina aizsargātos failus:
 pulls.can_auto_merge_desc=Šo izmaiņu pieprasījumu var automātiski sapludināt.
 pulls.cannot_auto_merge_desc=Šis izmaiņu pieprasījums nevar tikt automātiski sapludināts konfliktu dēļ.
 pulls.cannot_auto_merge_helper=Sapludiniet manuāli, lai atrisinātu konfliktus.
@@ -1690,6 +1777,7 @@ pulls.rebase_conflict_summary=Kļūdas paziņojums
 pulls.unrelated_histories=Sapludināšana neizdevās: mērķa un bāzes atzariem nav kopējas vēstures. Ieteikums: izvēlieties citu sapludināšanas stratēģiju
 pulls.merge_out_of_date=Sapludināšana neizdevās: sapludināšanas laikā, bāzes atzarā tika iesūtītas izmaiņas. Ieteikums: mēģiniet atkārtoti.
 pulls.head_out_of_date=Sapludināšana neizdevās: sapludināšanas laikā, bāzes atzarā tika iesūtītas izmaiņas. Ieteikums: mēģiniet atkārtoti.
+pulls.has_merged=Neizdevās: izmaiņu pieprasījums jau ir sapludināts, nevar to darīt atkārtoti vai mainīt mērķa atzaru.
 pulls.push_rejected=Sapludināšana neizdevās: iesūtīšana tika noraidīta. Pārbaudiet git āķus šim repozitorijam.
 pulls.push_rejected_summary=Pilns noraidīšanas ziņojums
 pulls.push_rejected_no_message=Sapludināšana neizdevās: Izmaiņu iesūtīšana tika noraidīta, bet serveris neatgrieza paziņojumu.<br>Pārbaudiet git āķus šim repozitorijam
@@ -1701,6 +1789,8 @@ pulls.status_checks_failure=Dažas pārbaudes neizdevās izpildīt
 pulls.status_checks_error=Dažu pārbaužu izpildes laikā, radās kļūdas
 pulls.status_checks_requested=Obligāts
 pulls.status_checks_details=Papildu informācija
+pulls.status_checks_hide_all=Paslēpt visas pārbaudes
+pulls.status_checks_show_all=Parādīt visas pārbaudes
 pulls.update_branch=Atjaunot atzaru, izmantojot, sapludināšanu
 pulls.update_branch_rebase=Atjaunot atzaru, izmantojot, pārbāzēšanu
 pulls.update_branch_success=Atzara atjaunināšana veiksmīgi pabeigta
@@ -1709,6 +1799,11 @@ pulls.outdated_with_base_branch=Atzars ir novecojis salīdzinot ar bāzes atzaru
 pulls.close=Aizvērt izmaiņu pieprasījumu
 pulls.closed_at=`aizvēra šo izmaiņu pieprasījumu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`atkārtoti atvēra šo izmaiņu pieprasījumu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_hint=`Apskatīt <a class="show-instruction">komandrindas izmantošanas norādes</a>.`
+pulls.cmd_instruction_checkout_title=Paņemt
+pulls.cmd_instruction_checkout_desc=Projekta repozitorijā jāizveido jauns atzars un jāpārbauda izmaiņas.
+pulls.cmd_instruction_merge_title=Sapludināt
+pulls.cmd_instruction_merge_desc=Sapludināt izmaiņas un atjaunot tās Gitea.
 pulls.clear_merge_message=Notīrīt sapludināšanas ziņojumu
 pulls.clear_merge_message_hint=Notīrot sapludināšanas ziņojumu tiks noņemts tikai pats ziņojums, bet tiks paturēti ģenerētie git ziņojumu, kā "Co-Authored-By …".
 
@@ -1727,6 +1822,7 @@ pulls.auto_merge_canceled_schedule_comment=`atcēla automātisko sapludināšanu
 pulls.delete.title=Dzēst šo izmaiņu pieprasījumu?
 pulls.delete.text=Vai patiešām vēlaties dzēst šo izmaiņu pieprasījumu? (Neatgriezeniski tiks izdzēsts viss saturs. Apsveriet iespēju to aizvērt, ja vēlaties informāciju saglabāt vēsturei)
 
+pulls.recently_pushed_new_branches=Tu iesūtīji izmaiņas atzarā <strong>%[1]s</strong> %[2]s
 
 pull.deleted_branch=(izdzēsts):%s
 
@@ -1736,6 +1832,7 @@ milestones.update_ago=Atjaunots %s
 milestones.no_due_date=Bez termiņa
 milestones.open=Atvērta
 milestones.close=Aizvērt
+milestones.new_subheader=Atskaites punkti var palīdzēt pārvaldīt problēmas un sekot to virzībai.
 milestones.completeness=%d%% pabeigti
 milestones.create=Izveidot atskaites punktu
 milestones.title=Virsraksts
@@ -1752,6 +1849,8 @@ milestones.edit_success=Izmaiņas atskaites punktā "%s" tika veiksmīgi saglab
 milestones.deletion=Dzēst atskaites punktu
 milestones.deletion_desc=Dzēšot šo atskaites punktu, tas tiks noņemts no visām saistītajām problēmām un izmaiņu pieprasījumiem. Vai turpināt?
 milestones.deletion_success=Atskaites punkts tika veiksmīgi izdzēsts.
+milestones.filter_sort.earliest_due_data=Agrākais izpildes laiks
+milestones.filter_sort.latest_due_date=Vēlākais izpildes laiks
 milestones.filter_sort.least_complete=Vismazāk pabeigtais
 milestones.filter_sort.most_complete=Visvairāk pabeigtais
 milestones.filter_sort.most_issues=Visvairāk problēmu
@@ -1768,6 +1867,7 @@ signing.wont_sign.parentsigned=Revīzija netiks parakstīta, jo nav parakstīta
 signing.wont_sign.basesigned=Sapludināšanas revīzija netiks parakstīta, jo pamata revīzija nav parakstīta.
 signing.wont_sign.headsigned=Sapludināšanas revīzija netiks parakstīta, jo galvenā revīzija nav parakstīta.
 signing.wont_sign.commitssigned=Sapludināšana netiks parakstīta, jo visas saistītās revīzijas nav parakstītas.
+signing.wont_sign.approved=Sapludināšana netiks parakstīta, jo izmaiņu pieprasījums nav apstiprināts.
 signing.wont_sign.not_signed_in=Jūs neesat pieteicies.
 
 ext_wiki=Piekļuve ārējai vikivietnei
@@ -1898,6 +1998,7 @@ settings.mirror_settings.docs.disabled_push_mirror.info=Iesūtīšanas spoguļus
 settings.mirror_settings.docs.no_new_mirrors=Šis repozitorijs spoguļo izmaiņas uz vai no cita repozitorija. Pašlaik vairāk nav iespējams izveidot jaunus spoguļa repozitorijus.
 settings.mirror_settings.docs.can_still_use=Lai arī nav iespējams mainīt esošos vai izveidot jaunus spoguļa repozitorijus, esošie turpinās strādāt.
 settings.mirror_settings.docs.pull_mirror_instructions=Lai ietatītu atvilkšanas spoguli, sekojiet instrukcijām:
+settings.mirror_settings.docs.more_information_if_disabled=Vairāk par piegādāšanas un saņemšanas spoguļiem var uzzināt šeit:
 settings.mirror_settings.docs.doc_link_title=Kā spoguļot repozitorijus?
 settings.mirror_settings.docs.doc_link_pull_section=dokumentācijas nodaļā "Pulling from a remote repository".
 settings.mirror_settings.docs.pulling_remote_title=Atvilkt no attāla repozitorija
@@ -1909,8 +2010,11 @@ settings.mirror_settings.last_update=Pēdējās izmaiņas
 settings.mirror_settings.push_mirror.none=Nav konfigurēts iesūtīšanas spogulis
 settings.mirror_settings.push_mirror.remote_url=Git attālinātā repozitorija URL
 settings.mirror_settings.push_mirror.add=Pievienot iesūtīšanas spoguli
+settings.mirror_settings.push_mirror.edit_sync_time=Labot spoguļa sinhronizācijas intervālu
 
 settings.sync_mirror=Sinhronizēt tagad
+settings.pull_mirror_sync_in_progress=Pašlaik tiek saņemtas izmaiņas no attālā %s.
+settings.push_mirror_sync_in_progress=Pašlaik tiek piegādātas izmaiņas uz attālo %s.
 settings.site=Mājas lapa
 settings.update_settings=Mainīt iestatījumus
 settings.update_mirror_settings=Atjaunot spoguļa iestatījumus
@@ -1977,6 +2081,7 @@ settings.transfer.rejected=Repozitorija īpašnieka maiņas pieprasījums tika n
 settings.transfer.success=Repozitorija īpašnieka maiņa veiksmīga.
 settings.transfer_abort=Atcelt īpašnieka maiņu
 settings.transfer_abort_invalid=Nevar atcelt neeksistējoša repozitorija īpašnieka maiņu.
+settings.transfer_abort_success=Repozitorija īpašnieka maiņa uz %s tika veiksmīgi atcelta.
 settings.transfer_desc=Mainīt šī repozitorija īpašnieku uz citu lietotāju vai organizāciju, kurai Jums ir administratora tiesības.
 settings.transfer_form_title=Ievadiet repozitorija nosaukumu, lai apstiprinātu:
 settings.transfer_in_progress=Pašlaik jau tiek veikta repozitorija īpašnieka maiņa. Atceliet iepriekšējo īpašnieka maiņu, ja vēlaties mainīt uz citu.
@@ -2001,12 +2106,12 @@ settings.trust_model.collaboratorcommitter=Līdzstrādnieka un revīzijas iesūt
 settings.trust_model.collaboratorcommitter.long=Līdzstrādnieka un revīzijas iesūtītāja: Uzticēties līdzstrādnieku parakstiem, kas atbilst revīzijas iesūtītājam
 settings.trust_model.collaboratorcommitter.desc=Derīgi līdzstrādnieku paraksti tiks atzīmēti kā "uzticami", ja tie atbilst revīzijas iesūtītājam, citos gadījumos tie tiks atzīmēti kā "neuzticami", ja paraksts atbilst revīzijas iesūtītajam, vai "nesakrītoši", ja neatbilst. Šis nozīmē, ka Gitea būs kā revīzijas iesūtītājs parakstītām revīzijām, kur īstais revīzijas iesūtītājs tiks atīzmēts revīzijas komentāra beigās ar tekstu Co-Authored-By: un Co-Committed-By:. Noklusētajai Gitea atslēgai ir jāatbilst lietotājam datubāzē.
 settings.wiki_delete=Dzēst vikivietnes datus
-settings.wiki_delete_desc=Vikivietnes repozitorija dzēšana ir <strong>NEATGRIEZENISKA</strong>. Vai turpināt?
+settings.wiki_delete_desc=Vikivietnes repozitorija dzēšana ir neatgriezeniska un nav atsaucama.
 settings.wiki_delete_notices_1=- Šī darbība dzēsīs un atspējos repozitorija %s vikivietni.
 settings.confirm_wiki_delete=Dzēst vikivietnes datus
 settings.wiki_deletion_success=Repozitorija vikivietnes dati tika izdzēsti.
 settings.delete=Dzēst šo repozitoriju
-settings.delete_desc=Repozitorija dzēšana ir <strong>NEATGRIEZENISKA</strong>. Vai turpināt?
+settings.delete_desc=Repozitorija dzēšana ir neatgriezeniska un nav atsaucama.
 settings.delete_notices_1=- Šī darbība ir <strong>NEATGRIEZENISKA</strong>.
 settings.delete_notices_2=- Šī darbība neatgriezeniski izdzēsīs visu repozitorijā <strong>%s</strong>, tai skaitā problēmas, komentārus, vikivietni un līdzstrādnieku piesaisti.
 settings.delete_notices_fork_1=- Visi atdalītie repozitoriju pēc dzēšanas kļūs neatkarīgi.
@@ -2043,12 +2148,14 @@ settings.webhook_deletion_desc=Noņemot tīmekļa āķi, tiks dzēsti visi tā i
 settings.webhook_deletion_success=Tīmekļa āķis tika noņemts.
 settings.webhook.test_delivery=Testa piegāde
 settings.webhook.test_delivery_desc=Veikt viltus push-notikuma piegādi, lai notestētu Jūsu tīmekļa āķa iestatījumus.
+settings.webhook.test_delivery_desc_disabled=Lai pārbaudītu šo tīmekļa āķi ar neīstu notikumu, tas ir jāiespējo.
 settings.webhook.request=Pieprasījums
 settings.webhook.response=Atbilde
 settings.webhook.headers=Galvenes
 settings.webhook.payload=Saturs
 settings.webhook.body=Saturs
 settings.webhook.replay.description=Izpildīt atkārtoti šo tīmekļa āķi.
+settings.webhook.replay.description_disabled=Lai atkārtoti izpildītu šo tīmekļa āķi, tas ir jāiespējo.
 settings.webhook.delivery.success=Notikums tika veiksmīgi pievienots piegādes rindai. Var paiet vairākas sekundes līdz tas parādās piegādes vēsturē.
 settings.githooks_desc=Git āķus apstrādā pats Git. Jūs varat labot atbalstīto āku failus sarakstā zemāk, lai veiktu pielāgotas darbības.
 settings.githook_edit_desc=Ja āķis nav aktīvs, tiks attēlots piemērs kā to izmantot. Atstājot āķa saturu tukšu, tas tiks atspējots.
@@ -2248,16 +2355,23 @@ settings.tags.protection.none=Nav uzstādīta tagu aizsargāšana.
 settings.tags.protection.pattern.description=Var izmantot vienkāršu nosaukumu vai glob šablonu, vai regulāro izteiksmi, lai atbilstu vairākiem tagiem. Vairāk ir lasāms <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">aizsargāto tagu šablonu dokumentācijā</a>.
 settings.bot_token=Bota pilnvara
 settings.chat_id=Tērzēšanas ID
+settings.thread_id=Pavediena ID
 settings.matrix.homeserver_url=Mājas servera URL
 settings.matrix.room_id=Istabas ID
 settings.matrix.message_type=Ziņas veids
 settings.archive.button=Arhivēt
 settings.archive.header=Arhivēt repozitoriju
+settings.archive.text=Repozitorija arhivēšana padarīs to tikai lasāmu. Tas nebūs redzams infopanelī. Neviens nevarēs izveidot jaunas revīzijas vai atvērt jaunus problēmu pieteikumus vai izmaiņu pieprasījumus.
 settings.archive.success=Repozitorijs veiksmīgi arhivēts.
 settings.archive.error=Arhivējot repozitoriju radās neparedzēta kļūda. Pārbaudiet kļūdu žurnālu, lai uzzinātu sīkāk.
 settings.archive.error_ismirror=Nav iespējams arhivēt spoguļotus repozitorijus.
 settings.archive.branchsettings_unavailable=Atzaru iestatījumi nav pieejami, ja repozitorijs ir arhivēts.
 settings.archive.tagsettings_unavailable=Tagu iestatījumi nav pieejami, ja repozitorijs ir arhivēts.
+settings.unarchive.button=Atcelt repozitorija arhivēšanu
+settings.unarchive.header=Atcelt šī repozitorija arhivēšanu
+settings.unarchive.text=Repozitorija arhivēšanas atcelšana atjaunos tā spēju saņemt izmaiņas, kā arī jaunus problēmu pieteikumus un izmaiņu pieprasījumus.
+settings.unarchive.success=Repozitorijam veiksmīgi atcelta arhivācija.
+settings.unarchive.error=Repozitorija arhivēšanas atcelšanas laikā atgadījās kļūda. Vairāk ir redzams žurnālā.
 settings.update_avatar_success=Repozitorija attēls tika atjaunināts.
 settings.lfs=LFS
 settings.lfs_filelist=LFS faili, kas saglabāti šajā repozitorijā
@@ -2324,6 +2438,7 @@ diff.show_more=Rādīt vairāk
 diff.load=Ielādēt izmaiņas
 diff.generated=ģenerēts
 diff.vendored=ārējs
+diff.comment.add_line_comment=Pievienot rindas komentāru
 diff.comment.placeholder=Ievadiet komentāru
 diff.comment.markdown_info=Tiek atbalstīta formatēšana ar Markdown.
 diff.comment.add_single_comment=Pievienot vienu komentāru
@@ -2380,6 +2495,7 @@ release.edit_release=Labot laidienu
 release.delete_release=Dzēst laidienu
 release.delete_tag=Dzēst tagu
 release.deletion=Dzēst laidienu
+release.deletion_desc=Laidiena izdzēšana tikai noņem to no Gitea. Tā neietekmēs Git tagu, repozitorija saturu vai vēsturi. Vai turpināt?
 release.deletion_success=Laidiens tika izdzēsts.
 release.deletion_tag_desc=Tiks izdzēsts tags no repozitorija. Repozitorija saturs un vēsture netiks mainīta. Vai turpināt?
 release.deletion_tag_success=Tags tika izdzēsts.
@@ -2399,6 +2515,7 @@ branch.already_exists=Atzars ar nosaukumu "%s" jau eksistē.
 branch.delete_head=Dzēst
 branch.delete=`Dzēst atzaru "%s"`
 branch.delete_html=Dzēst atzaru
+branch.delete_desc=Atzara dzēšana ir neatgriezeniska. Kaut arī izdzēstais zars neilgu laiku var turpināt pastāvēt, pirms tas tiešām tiek noņemts, to vairumā gadījumu NEVAR atsaukt. Vai turpināt?
 branch.deletion_success=Atzars "%s" tika izdzēsts.
 branch.deletion_failed=Neizdevās izdzēst atzaru "%s".
 branch.delete_branch_has_new_commits=Atzars "%s" nevar tik dzēsts, jo pēc sapludināšanas, tam ir pievienotas jaunas revīzijas.
@@ -2439,6 +2556,7 @@ tag.create_success=Tags "%s" tika izveidots.
 topic.manage_topics=Pārvaldīt tēmas
 topic.done=Gatavs
 topic.count_prompt=Nevar pievienot vairāk kā 25 tēmas
+topic.format_prompt=Tēmai jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un punktus ('.') un var būt līdz 35 rakstzīmēm gara. Burtiem jābūt mazajiem.
 
 find_file.go_to_file=Iet uz failu
 find_file.no_matching=Atbilstošs fails netika atrasts
@@ -2477,6 +2595,7 @@ form.create_org_not_allowed=Jums nav tiesību veidot jauno organizāciju.
 settings=Iestatījumi
 settings.options=Organizācija
 settings.full_name=Pilns vārds, uzvārds
+settings.email=E-pasta adrese saziņai
 settings.website=Mājas lapa
 settings.location=Atrašanās vieta
 settings.permission=Tiesības
@@ -2490,6 +2609,7 @@ settings.visibility.private_shortname=Privāta
 
 settings.update_settings=Mainīt iestatījumus
 settings.update_setting_success=Organizācijas iestatījumi tika saglabāti.
+settings.change_orgname_prompt=Piezīme: organizācijas nosaukuma maiņa izmainīs arī organizācijas URL un atbrīvos veco nosaukumu.
 settings.change_orgname_redirect_prompt=Vecais vārds pārsūtīs uz jauno, kamēr vien tas nebūs izmantots.
 settings.update_avatar_success=Organizācijas attēls tika saglabāts.
 settings.delete=Dzēst organizāciju
@@ -2509,7 +2629,7 @@ members.private=Slēpts
 members.private_helper=padarīt redzemu
 members.member_role=Dalībnieka loma:
 members.owner=Īpašnieks
-members.member=Biedri
+members.member=Dalībnieks
 members.remove=Noņemt
 members.remove.detail=Noņemt lietotāju %[1]s no organizācijas %[2]s?
 members.leave=Atstāt
@@ -2565,15 +2685,19 @@ teams.all_repositories_helper=Šai komandai ir piekļuve visiem repozitorijiem.
 teams.all_repositories_read_permission_desc=Šī komanda piešķirt <strong>skatīšanās</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties un klonēt visus organizācijas repozitorijus.
 teams.all_repositories_write_permission_desc=Šī komanda piešķirt <strong>labošanas</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties un nosūtīt izmaiņas visiem organizācijas repozitorijiem.
 teams.all_repositories_admin_permission_desc=Šī komanda piešķirt <strong>administratora</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties, nosūtīt izmaiņas un mainīt iestatījumus visiem organizācijas repozitorijiem.
+teams.invite.title=Tu esi uzaicināts pievienoties organizācijas <strong>%[2]s</strong> komandai <strong>%[1]s</strong>.
 teams.invite.by=Uzaicināja %s
 teams.invite.description=Nospiediet pogu zemāk, lai pievienotos komandai.
 
 [admin]
 dashboard=Infopanelis
+identity_access=Identitāte un piekļuve
 users=Lietotāju konti
 organizations=Organizācijas
+assets=Koda aktīvi
 repositories=Repozitoriji
 hooks=Tīmekļa āķi
+integrations=Integrācijas
 authentication=Autentificēšanas avoti
 emails=Lietotāja e-pasts
 config=Konfigurācija
@@ -2582,6 +2706,7 @@ monitor=Uzraudzība
 first_page=Pirmā
 last_page=Pēdējā
 total=Kopā: %d
+settings=Administratora iestatījumi
 
 dashboard.new_version_hint=Ir pieejama Gitea versija %s, pašreizējā versija %s. Papildus informācija par jauno versiju ir pieejama <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">mājas lapā</a>.
 dashboard.statistic=Kopsavilkums
@@ -2594,11 +2719,13 @@ dashboard.clean_unbind_oauth=Notīrīt nepiesaistītos OAuth savienojumus
 dashboard.clean_unbind_oauth_success=Visi nepiesaistītie OAuth savienojumu tika izdzēsti.
 dashboard.task.started=Uzsākts uzdevums: %[1]s
 dashboard.task.process=Uzdevums: %[1]s
+dashboard.task.cancelled=Uzdevums: %[1]s atcelts: %[3]s
 dashboard.task.error=Kļūda uzdevuma izpildē: %[1]s: %[3]s
 dashboard.task.finished=Uzdevums: %[1]s, ko iniciēja %[2]s ir izpildīts
 dashboard.task.unknown=Nezināms uzdevums: %[1]s
 dashboard.cron.started=Uzsākts Cron: %[1]s
 dashboard.cron.process=Cron: %[1]s
+dashboard.cron.cancelled=Cron: %[1]s atcelts: %[3]s
 dashboard.cron.error=Kļūda Cron: %s: %[3]s
 dashboard.cron.finished=Cron: %[1]s pabeigts
 dashboard.delete_inactive_accounts=Dzēst visus neaktivizētos kontus
@@ -2608,6 +2735,7 @@ dashboard.delete_repo_archives.started=Uzdevums visu repozitoriju arhīvu dzēš
 dashboard.delete_missing_repos=Dzēst visus repozitorijus, kam trūkst Git failu
 dashboard.delete_missing_repos.started=Uzdevums visu repozitoriju dzēšanai, kam trūkst git failu, uzsākts.
 dashboard.delete_generated_repository_avatars=Dzēst ģenerētos repozitoriju attēlus
+dashboard.sync_repo_branches=Sinhronizācija ar dabubāzi izlaida atzarus no git datiem
 dashboard.update_mirrors=Atjaunot spoguļus
 dashboard.repo_health_check=Pārbaudīt visu repozitoriju veselību
 dashboard.check_repo_stats=Pārbaudīt visu repozitoriju statistiku
@@ -2622,6 +2750,7 @@ dashboard.reinit_missing_repos=Atkārtoti inicializēt visus pazaudētos Git rep
 dashboard.sync_external_users=Sinhronizēt ārējo lietotāju datus
 dashboard.cleanup_hook_task_table=Iztīrīt tīmekļa āķu vēsturi
 dashboard.cleanup_packages=Notīrīt novecojušās pakotnes
+dashboard.cleanup_actions=Notīrīt darbību izbeigušos žurnālus un artefaktus
 dashboard.server_uptime=Servera darbības laiks
 dashboard.current_goroutine=Izmantotās Gorutīnas
 dashboard.current_memory_usage=Pašreiz izmantotā atmiņa
@@ -2659,7 +2788,9 @@ dashboard.gc_lfs=Veikt atkritumu uzkopšanas darbus LFS meta objektiem
 dashboard.stop_zombie_tasks=Apturēt zombija uzdevumus
 dashboard.stop_endless_tasks=Apturēt nepārtrauktus uzdevumus
 dashboard.cancel_abandoned_jobs=Atcelt pamestus darbus
+dashboard.start_schedule_tasks=Sākt plānotos uzdevumus
 dashboard.sync_branch.started=Sākta atzaru sinhronizācija
+dashboard.rebuild_issue_indexer=Pārbūvēt problēmu indeksu
 
 users.user_manage_panel=Lietotāju kontu pārvaldība
 users.new_account=Izveidot lietotāja kontu
@@ -2668,6 +2799,9 @@ users.full_name=Vārds, uzvārds
 users.activated=Aktivizēts
 users.admin=Administrators
 users.restricted=Ierobežots
+users.reserved=Aizņemts
+users.bot=Bots
+users.remote=Attāls
 users.2fa=2FA
 users.repos=Repozitoriji
 users.created=Izveidots
@@ -2714,6 +2848,7 @@ users.list_status_filter.is_prohibit_login=Nav atļauta autorizēšanās
 users.list_status_filter.not_prohibit_login=Atļaut autorizāciju
 users.list_status_filter.is_2fa_enabled=2FA iespējots
 users.list_status_filter.not_2fa_enabled=2FA nav iespējots
+users.details=Lietotāja informācija
 
 emails.email_manage_panel=Lietotāju e-pastu pārvaldība
 emails.primary=Primārais
@@ -2726,6 +2861,7 @@ emails.updated=E-pasts atjaunots
 emails.not_updated=Neizdevās atjaunot pieprasīto e-pasta adresi: %v
 emails.duplicate_active=E-pasta adrese jau ir aktīva citam lietotājam.
 emails.change_email_header=Atjaunot e-pasta rekvizītus
+emails.change_email_text=Vai patiešām vēlaties atjaunot šo e-pasta adresi?
 
 orgs.org_manage_panel=Organizāciju pārvaldība
 orgs.name=Nosaukums
@@ -2740,14 +2876,17 @@ repos.owner=Īpašnieks
 repos.name=Nosaukums
 repos.private=Privāts
 repos.watches=Vērošana
-repos.stars=Atzīmētās zvaigznītes
+repos.stars=Zvaigznes
 repos.forks=Atdalītie
 repos.issues=Problēmas
 repos.size=Izmērs
+repos.lfs_size=LFS izmērs
 
 packages.package_manage_panel=Pakotņu pārvaldība
 packages.total_size=Kopējais izmērs: %s
 packages.unreferenced_size=Izmērs bez atsauces: %s
+packages.cleanup=Notīrīt novecojušos datus
+packages.cleanup.success=Novecojuši dati veiksmīgi notīrīti
 packages.owner=Īpašnieks
 packages.creator=Izveidotājs
 packages.name=Nosaukums
@@ -2758,10 +2897,12 @@ packages.size=Izmērs
 packages.published=Publicēts
 
 defaulthooks=Noklusētie tīmekļa āķi
+defaulthooks.desc=Tīmekļa āķi automātiski nosūta HTTP POST pieprasījumus serverim, kad iestājas noteikti Gitea notikumi. Šeit pievienotie tīmekļa āķi ir noklusējuma, un tie tiks pievienoti visiem jaunajiem repozitorijiem. Vairāk ir lasāms <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">tīmekļa āķu dokumentācijā</a>.
 defaulthooks.add_webhook=Pievienot noklusēto tīmekļa āķi
 defaulthooks.update_webhook=Mainīt noklusēto tīmekļa āķi
 
 systemhooks=Sistēmas tīmekļa āķi
+systemhooks.desc=Tīmekļa āķi automātiski nosūta HTTP POST pieprasījumus serverim, kad iestājas noteikti Gitea notikumi. Šeit pievienotie tīmekļa āķi tiks izsaukti visiem sistēmas repozitorijiem, tādēļ lūgums apsvērt to iespējamo ietekmi uz veiktspēju. Vairāk ir lasāms <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">tīmekļa āķu dokumentācijā</a>.
 systemhooks.add_webhook=Pievienot sistēmas tīmekļa āķi
 systemhooks.update_webhook=Mainīt sistēmas tīmekļa āķi
 
@@ -2854,6 +2995,7 @@ auths.sspi_default_language=Noklusētā lietotāja valoda
 auths.sspi_default_language_helper=Noklusētā valoda, ko uzstādīt automātiski izveidotajiem lietotājiem, kas izmanto SSPI autentifikācijas veidu. Atstājiet tukšu, ja vēlaties, lai valoda tiktu noteikta automātiski.
 auths.tips=Padomi
 auths.tips.oauth2.general=OAuth2 autentifikācija
+auths.tips.oauth2.general.tip=Kad tiek reģistrēta jauna OAuth2 autentifikācija, atzvanīšanas/pārvirzīšanas URL vajadzētu būt:
 auths.tip.oauth2_provider=OAuth2 pakalpojuma sniedzējs
 auths.tip.bitbucket=Reģistrējiet jaunu OAuth klientu adresē https://bitbucket.org/account/user/<jūsu lietotājvārds>/oauth-consumers/new un piešķiriet tam "Account" - "Read" tiesības
 auths.tip.nextcloud=`Reģistrējiet jaunu OAuth klientu jūsu instances sadāļā "Settings -> Security -> OAuth 2.0 client"`
@@ -2865,6 +3007,7 @@ auths.tip.google_plus=Iegūstiet OAuth2 klienta pilnvaru no Google API konsoles
 auths.tip.openid_connect=Izmantojiet OpenID pieslēgšanās atklāšanas URL (<serveris>/.well-known/openid-configuration), lai norādītu galapunktus
 auths.tip.twitter=Dodieties uz adresi https://dev.twitter.com/apps, izveidojiet lietotni un pārliecinieties, ka ir atzīmēts “Allow this application to be used to Sign in with Twitter”
 auths.tip.discord=Reģistrējiet jaunu aplikāciju adresē https://discordapp.com/developers/applications/me
+auths.tip.gitea=Pievienot jaunu OAuth2 lietojumprogrammu. Dokumentācija ir pieejama https://docs.gitea.com/development/oauth2-provider
 auths.tip.yandex=`Izveidojiet jaunu lietotni adresē https://oauth.yandex.com/client/new. Izvēlieties sekojošas tiesības "Yandex.Passport API" sadaļā: "Access to email address", "Access to user avatar" un "Access to username, first name and surname, gender"`
 auths.tip.mastodon=Norādiet pielāgotu mastodon instances URL, ar kuru vēlaties autorizēties (vai izmantojiet noklusēto)
 auths.edit=Labot autentifikācijas avotu
@@ -2894,6 +3037,7 @@ config.disable_router_log=Atspējot maršrutētāja žurnalizēšanu
 config.run_user=Izpildes lietotājs
 config.run_mode=Izpildes režīms
 config.git_version=Git versija
+config.app_data_path=Lietotnes datu ceļš
 config.repo_root_path=Repozitoriju glabāšanas vieta
 config.lfs_root_path=LFS saknes ceļš
 config.log_file_root_path=Žurnalizēšanas ceļš
@@ -3043,8 +3187,10 @@ monitor.queue.name=Nosaukums
 monitor.queue.type=Veids
 monitor.queue.exemplar=Eksemplāra veids
 monitor.queue.numberworkers=Strādņu skaits
+monitor.queue.activeworkers=Darbojošies strādņi
 monitor.queue.maxnumberworkers=Maksimālais strādņu skaits
 monitor.queue.numberinqueue=Skaits rindā
+monitor.queue.review_add=Pārskatīt/pievienot strādņus
 monitor.queue.settings.title=Pūla iestatījumi
 monitor.queue.settings.desc=Pūls dinamiski tiek palielināts atkarībā no bloķētiem darbiem rindā.
 monitor.queue.settings.maxnumberworkers=Maksimālais strādņu skaits
@@ -3100,7 +3246,7 @@ publish_release=`izveidoja versiju <a href="%[2]s"> "%[4]s" </a> repozitorijā <
 review_dismissed=`noraidīja lietotāja <b>%[4]s</b> recenziju izmaiņu pieprasījumam <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Iemesls:
 create_branch=izveidoja atzaru <a href="%[2]s">%[3]s</a> repozitorijā <a href="%[1]s">%[4]s</a>
-starred_repo=atzīmēja ar zvaigznīti <a href="%[1]s">%[2]s</a>
+starred_repo=pievienoja izlasē <a href="%[1]s">%[2]s</a>
 watched_repo=sāka sekot <a href="%[1]s">%[2]s</a>
 
 [tool]
@@ -3165,6 +3311,7 @@ desc=Pārvaldīt repozitorija pakotnes.
 empty=Pašlaik šeit nav nevienas pakotnes.
 empty.documentation=Papildus informācija par pakotņu reģistru pieejama <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>.
 empty.repo=Neparādās augšupielādēta pakotne? Apmeklējiet <a href="%[1]s">pakotņu iestatījumus</a>, lai sasaistītu ar repozitoriju.
+registry.documentation=Vairāk informācija par %s reģistru ir pieejama <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>.
 filter.type=Veids
 filter.type.all=Visas
 filter.no_result=Pēc norādītajiem kritērijiem nekas netika atrasts.
@@ -3250,6 +3397,8 @@ pub.install=Lai instalētu Dart pakotni, izpildiet sekojošu komandu:
 pypi.requires=Nepieciešams Python
 pypi.install=Lai instalētu pip pakotni, izpildiet sekojošu komandu:
 rpm.registry=Konfigurējiet šo reģistru no komandrindas:
+rpm.distros.redhat=uz RedHat balstītās operētājsistēmās
+rpm.distros.suse=uz SUSE balstītās operētājsistēmās
 rpm.install=Lai uzstādītu pakotni, ir jāizpilda šī komanda:
 rubygems.install=Lai instalētu gem pakotni, izpildiet sekojošu komandu:
 rubygems.install2=vai pievienojiet Gemfile:
@@ -3274,14 +3423,17 @@ settings.delete.success=Pakotne tika izdzēsta.
 settings.delete.error=Neizdevās izdzēst pakotni.
 owner.settings.cargo.title=Cargo reģistra inkdess
 owner.settings.cargo.initialize=Inicializēt indeksu
+owner.settings.cargo.initialize.description=Ir nepieciešams īpašs indeksa Git repozitorijs, lai izmantotu Cargo reģistru. Šīs iespējas izmantošana (atkārtoti) izveidos repozitoriju un automātiski to iestatīs.
 owner.settings.cargo.initialize.error=Neizdevās inicializēt Cargo indeksu: %v
 owner.settings.cargo.initialize.success=Cargo indekss tika veiksmīgi inicializēts.
 owner.settings.cargo.rebuild=Pārbūvēt indeksu
+owner.settings.cargo.rebuild.description=Pārbūvēšana var būt noderīga, ja indekss nav sinhronizēts ar saglabātajām Cargo pakotnēm.
 owner.settings.cargo.rebuild.error=Neizdevās pārbūvēt Cargo indeksu: %v
 owner.settings.cargo.rebuild.success=Cargo indekss tika veiksmīgi pārbūvēts.
 owner.settings.cleanuprules.title=Pārvaldīt notīrīšanas noteikumus
 owner.settings.cleanuprules.add=Pievienot notīrīšanas noteikumu
 owner.settings.cleanuprules.edit=Labot notīrīšanas noteikumu
+owner.settings.cleanuprules.none=Nav pievienoti tīrīšanas noteikumi. Sīkāku informāciju iespējams iegūt dokumentācijā.
 owner.settings.cleanuprules.preview=Notīrīšānas noteikuma priekšskatījums
 owner.settings.cleanuprules.preview.overview=Ir ieplānota %d paku dzēšana.
 owner.settings.cleanuprules.preview.none=Notīrīšanas noteikumam neatbilst neviena pakotne.
@@ -3300,6 +3452,7 @@ owner.settings.cleanuprules.success.update=Notīrīšanas noteikumi tika atjauno
 owner.settings.cleanuprules.success.delete=Notīrīšanas noteikumi tika izdzēsti.
 owner.settings.chef.title=Chef reģistrs
 owner.settings.chef.keypair=Ģenerēt atslēgu pāri
+owner.settings.chef.keypair.description=Atslēgu pāris ir nepieciešams, lai autentificētos Chef reģistrā. Ja iepriekš ir izveidots atslēgu pāris, jauna pāra izveidošana veco atslēgu pāri padarīs nederīgu.
 
 [secrets]
 secrets=Noslēpumi
@@ -3326,6 +3479,7 @@ status.waiting=Gaida
 status.running=Izpildās
 status.success=Pabeigts
 status.failure=Neveiksmīgs
+status.cancelled=Atcelts
 status.skipped=Izlaists
 status.blocked=Bloķēts
 
@@ -3338,11 +3492,12 @@ runners.id=ID
 runners.name=Nosaukums
 runners.owner_type=Veids
 runners.description=Apraksts
-runners.labels=Etiķetes
+runners.labels=Iezīmes
 runners.last_online=Pēdējo reizi tiešsaistē
 runners.runner_title=Izpildītājs
 runners.task_list=Pēdējās darbības, kas izpildītas
-runners.task_list.run=Palaist
+runners.task_list.no_tasks=Vēl nav uzdevumu.
+runners.task_list.run=Izpildīt
 runners.task_list.status=Statuss
 runners.task_list.repository=Repozitorijs
 runners.task_list.commit=Revīzija
@@ -3362,18 +3517,49 @@ runners.status.idle=Dīkstāvē
 runners.status.active=Aktīvs
 runners.status.offline=Bezsaistē
 runners.version=Versija
+runners.reset_registration_token=Atiestatīt reģistrācijas pilnvaru
 runners.reset_registration_token_success=Izpildītāja reģistrācijas pilnvara tika veiksmīgi atiestatīta
 
 runs.all_workflows=Visas darbaplūsmas
 runs.commit=Revīzija
+runs.scheduled=Ieplānots
+runs.pushed_by=iesūtīja
 runs.invalid_workflow_helper=Darbaplūsmas konfigurācijas fails ir kļūdains. Pārbaudiet konfiugrācijas failu: %s
+runs.no_matching_online_runner_helper=Nav pieejami izpildītāji, kas atbilstu šai iezīmei: %s
+runs.actor=Aktors
 runs.status=Statuss
+runs.actors_no_select=Visi aktori
+runs.status_no_select=Visi stāvokļi
+runs.no_results=Netika atrasts nekas atbilstošs.
+runs.no_workflows=Vēl nav nevienas darbplūsmas.
+runs.no_workflows.quick_start=Nav skaidrs, kā sākt izmantot Gitea darbības? Skatīt <a target="_blank" rel="noopener noreferrer" href="%s">ātrās sākšanas norādes</a>.
+runs.no_workflows.documentation=Vairāk informācijas par Gitea darbībām ir skatāma <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>.
+runs.no_runs=Darbplūsmai vēl nav nevienas izpildes.
 runs.empty_commit_message=(tukšs revīzijas ziņojums)
 
+workflow.disable=Atspējot darbplūsmu
+workflow.disable_success=Darbplūsma '%s' ir veiksmīgi atspējota.
+workflow.enable=Iespējot darbplūsmu
+workflow.enable_success=Darbplūsma '%s' ir veiksmīgi iespējota.
+workflow.disabled=Darbplūsma ir atspējota.
 
 need_approval_desc=Nepieciešams apstiprinājums, lai izpildītu izmaiņu pieprasījumu darbaplūsmas no atdalītiem repozitorijiem.
 
+variables=Mainīgie
+variables.management=Mainīgo pārvaldība
+variables.creation=Pievienot mainīgo
+variables.none=Vēl nav neviena mainīgā.
+variables.deletion=Noņemt mainīgo
+variables.deletion.description=Mainīgā noņemšana ir neatgriezeniska un nav atsaucama. Vai turpināt?
+variables.description=Mainīgie tiks padoti noteiktām darbībām, un citādāk tos nevar nolasīt.
 variables.id_not_exist=Mainīgais ar identifikatoru %d neeksistē.
+variables.edit=Labot mainīgo
+variables.deletion.failed=Neizdevās noņemt mainīgo.
+variables.deletion.success=Mainīgais tika noņemts.
+variables.creation.failed=Neizdevās pievienot mainīgo.
+variables.creation.success=Mainīgais "%s" tika pievienots.
+variables.update.failed=Neizdevās labot mainīgo.
+variables.update.success=Mainīgais tika labots.
 
 [projects]
 type-1.display_name=Individuālais projekts
@@ -3381,6 +3567,11 @@ type-2.display_name=Repozitorija projekts
 type-3.display_name=Organizācijas projekts
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Direktorija
+normal_file=Parasts fails
+executable_file=Izpildāmais fails
 symbolic_link=Simboliska saite
+submodule=Apakšmodulis
 

From 3d3c3d9ee5e934c515370d98f1c552ca8ef10f8a Mon Sep 17 00:00:00 2001
From: DC <106393991+DanielMatiasCarvalho@users.noreply.github.com>
Date: Wed, 21 Feb 2024 01:55:26 +0000
Subject: [PATCH 107/679] Update Discord logo (#29285)

Fixes #27057 by changing the discord .svg file and running `make svg`.

Before:

<img width="637"
src="https://private-user-images.githubusercontent.com/85847352/267667100-1eaf5d20-b4e9-4736-bb55-7f1da04bbde7.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDg0NzAwNDUsIm5iZiI6MTcwODQ2OTc0NSwicGF0aCI6Ii84NTg0NzM1Mi8yNjc2NjcxMDAtMWVhZjVkMjAtYjRlOS00NzM2LWJiNTUtN2YxZGEwNGJiZGU3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAyMjAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMjIwVDIyNTU0NVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTIwN2Y2ODc5N2MzZDU5NzgzODRhNDIzZWY3MDk3ODhiYmIzZDU4NWVlYmFmZjc2OTIyZjE3MWM4ZDg0ODZjNTYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.C6jVQLFPfq4fhGV8wiY9D-P21PUNTDMkX2d2-kU17Ug">

After:

<img width="637"
src="https://github.com/go-gitea/gitea/assets/106393991/45b197ae-e422-42f4-999e-25dc8f6b7a92">
---
 public/assets/img/svg/gitea-discord.svg | 2 +-
 web_src/svg/gitea-discord.svg           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/assets/img/svg/gitea-discord.svg b/public/assets/img/svg/gitea-discord.svg
index 6ebbdcdcc3..2edcb4fed7 100644
--- a/public/assets/img/svg/gitea-discord.svg
+++ b/public/assets/img/svg/gitea-discord.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-discord__svg gitea-discord__gitea-discord svg gitea-discord" preserveAspectRatio="xMidYMid" viewBox="0 0 256 293" width="16" height="16"><path fill="#7289DA" d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0m-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732a2 2 0 0 1-.585-.438c-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.8 123.8 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238s-6.582-16.237-14.92-16.237"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" class="svg gitea-discord" width="16" height="16" aria-hidden="true"><path fill="#5865f2" d="M107.7 8.07A105.2 105.2 0 0 0 81.47 0a72 72 0 0 0-3.36 6.83 97.7 97.7 0 0 0-29.11 0A72 72 0 0 0 45.64 0a106 106 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.7 105.7 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.4 68.4 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.7 68.7 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.3 105.3 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15M42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69m42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69"/></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-discord.svg b/web_src/svg/gitea-discord.svg
index ea64a39f6e..4cadbc7f7e 100644
--- a/web_src/svg/gitea-discord.svg
+++ b/web_src/svg/gitea-discord.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="0 0 256 293" class="svg gitea-discord" width="16" height="16" aria-hidden="true"><path fill="#7289DA" d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0zm-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732a2.022 2.022 0 0 1-.585-.438c-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.776 123.776 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238 0-8.923-6.582-16.237-14.92-16.237"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path fill="#5865f2" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></svg>
\ No newline at end of file

From 22b8de85ddda50725480b21c5bf6ef9c0202b5e9 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Wed, 21 Feb 2024 12:57:22 +0800
Subject: [PATCH 108/679] Do not use `ctx.Doer` when reset password (#29289)

Fix #29278.

Caused by a small typo in #28733
---
 routers/web/auth/password.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index c23379b87a..1f2d133282 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -204,7 +204,7 @@ func ResetPasswdPost(ctx *context.Context) {
 		Password:           optional.Some(ctx.FormString("password")),
 		MustChangePassword: optional.Some(false),
 	}
-	if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+	if err := user_service.UpdateAuth(ctx, u, opts); err != nil {
 		ctx.Data["IsResetForm"] = true
 		ctx.Data["Err_Password"] = true
 		switch {

From 7f45dfb030f30a3ada58e636e3b8bfde391224bd Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 21 Feb 2024 15:01:48 +0800
Subject: [PATCH 109/679] Always write proc-receive hook for all git versions
 (#29287)

---
 modules/repository/hooks.go | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go
index daab7c3091..95849789ab 100644
--- a/modules/repository/hooks.go
+++ b/modules/repository/hooks.go
@@ -9,7 +9,6 @@ import (
 	"path/filepath"
 	"runtime"
 
-	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
@@ -94,15 +93,14 @@ done
 `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
 	}
 
-	if git.SupportProcReceive {
-		hookNames = append(hookNames, "proc-receive")
-		hookTpls = append(hookTpls,
-			fmt.Sprintf(`#!/usr/bin/env %s
+	// although only new git (>=2.29) supports proc-receive, it's still good to create its hook, in case the user upgrades git
+	hookNames = append(hookNames, "proc-receive")
+	hookTpls = append(hookTpls,
+		fmt.Sprintf(`#!/usr/bin/env %s
 # AUTO GENERATED BY GITEA, DO NOT MODIFY
 %s hook --config=%s proc-receive
 `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)))
-		giteaHookTpls = append(giteaHookTpls, "")
-	}
+	giteaHookTpls = append(giteaHookTpls, "")
 
 	return hookNames, hookTpls, giteaHookTpls
 }

From 4e536edaead97d61a64508db0e93cf781a889472 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 21 Feb 2024 10:13:48 +0200
Subject: [PATCH 110/679] Remove jQuery from the installation page (#29284)

- Switched to plain JavaScript
- Tested the installation page functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/286475b3-1919-4d99-b790-def10fa36e66)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/install.js | 101 ++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 52 deletions(-)

diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js
index 9fda7f7d27..2d6d345c1d 100644
--- a/web_src/js/features/install.js
+++ b/web_src/js/features/install.js
@@ -1,19 +1,17 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 import {GET} from '../modules/fetch.js';
 
 export function initInstall() {
-  const $page = $('.page-content.install');
-  if ($page.length === 0) {
+  const page = document.querySelector('.page-content.install');
+  if (!page) {
     return;
   }
-  if ($page.is('.post-install')) {
+  if (page.classList.contains('post-install')) {
     initPostInstall();
   } else {
     initPreInstall();
   }
 }
-
 function initPreInstall() {
   const defaultDbUser = 'gitea';
   const defaultDbName = 'gitea';
@@ -24,83 +22,82 @@ function initPreInstall() {
     mssql: '127.0.0.1:1433'
   };
 
-  const $dbHost = $('#db_host');
-  const $dbUser = $('#db_user');
-  const $dbName = $('#db_name');
+  const dbHost = document.getElementById('db_host');
+  const dbUser = document.getElementById('db_user');
+  const dbName = document.getElementById('db_name');
 
   // Database type change detection.
-  $('#db_type').on('change', function () {
-    const dbType = $(this).val();
-    hideElem($('div[data-db-setting-for]'));
-    showElem($(`div[data-db-setting-for=${dbType}]`));
+  document.getElementById('db_type').addEventListener('change', function () {
+    const dbType = this.value;
+    hideElem('div[data-db-setting-for]');
+    showElem(`div[data-db-setting-for=${dbType}]`);
 
     if (dbType !== 'sqlite3') {
       // for most remote database servers
-      showElem($(`div[data-db-setting-for=common-host]`));
-      const lastDbHost = $dbHost.val();
+      showElem('div[data-db-setting-for=common-host]');
+      const lastDbHost = dbHost.value;
       const isDbHostDefault = !lastDbHost || Object.values(defaultDbHosts).includes(lastDbHost);
       if (isDbHostDefault) {
-        $dbHost.val(defaultDbHosts[dbType] ?? '');
+        dbHost.value = defaultDbHosts[dbType] ?? '';
       }
-      if (!$dbUser.val() && !$dbName.val()) {
-        $dbUser.val(defaultDbUser);
-        $dbName.val(defaultDbName);
+      if (!dbUser.value && !dbName.value) {
+        dbUser.value = defaultDbUser;
+        dbName.value = defaultDbName;
       }
     } // else: for SQLite3, the default path is always prepared by backend code (setting)
-  }).trigger('change');
+  });
+  document.getElementById('db_type').dispatchEvent(new Event('change'));
 
-  const $appUrl = $('#app_url');
-  const configAppUrl = $appUrl.val();
-  if (configAppUrl.includes('://localhost')) {
-    $appUrl.val(window.location.href);
+  const appUrl = document.getElementById('app_url');
+  if (appUrl.value.includes('://localhost')) {
+    appUrl.value = window.location.href;
   }
 
-  const $domain = $('#domain');
-  const configDomain = $domain.val().trim();
-  if (configDomain === 'localhost') {
-    $domain.val(window.location.hostname);
+  const domain = document.getElementById('domain');
+  if (domain.value.trim() === 'localhost') {
+    domain.value = window.location.hostname;
   }
 
   // TODO: better handling of exclusive relations.
-  $('#offline-mode input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#disable-gravatar').checkbox('check');
-      $('#federated-avatar-lookup').checkbox('uncheck');
+  document.querySelector('#offline-mode input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#disable-gravatar input').checked = true;
+      document.querySelector('#federated-avatar-lookup input').checked = false;
     }
   });
-  $('#disable-gravatar input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#federated-avatar-lookup').checkbox('uncheck');
+  document.querySelector('#disable-gravatar input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#federated-avatar-lookup input').checked = false;
     } else {
-      $('#offline-mode').checkbox('uncheck');
+      document.querySelector('#offline-mode input').checked = false;
     }
   });
-  $('#federated-avatar-lookup input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#disable-gravatar').checkbox('uncheck');
-      $('#offline-mode').checkbox('uncheck');
+  document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#disable-gravatar input').checked = false;
+      document.querySelector('#offline-mode input').checked = false;
     }
   });
-  $('#enable-openid-signin input').on('change', function () {
-    if ($(this).is(':checked')) {
-      if (!$('#disable-registration input').is(':checked')) {
-        $('#enable-openid-signup').checkbox('check');
+  document.querySelector('#enable-openid-signin input').addEventListener('change', function () {
+    if (this.checked) {
+      if (!document.querySelector('#disable-registration input').checked) {
+        document.querySelector('#enable-openid-signup input').checked = true;
       }
     } else {
-      $('#enable-openid-signup').checkbox('uncheck');
+      document.querySelector('#enable-openid-signup input').checked = false;
     }
   });
-  $('#disable-registration input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#enable-captcha').checkbox('uncheck');
-      $('#enable-openid-signup').checkbox('uncheck');
+  document.querySelector('#disable-registration input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#enable-captcha input').checked = false;
+      document.querySelector('#enable-openid-signup input').checked = false;
     } else {
-      $('#enable-openid-signup').checkbox('check');
+      document.querySelector('#enable-openid-signup input').checked = true;
     }
   });
-  $('#enable-captcha input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#disable-registration').checkbox('uncheck');
+  document.querySelector('#enable-captcha input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#disable-registration input').checked = false;
     }
   });
 }

From 6130522aa86316c7d87e130cc8c440fd06920928 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 21 Feb 2024 18:08:08 +0800
Subject: [PATCH 111/679] Refactor markup rendering to accept general
 "protocol:" prefix (#29276)

Follow #29024

Major changes:

* refactor validLinksPattern to fullURLPattern and add comments, now it
accepts "protocol:" prefix
* rename `IsLink*` to `IsFullURL*`, and remove unnecessray "mailto:"
check
* fix some comments (by the way)
* rename EmojiShortCodeRegex -> emojiShortCodeRegex (by the way)
---
 modules/markup/html.go              | 34 ++++++++++++++---------------
 modules/markup/html_test.go         | 15 +++++++++++++
 modules/markup/markdown/goldmark.go | 18 +++++----------
 modules/markup/orgmode/orgmode.go   |  3 +--
 4 files changed, 38 insertions(+), 32 deletions(-)

diff --git a/modules/markup/html.go b/modules/markup/html.go
index b7291823b5..56e1a1c54e 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -53,38 +53,38 @@ var (
 	// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
 	shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
 
-	// anySHA1Pattern splits url containing SHA into parts
+	// anyHashPattern splits url containing SHA into parts
 	anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
 
 	// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
 	comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
 
-	validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`)
+	// fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..."
+	fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`)
 
-	// While this email regex is definitely not perfect and I'm sure you can come up
-	// with edge cases, it is still accepted by the CommonMark specification, as
-	// well as the HTML5 spec:
+	// emailRegex is definitely not perfect with edge cases,
+	// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
 	//   http://spec.commonmark.org/0.28/#email-address
 	//   https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
 	emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
 
-	// blackfriday extensions create IDs like fn:user-content-footnote
+	// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
 	blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
 
-	// EmojiShortCodeRegex find emoji by alias like :smile:
-	EmojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
+	// emojiShortCodeRegex find emoji by alias like :smile:
+	emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
 )
 
 // CSS class for action keywords (e.g. "closes: #1")
 const keywordClass = "issue-keyword"
 
-// IsLink reports whether link fits valid format.
-func IsLink(link []byte) bool {
-	return validLinksPattern.Match(link)
+// IsFullURLBytes reports whether link fits valid format.
+func IsFullURLBytes(link []byte) bool {
+	return fullURLPattern.Match(link)
 }
 
-func IsLinkStr(link string) bool {
-	return validLinksPattern.MatchString(link)
+func IsFullURLString(link string) bool {
+	return fullURLPattern.MatchString(link)
 }
 
 // regexp for full links to issues/pulls
@@ -399,7 +399,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
 				if attr.Key != "src" {
 					continue
 				}
-				if len(attr.Val) > 0 && !IsLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
+				if len(attr.Val) > 0 && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
 					attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
 				}
 				attr.Val = camoHandleLink(attr.Val)
@@ -650,7 +650,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
 				// There is no equal in this argument; this is a mandatory arg
 				if props["name"] == "" {
-					if IsLinkStr(v) {
+					if IsFullURLString(v) {
 						// If we clearly see it is a link, we save it so
 
 						// But first we need to ensure, that if both mandatory args provided
@@ -725,7 +725,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			DataAtom:   atom.A,
 		}
 		childNode.Parent = linkNode
-		absoluteLink := IsLinkStr(link)
+		absoluteLink := IsFullURLString(link)
 		if !absoluteLink {
 			if image {
 				link = strings.ReplaceAll(link, " ", "+")
@@ -1059,7 +1059,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
 	start := 0
 	next := node.NextSibling
 	for node != nil && node != next && start < len(node.Data) {
-		m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
+		m := emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
 		if m == nil {
 			return
 		}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 89ecfc036b..cb29431d4b 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -204,6 +204,15 @@ func TestRender_links(t *testing.T) {
 	test(
 		"magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download",
 		`<p><a href="magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&amp;dn=download" rel="nofollow">magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&amp;dn=download</a></p>`)
+	test(
+		`[link](https://example.com)`,
+		`<p><a href="https://example.com" rel="nofollow">link</a></p>`)
+	test(
+		`[link](mailto:test@example.com)`,
+		`<p><a href="mailto:test@example.com" rel="nofollow">link</a></p>`)
+	test(
+		`[link](javascript:xss)`,
+		`<p>link</p>`)
 
 	// Test that should *not* be turned into URL
 	test(
@@ -673,3 +682,9 @@ func TestIssue18471(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String())
 }
+
+func TestIsFullURL(t *testing.T) {
+	assert.True(t, markup.IsFullURLString("https://example.com"))
+	assert.True(t, markup.IsFullURLString("mailto:test@example.com"))
+	assert.False(t, markup.IsFullURLString("/foo:bar"))
+}
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 36ce6397f4..c4b23e66fc 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -26,8 +26,6 @@ import (
 	"github.com/yuin/goldmark/util"
 )
 
-var byteMailto = []byte("mailto:")
-
 // ASTTransformer is a default transformer of the goldmark tree.
 type ASTTransformer struct{}
 
@@ -84,7 +82,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 			// 2. If they're not wrapped with a link they need a link wrapper
 
 			// Check if the destination is a real link
-			if len(v.Destination) > 0 && !markup.IsLink(v.Destination) {
+			if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
 				v.Destination = []byte(giteautil.URLJoin(
 					ctx.Links.ResolveMediaLink(ctx.IsWiki),
 					strings.TrimLeft(string(v.Destination), "/"),
@@ -130,23 +128,17 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 		case *ast.Link:
 			// Links need their href to munged to be a real value
 			link := v.Destination
-			if len(link) > 0 && !markup.IsLink(link) &&
-				link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
-				// special case: this is not a link, a hash link or a mailto:, so it's a
-				// relative URL
-
-				var base string
+			isAnchorFragment := len(link) > 0 && link[0] == '#'
+			if !isAnchorFragment && !markup.IsFullURLBytes(link) {
+				base := ctx.Links.Base
 				if ctx.IsWiki {
 					base = ctx.Links.WikiLink()
 				} else if ctx.Links.HasBranchInfo() {
 					base = ctx.Links.SrcLink()
-				} else {
-					base = ctx.Links.Base
 				}
-
 				link = []byte(giteautil.URLJoin(base, string(link)))
 			}
-			if len(link) > 0 && link[0] == '#' {
+			if isAnchorFragment {
 				link = []byte("#user-content-" + string(link)[1:])
 			}
 			v.Destination = link
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index ac1cedff6d..7f253ae5f1 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -136,8 +136,7 @@ type Writer struct {
 func (r *Writer) resolveLink(kind, link string) string {
 	link = strings.TrimPrefix(link, "file:")
 	if !strings.HasPrefix(link, "#") && // not a URL fragment
-		!markup.IsLinkStr(link) && // not an absolute URL
-		!strings.HasPrefix(link, "mailto:") {
+		!markup.IsFullURLString(link) {
 		if kind == "regular" {
 			// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]"
 			// so we need to try to guess the link kind again here

From 79217ea63c1f77de7ca79813ae45950724e63d02 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Wed, 21 Feb 2024 19:40:46 +0800
Subject: [PATCH 112/679] Fix error display when merging PRs (#29288)

Partially fix #29071, regression of Modernize merge button #28140

Fix some missing `Redirect` -> `JSONRedirect`.

Thanks @yp05327 for the help in
https://github.com/go-gitea/gitea/issues/29071#issuecomment-1931261075
---
 routers/web/repo/pull.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 7052467e64..b9a4aff02e 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1283,19 +1283,19 @@ func MergePullRequest(ctx *context.Context) {
 				return
 			}
 			ctx.Flash.Error(flashError)
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if models.IsErrMergeUnrelatedHistories(err) {
 			log.Debug("MergeUnrelatedHistories error: %v", err)
 			ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if git.IsErrPushOutOfDate(err) {
 			log.Debug("MergePushOutOfDate error: %v", err)
 			ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if models.IsErrSHADoesNotMatch(err) {
 			log.Debug("MergeHeadOutOfDate error: %v", err)
 			ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date"))
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if git.IsErrPushRejected(err) {
 			log.Debug("MergePushRejected error: %v", err)
 			pushrejErr := err.(*git.ErrPushRejected)

From e6e50696b83164805bec83a1b20c95a85a4dd7e5 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 21 Feb 2024 22:14:37 +0800
Subject: [PATCH 113/679] Revert #28753 because UI broken. (#29293)

Revert #29255
Revert #28753
---
 templates/user/auth/signin_inner.tmpl  | 11 +++++++----
 templates/user/auth/signin_openid.tmpl |  6 ++++--
 templates/user/auth/signup_inner.tmpl  | 14 ++++++++------
 web_src/css/form.css                   |  4 ++++
 web_src/css/helpers.css                |  1 -
 5 files changed, 23 insertions(+), 13 deletions(-)

diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index a0aea5cb9b..40e54ec8fa 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -9,20 +9,21 @@
 	{{end}}
 </h4>
 <div class="ui attached segment">
-	<form class="ui form gt-max-width-36rem gt-m-auto" action="{{.SignInLink}}" method="post">
+	<form class="ui form" action="{{.SignInLink}}" method="post">
 	{{.CsrfTokenHtml}}
 	<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
-		<input id="user_name" class="gt-w-full" type="text" name="user_name" value="{{.user_name}}" autofocus required>
+		<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 	</div>
 	{{if or (not .DisablePassword) .LinkAccountMode}}
 	<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="password">{{ctx.Locale.Tr "password"}}</label>
-		<input id="password" class="gt-w-full" name="password" type="password" value="{{.password}}" autocomplete="current-password" required>
+		<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required>
 	</div>
 	{{end}}
 	{{if not .LinkAccountMode}}
 	<div class="inline field">
+		<label></label>
 		<div class="ui checkbox">
 			<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 			<input name="remember" type="checkbox">
@@ -33,6 +34,7 @@
 	{{template "user/auth/captcha" .}}
 
 	<div class="inline field">
+		<label></label>
 		<button class="ui primary button">
 			{{if .LinkAccountMode}}
 				{{ctx.Locale.Tr "auth.oauth_signin_submit"}}
@@ -45,6 +47,7 @@
 
 	{{if .ShowRegistrationButton}}
 		<div class="inline field">
+			<label></label>
 			<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a>
 		</div>
 	{{end}}
@@ -57,7 +60,7 @@
 		<div class="gt-df gt-fc gt-jc">
 			<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
 				{{range $provider := .OAuth2Providers}}
-					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 gt-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
 						{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 					</a>
diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl
index a138ea0b8d..0428026aa8 100644
--- a/templates/user/auth/signin_openid.tmpl
+++ b/templates/user/auth/signin_openid.tmpl
@@ -8,7 +8,7 @@
 			OpenID
 		</h4>
 		<div class="ui attached segment">
-			<form class="ui form gt-m-auto" action="{{.Link}}" method="post">
+			<form class="ui form" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="inline field">
 				{{ctx.Locale.Tr "auth.openid_signin_desc"}}
@@ -18,15 +18,17 @@
 				{{svg "fontawesome-openid"}}
 				OpenID URI
 				</label>
-				<input id="openid" class="gt-w-full" name="openid" value="{{.openid}}" autofocus required>
+				<input id="openid" name="openid" value="{{.openid}}" autofocus required>
 			</div>
 			<div class="inline field">
+				<label></label>
 				<div class="ui checkbox">
 					<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 					<input name="remember" type="checkbox">
 				</div>
 			</div>
 			<div class="inline field">
+				<label></label>
 				<button class="ui primary button">{{ctx.Locale.Tr "sign_in"}}</button>
 			</div>
 			</form>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index 65ce98c31a..e930bd3d15 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -7,7 +7,7 @@
 		{{end}}
 	</h4>
 	<div class="ui attached segment">
-		<form class="ui form gt-max-width-36rem gt-m-auto" action="{{.SignUpLink}}" method="post">
+		<form class="ui form" action="{{.SignUpLink}}" method="post">
 			{{.CsrfTokenHtml}}
 			{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
 			{{template "base/alert" .}}
@@ -17,27 +17,28 @@
 			{{else}}
 				<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 					<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
-					<input id="user_name" class="gt-w-full" type="text" name="user_name" value="{{.user_name}}" autofocus required>
+					<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 				</div>
 				<div class="required inline field {{if .Err_Email}}error{{end}}">
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
-					<input id="email" class="gt-w-full" name="email" type="email" value="{{.email}}" required>
+					<input id="email" name="email" type="email" value="{{.email}}" required>
 				</div>
 
 				{{if not .DisablePassword}}
 					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="password">{{ctx.Locale.Tr "password"}}</label>
-						<input id="password" class="gt-w-full" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
+						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
 					</div>
 					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="retype">{{ctx.Locale.Tr "re_type"}}</label>
-						<input id="retype" class="gt-w-full" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required>
+						<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required>
 					</div>
 				{{end}}
 
 				{{template "user/auth/captcha" .}}
 
 				<div class="inline field">
+					<label></label>
 					<button class="ui primary button">
 						{{if .LinkAccountMode}}
 							{{ctx.Locale.Tr "auth.oauth_signup_submit"}}
@@ -49,6 +50,7 @@
 
 				{{if not .LinkAccountMode}}
 				<div class="inline field">
+					<label></label>
 					<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.register_helper_msg"}}</a>
 				</div>
 				{{end}}
@@ -62,7 +64,7 @@
 				<div class="gt-df gt-fc gt-jc">
 					<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
 						{{range $provider := .OAuth2Providers}}
-							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 gt-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
 								{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 							</a>
diff --git a/web_src/css/form.css b/web_src/css/form.css
index a5288c9309..e4efa34948 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -242,6 +242,8 @@ textarea:focus,
 .user.activate form,
 .user.forgot.password form,
 .user.reset.password form,
+.user.link-account form,
+.user.signin form,
 .user.signup form {
   margin: auto;
   width: 700px !important;
@@ -276,6 +278,8 @@ textarea:focus,
   .user.activate form .inline.field > label,
   .user.forgot.password form .inline.field > label,
   .user.reset.password form .inline.field > label,
+  .user.link-account form .inline.field > label,
+  .user.signin form .inline.field > label,
   .user.signup form .inline.field > label {
     text-align: right;
     width: 250px !important;
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index c7d8abb1d4..da94ebb486 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -48,7 +48,6 @@ Gitea's private styles use `g-` prefix.
 
 .gt-max-width-12rem { max-width: 12rem !important; }
 .gt-max-width-24rem { max-width: 24rem !important; }
-.gt-max-width-36rem { max-width: 36rem !important; }
 
 /* below class names match Tailwind CSS */
 .gt-break-all { word-break: break-all !important; }

From f74c869221624092999097af38b6f7fae4701420 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Wed, 21 Feb 2024 19:54:17 +0100
Subject: [PATCH 114/679] Prevent double use of `git cat-file` session.
 (#29298)

Fixes the reason why #29101 is hard to replicate.
Related #29297

Create a repo with a file with minimum size 4097 bytes (I use 10000) and
execute the following code:
```go
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, <repo>)
assert.NoError(t, err)

commit, err := gitRepo.GetCommit(<sha>)
assert.NoError(t, err)

entry, err := commit.GetTreeEntryByPath(<file>)
assert.NoError(t, err)

b := entry.Blob()

// Create a reader
r, err := b.DataAsync()
assert.NoError(t, err)
defer r.Close()

// Create a second reader
r2, err := b.DataAsync()
assert.NoError(t, err) // Should be no error but is ErrNotExist
defer r2.Close()
```

The problem is the check in `CatFileBatch`:

https://github.com/go-gitea/gitea/blob/79217ea63c1f77de7ca79813ae45950724e63d02/modules/git/repo_base_nogogit.go#L81-L87
`Buffered() > 0` is used to check if there is a "operation" in progress
at the moment. This is a problem because we can't control the internal
buffer in the `bufio.Reader`. The code above demonstrates a sequence
which initiates an operation for which the code thinks there is no
active processing. The second call to `DataAsync()` therefore reuses the
existing instances instead of creating a new batch reader.
---
 modules/git/repo_base_nogogit.go | 21 ++++++++++-----
 tests/integration/git_test.go    | 44 ++++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+), 6 deletions(-)

diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
index d5a350a926..8c6eae5897 100644
--- a/modules/git/repo_base_nogogit.go
+++ b/modules/git/repo_base_nogogit.go
@@ -27,10 +27,12 @@ type Repository struct {
 
 	gpgSettings *GPGSettings
 
+	batchInUse  bool
 	batchCancel context.CancelFunc
 	batchReader *bufio.Reader
 	batchWriter WriteCloserError
 
+	checkInUse  bool
 	checkCancel context.CancelFunc
 	checkReader *bufio.Reader
 	checkWriter WriteCloserError
@@ -79,23 +81,28 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 
 // CatFileBatch obtains a CatFileBatch for this repository
 func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
-	if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 {
+	if repo.batchCancel == nil || repo.batchInUse {
 		log.Debug("Opening temporary cat file batch for: %s", repo.Path)
 		return CatFileBatch(ctx, repo.Path)
 	}
-	return repo.batchWriter, repo.batchReader, func() {}
+	repo.batchInUse = true
+	return repo.batchWriter, repo.batchReader, func() {
+		repo.batchInUse = false
+	}
 }
 
 // CatFileBatchCheck obtains a CatFileBatchCheck for this repository
 func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
-	if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 {
-		log.Debug("Opening temporary cat file batch-check: %s", repo.Path)
+	if repo.checkCancel == nil || repo.checkInUse {
+		log.Debug("Opening temporary cat file batch-check for: %s", repo.Path)
 		return CatFileBatchCheck(ctx, repo.Path)
 	}
-	return repo.checkWriter, repo.checkReader, func() {}
+	repo.checkInUse = true
+	return repo.checkWriter, repo.checkReader, func() {
+		repo.checkInUse = false
+	}
 }
 
-// Close this repository, in particular close the underlying gogitStorage if this is not nil
 func (repo *Repository) Close() (err error) {
 	if repo == nil {
 		return nil
@@ -105,12 +112,14 @@ func (repo *Repository) Close() (err error) {
 		repo.batchReader = nil
 		repo.batchWriter = nil
 		repo.batchCancel = nil
+		repo.batchInUse = false
 	}
 	if repo.checkCancel != nil {
 		repo.checkCancel()
 		repo.checkCancel = nil
 		repo.checkReader = nil
 		repo.checkWriter = nil
+		repo.checkInUse = false
 	}
 	repo.LastCommitCache = nil
 	repo.tagCache = nil
diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go
index 0c3a8616f0..95350d79ca 100644
--- a/tests/integration/git_test.go
+++ b/tests/integration/git_test.go
@@ -4,6 +4,7 @@
 package integration
 
 import (
+	"bytes"
 	"encoding/hex"
 	"fmt"
 	"math/rand"
@@ -25,9 +26,11 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -848,3 +851,44 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 		t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
 	}
 }
+
+func TestDataAsync_Issue29101(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+		resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      "test.txt",
+					ContentReader: bytes.NewReader(make([]byte, 10000)),
+				},
+			},
+			OldBranch: repo.DefaultBranch,
+			NewBranch: repo.DefaultBranch,
+		})
+		assert.NoError(t, err)
+
+		sha := resp.Commit.SHA
+
+		gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
+		assert.NoError(t, err)
+
+		commit, err := gitRepo.GetCommit(sha)
+		assert.NoError(t, err)
+
+		entry, err := commit.GetTreeEntryByPath("test.txt")
+		assert.NoError(t, err)
+
+		b := entry.Blob()
+
+		r, err := b.DataAsync()
+		assert.NoError(t, err)
+		defer r.Close()
+
+		r2, err := b.DataAsync()
+		assert.NoError(t, err)
+		defer r2.Close()
+	})
+}

From 2bd999a28b472f2909f6856740cdc712eb0ad136 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Thu, 22 Feb 2024 00:23:48 +0000
Subject: [PATCH 115/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_cs-CZ.ini | 11 ++++++--
 options/locale/locale_de-DE.ini |  9 +++++-
 options/locale/locale_el-GR.ini | 11 ++++++--
 options/locale/locale_es-ES.ini |  9 +++++-
 options/locale/locale_fa-IR.ini |  6 ++++
 options/locale/locale_fi-FI.ini |  6 ++++
 options/locale/locale_fr-FR.ini | 11 ++++++--
 options/locale/locale_hu-HU.ini |  6 ++++
 options/locale/locale_id-ID.ini |  6 ++++
 options/locale/locale_is-IS.ini |  6 ++++
 options/locale/locale_it-IT.ini |  6 ++++
 options/locale/locale_ja-JP.ini | 11 ++++++--
 options/locale/locale_ko-KR.ini |  6 ++++
 options/locale/locale_lv-LV.ini | 11 ++++++--
 options/locale/locale_nl-NL.ini |  6 ++++
 options/locale/locale_pl-PL.ini |  6 ++++
 options/locale/locale_pt-BR.ini |  8 ++++++
 options/locale/locale_pt-PT.ini | 49 +++++++++++++++++++++++++++++++--
 options/locale/locale_ru-RU.ini | 11 ++++++--
 options/locale/locale_si-LK.ini |  6 ++++
 options/locale/locale_sk-SK.ini |  6 ++++
 options/locale/locale_sv-SE.ini |  6 ++++
 options/locale/locale_tr-TR.ini | 11 ++++++--
 options/locale/locale_uk-UA.ini |  6 ++++
 options/locale/locale_zh-CN.ini | 45 ++++++++++++++++++++++++++++--
 options/locale/locale_zh-HK.ini |  6 ++++
 options/locale/locale_zh-TW.ini |  8 ++++++
 27 files changed, 260 insertions(+), 29 deletions(-)

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 8d1a46c6b6..d30103a8eb 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -588,6 +588,7 @@ org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. N
 
 target_branch_not_exist=Cílová větev neexistuje.
 
+
 [user]
 change_avatar=Změnit váš avatar…
 joined_on=Přidal/a se %s
@@ -1954,6 +1955,8 @@ activity.git_stats_and_deletions=a
 activity.git_stats_deletion_1=%d odebrání
 activity.git_stats_deletion_n=%d odebrání
 
+contributors.contribution_type.commits=Commity
+
 search=Vyhledat
 search.search_repo=Hledat repozitář
 search.type.tooltip=Druh vyhledávání
@@ -2541,6 +2544,8 @@ error.csv.too_large=Tento soubor nelze vykreslit, protože je příliš velký.
 error.csv.unexpected=Tento soubor nelze vykreslit, protože obsahuje neočekávaný znak na řádku %d ve sloupci %d.
 error.csv.invalid_field_count=Soubor nelze vykreslit, protože má nesprávný počet polí na řádku %d.
 
+[graphs]
+
 [org]
 org_name_holder=Název organizace
 org_full_name_holder=Celý název organizace
@@ -3179,6 +3184,7 @@ notices.desc=Popis
 notices.op=Akce
 notices.delete_success=Systémové upozornění bylo smazáno.
 
+
 [action]
 create_repo=vytvořil/a repozitář <a href="%s">%s</a>
 rename_repo=přejmenoval/a repozitář z <code>%[1]s</code> na <a href="%[2]s">%[3]s</a>
@@ -3363,6 +3369,8 @@ rpm.registry=Nastavte tento registr z příkazového řádku:
 rpm.distros.redhat=na distribuce založené na RedHat
 rpm.distros.suse=na distribuce založené na SUSE
 rpm.install=Pro instalaci balíčku spusťte následující příkaz:
+rpm.repository=Informace o repozitáři
+rpm.repository.architectures=Architektury
 rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz:
 rubygems.install2=nebo ho přidejte do Gemfie:
 rubygems.dependencies.runtime=Běhové závislosti
@@ -3490,8 +3498,6 @@ runs.actors_no_select=Všichni aktéři
 runs.status_no_select=Všechny stavy
 runs.no_results=Nebyly nalezeny žádné výsledky.
 runs.no_workflows=Zatím neexistují žádné pracovní postupy.
-runs.no_workflows.quick_start=Nevíte jak začít s Gitea Action? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">průvodce rychlým startem</a>.
-runs.no_workflows.documentation=Další informace o Gitea Action, viz <a target="_blank" rel="noopener noreferrer" href="%s">dokumentace</a>.
 runs.no_runs=Pracovní postup zatím nebyl spuštěn.
 runs.empty_commit_message=(prázdná zpráva commitu)
 
@@ -3509,7 +3515,6 @@ variables.none=Zatím nejsou žádné proměnné.
 variables.deletion=Odstranit proměnnou
 variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat?
 variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak.
-variables.id_not_exist=Proměnná s id %d neexistuje.
 variables.edit=Upravit proměnnou
 variables.deletion.failed=Nepodařilo se odstranit proměnnou.
 variables.deletion.success=Proměnná byla odstraněna.
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index c24d25b1ac..fa10bfcb11 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -585,6 +585,7 @@ org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete,
 
 target_branch_not_exist=Der Ziel-Branch existiert nicht.
 
+
 [user]
 change_avatar=Profilbild ändern…
 joined_on=Beigetreten am %s
@@ -1952,6 +1953,8 @@ activity.git_stats_and_deletions=und
 activity.git_stats_deletion_1=%d Löschung
 activity.git_stats_deletion_n=%d Löschungen
 
+contributors.contribution_type.commits=Commits
+
 search=Suchen
 search.search_repo=Repository durchsuchen
 search.type.tooltip=Suchmodus
@@ -2550,6 +2553,8 @@ error.csv.too_large=Diese Datei kann nicht gerendert werden, da sie zu groß ist
 error.csv.unexpected=Diese Datei kann nicht gerendert werden, da sie ein unerwartetes Zeichen in Zeile %d und Spalte %d enthält.
 error.csv.invalid_field_count=Diese Datei kann nicht gerendert werden, da sie eine falsche Anzahl an Feldern in Zeile %d hat.
 
+[graphs]
+
 [org]
 org_name_holder=Name der Organisation
 org_full_name_holder=Vollständiger Name der Organisation
@@ -3199,6 +3204,7 @@ notices.desc=Beschreibung
 notices.op=Aktion
 notices.delete_success=Diese Systemmeldung wurde gelöscht.
 
+
 [action]
 create_repo=hat das Repository <a href="%s">%s</a> erstellt
 rename_repo=hat das Repository von <code>%[1]s</code> zu <a href="%[2]s">%[3]s</a> umbenannt
@@ -3383,6 +3389,8 @@ rpm.registry=Diese Registry über die Kommandozeile einrichten:
 rpm.distros.redhat=auf RedHat-basierten Distributionen
 rpm.distros.suse=auf SUSE-basierten Distributionen
 rpm.install=Nutze folgenden Befehl, um das Paket zu installieren:
+rpm.repository=Repository-Informationen
+rpm.repository.architectures=Architekturen
 rubygems.install=Um das Paket mit gem zu installieren, führe den folgenden Befehl aus:
 rubygems.install2=oder füg es zum Gemfile hinzu:
 rubygems.dependencies.runtime=Laufzeitabhängigkeiten
@@ -3530,7 +3538,6 @@ variables.none=Es gibt noch keine Variablen.
 variables.deletion=Variable entfernen
 variables.deletion.description=Das Entfernen einer Variable ist dauerhaft und kann nicht rückgängig gemacht werden. Fortfahren?
 variables.description=Variablen werden an bestimmte Aktionen übergeben und können nicht anderweitig gelesen werden.
-variables.id_not_exist=Variable mit ID %d existiert nicht.
 variables.edit=Variable bearbeiten
 variables.deletion.failed=Fehler beim Entfernen der Variable.
 variables.deletion.success=Die Variable wurde entfernt.
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 749a2ae403..2662a49cea 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -588,6 +588,7 @@ org_still_own_packages=Αυτός ο οργανισμός κατέχει ακό
 
 target_branch_not_exist=Ο κλάδος προορισμού δεν υπάρχει.
 
+
 [user]
 change_avatar=Αλλαγή του avatar σας…
 joined_on=Εγγράφηκε την %s
@@ -1966,6 +1967,8 @@ activity.git_stats_and_deletions=και
 activity.git_stats_deletion_1=%d διαγραφή
 activity.git_stats_deletion_n=%d διαγραφές
 
+contributors.contribution_type.commits=Υποβολές
+
 search=Αναζήτηση
 search.search_repo=Αναζήτηση αποθετηρίου
 search.type.tooltip=Τύπος αναζήτησης
@@ -2565,6 +2568,8 @@ error.csv.too_large=Δεν είναι δυνατή η απόδοση αυτού
 error.csv.unexpected=Δεν είναι δυνατή η απόδοση αυτού του αρχείου, επειδή περιέχει έναν μη αναμενόμενο χαρακτήρα στη γραμμή %d και στη στήλη %d.
 error.csv.invalid_field_count=Δεν είναι δυνατή η απόδοση αυτού του αρχείου, επειδή έχει λάθος αριθμό πεδίων στη γραμμή %d.
 
+[graphs]
+
 [org]
 org_name_holder=Όνομα Οργανισμού
 org_full_name_holder=Πλήρες Όνομα Οργανισμού
@@ -3216,6 +3221,7 @@ notices.desc=Περιγραφή
 notices.op=Λειτ.
 notices.delete_success=Οι ειδοποιήσεις του συστήματος έχουν διαγραφεί.
 
+
 [action]
 create_repo=δημιούργησε το αποθετήριο <a href="%s">%s</a>
 rename_repo=μετονόμασε το αποθετήριο από <code>%[1]s</code> σε <a href="%[2]s">%[3]s</a>
@@ -3400,6 +3406,8 @@ rpm.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμ
 rpm.distros.redhat=σε διανομές βασισμένες στο RedHat
 rpm.distros.suse=σε διανομές με βάση το SUSE
 rpm.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή:
+rpm.repository=Πληροφορίες Αποθετηρίου
+rpm.repository.architectures=Αρχιτεκτονικές
 rubygems.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το gem, εκτελέστε την ακόλουθη εντολή:
 rubygems.install2=ή προσθέστε το στο Gemfile:
 rubygems.dependencies.runtime=Εξαρτήσεις Εκτέλεσης
@@ -3532,8 +3540,6 @@ runs.actors_no_select=Όλοι οι φορείς
 runs.status_no_select=Όλες οι καταστάσεις
 runs.no_results=Δεν βρέθηκαν αποτελέσματα.
 runs.no_workflows=Δεν υπάρχουν ροές εργασίας ακόμα.
-runs.no_workflows.quick_start=Δεν ξέρετε πώς να ξεκινήσετε με τις Δράσεις Gitea; Συμβουλευτείτε <a target="_blank" rel="noopener noreferrer" href="%s">τον οδηγό για γρήγορη αρχή</a>.
-runs.no_workflows.documentation=Για περισσότερες πληροφορίες σχετικά με τη Δράση Gitea, ανατρέξτε <a target="_blank" rel="noopener noreferrer" href="%s">στην τεκμηρίωση</a>.
 runs.no_runs=Η ροή εργασίας δεν έχει τρέξει ακόμα.
 runs.empty_commit_message=(κενό μήνυμα υποβολής)
 
@@ -3552,7 +3558,6 @@ variables.none=Δεν υπάρχουν μεταβλητές ακόμα.
 variables.deletion=Αφαίρεση μεταβλητής
 variables.deletion.description=Η αφαίρεση μιας μεταβλητής είναι μόνιμη και δεν μπορεί να αναιρεθεί. Συνέχεια;
 variables.description=Η μεταβλητές θα δίνονται σε ορισμένες δράσεις και δεν μπορούν να διαβαστούν αλλιώς.
-variables.id_not_exist=Η μεταβλητή με id %d δεν υπάρχει.
 variables.edit=Επεξεργασία Μεταβλητής
 variables.deletion.failed=Αποτυχία αφαίρεσης της μεταβλητής.
 variables.deletion.success=Η μεταβλητή έχει αφαιρεθεί.
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 1a82ce5b76..c013927157 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -585,6 +585,7 @@ org_still_own_packages=Esta organización todavía posee uno o más paquetes, el
 
 target_branch_not_exist=La rama de destino no existe
 
+
 [user]
 change_avatar=Cambiar su avatar…
 joined_on=Se unió el %s
@@ -1952,6 +1953,8 @@ activity.git_stats_and_deletions=y
 activity.git_stats_deletion_1=%d eliminación
 activity.git_stats_deletion_n=%d eliminaciones
 
+contributors.contribution_type.commits=Commits
+
 search=Buscar
 search.search_repo=Buscar repositorio
 search.type.tooltip=Tipo de búsqueda
@@ -2550,6 +2553,8 @@ error.csv.too_large=No se puede renderizar este archivo porque es demasiado gran
 error.csv.unexpected=No se puede procesar este archivo porque contiene un carácter inesperado en la línea %d y la columna %d.
 error.csv.invalid_field_count=No se puede procesar este archivo porque tiene un número incorrecto de campos en la línea %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nombre de la organización
 org_full_name_holder=Nombre completo de la organización
@@ -3199,6 +3204,7 @@ notices.desc=Descripción
 notices.op=Operación
 notices.delete_success=Los avisos del sistema se han eliminado.
 
+
 [action]
 create_repo=creó el repositorio <a href="%s">%s</a>
 rename_repo=repositorio renombrado de <code>%[1]s</code> a <a href="%[2]s">%[3]s</a>
@@ -3383,6 +3389,8 @@ rpm.registry=Configurar este registro desde la línea de comandos:
 rpm.distros.redhat=en distribuciones basadas en RedHat
 rpm.distros.suse=en distribuciones basadas en SUSE
 rpm.install=Para instalar el paquete, ejecute el siguiente comando:
+rpm.repository=Información del repositorio
+rpm.repository.architectures=Arquitecturas
 rubygems.install=Para instalar el paquete usando gem, ejecute el siguiente comando:
 rubygems.install2=o añádelo al archivo Gemfile:
 rubygems.dependencies.runtime=Dependencias en tiempo de ejecución
@@ -3530,7 +3538,6 @@ variables.none=Aún no hay variables.
 variables.deletion=Eliminar variable
 variables.deletion.description=Eliminar una variable es permanente y no se puede deshacer. ¿Continuar?
 variables.description=Las variables se pasarán a ciertas acciones y no se podrán leer de otro modo.
-variables.id_not_exist=Variable con id %d no existe.
 variables.edit=Editar variable
 variables.deletion.failed=No se pudo eliminar la variable.
 variables.deletion.success=La variable ha sido eliminada.
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index c9099299a0..d2db7a20e9 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -463,6 +463,7 @@ auth_failed=تشخیص هویت ناموفق: %v
 
 target_branch_not_exist=شاخه مورد نظر وجود ندارد.
 
+
 [user]
 change_avatar=تغییر آواتار…
 repositories=مخازن
@@ -1498,6 +1499,8 @@ activity.git_stats_and_deletions=و
 activity.git_stats_deletion_1=%d مذحوف
 activity.git_stats_deletion_n=%d مذحوف
 
+contributors.contribution_type.commits=کامیت‌ها
+
 search=جستجو
 search.search_repo=جستجوی مخزن
 search.fuzzy=درهم
@@ -1951,6 +1954,8 @@ error.csv.too_large=نمی توان این فایل را رندر کرد زیر
 error.csv.unexpected=نمی توان این فایل را رندر کرد زیرا حاوی یک کاراکتر غیرمنتظره در خط %d و ستون %d است.
 error.csv.invalid_field_count=نمی توان این فایل را رندر کرد زیرا تعداد فیلدهای آن در خط %d اشتباه است.
 
+[graphs]
+
 [org]
 org_name_holder=نام سازمان
 org_full_name_holder=نام کامل سازمان
@@ -2501,6 +2506,7 @@ notices.desc=توضیحات
 notices.op=عملیات.
 notices.delete_success=گزارش سیستم حذف شده است.
 
+
 [action]
 create_repo=مخزن ایجاد شده <a href="%s"> %s</a>
 rename_repo=مخزن تغییر نام داد از <code>%[1]s</code> به <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index b6abb49a35..ab0dcc443d 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -425,6 +425,7 @@ auth_failed=Todennus epäonnistui: %v
 
 target_branch_not_exist=Kohde branchia ei ole olemassa.
 
+
 [user]
 change_avatar=Vaihda profiilikuvasi…
 repositories=Repot
@@ -1074,6 +1075,8 @@ activity.git_stats_and_deletions=ja
 activity.git_stats_deletion_1=%d poisto
 activity.git_stats_deletion_n=%d poistoa
 
+contributors.contribution_type.commits=Commitit
+
 search=Haku
 search.match=Osuma
 search.code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt.
@@ -1314,6 +1317,8 @@ topic.done=Valmis
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Organisaatio
 org_full_name_holder=Organisaation täydellinen nimi
@@ -1659,6 +1664,7 @@ notices.type_1=Repo
 notices.desc=Kuvaus
 notices.op=Toiminta
 
+
 [action]
 create_repo=luotu repo <a href="%s">%s</a>
 rename_repo=uudelleennimetty repo <code>%[1]s</code> nimelle <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index f3a264c1c8..628bc2a777 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -587,6 +587,7 @@ org_still_own_packages=Cette organisation possède encore un ou plusieurs paquet
 
 target_branch_not_exist=La branche cible n'existe pas.
 
+
 [user]
 change_avatar=Changer votre avatar…
 joined_on=Inscrit le %s
@@ -1965,6 +1966,8 @@ activity.git_stats_and_deletions=et
 activity.git_stats_deletion_1=%d suppression
 activity.git_stats_deletion_n=%d suppressions
 
+contributors.contribution_type.commits=Révisions
+
 search=Chercher
 search.search_repo=Rechercher dans le dépôt
 search.type.tooltip=Type de recherche
@@ -2564,6 +2567,8 @@ error.csv.too_large=Impossible de visualiser le fichier car il est trop volumine
 error.csv.unexpected=Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d.
 error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nom de l'organisation
 org_full_name_holder=Nom complet de l'organisation
@@ -3215,6 +3220,7 @@ notices.desc=Description
 notices.op=Opération
 notices.delete_success=Les informations systèmes ont été supprimées.
 
+
 [action]
 create_repo=a créé le dépôt <a href="%s">%s</a>
 rename_repo=a rebaptisé le dépôt <s><code>%[1]s</code></s> en <a href="%[2]s">%[3]s</a>
@@ -3399,6 +3405,8 @@ rpm.registry=Configurez ce registre à partir d'un terminal :
 rpm.distros.redhat=sur les distributions basées sur RedHat
 rpm.distros.suse=sur les distributions basées sur SUSE
 rpm.install=Pour installer le paquet, exécutez la commande suivante :
+rpm.repository=Informations sur le Dépôt
+rpm.repository.architectures=Architectures
 rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante :
 rubygems.install2=ou ajoutez-le au Gemfile :
 rubygems.dependencies.runtime=Dépendances d'exécution
@@ -3531,8 +3539,6 @@ runs.actors_no_select=Tous les acteurs
 runs.status_no_select=Touts les statuts
 runs.no_results=Aucun résultat correspondant.
 runs.no_workflows=Il n'y a pas encore de workflows.
-runs.no_workflows.quick_start=Vous ne savez pas comment commencer avec Gitea Action ? Consultez <a target="_blank" rel="noopener noreferrer" href="%s">le guide de démarrage rapide</a>.
-runs.no_workflows.documentation=Pour plus d’informations sur les Actions Gitea, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
 runs.no_runs=Le flux de travail n'a pas encore d'exécution.
 runs.empty_commit_message=(message de révision vide)
 
@@ -3551,7 +3557,6 @@ variables.none=Il n'y a pas encore de variables.
 variables.deletion=Retirer la variable
 variables.deletion.description=La suppression d’une variable est permanente et ne peut être défaite. Continuer ?
 variables.description=Les variables sont passées aux actions et ne peuvent être lues autrement.
-variables.id_not_exist=La variable numéro %d n’existe pas.
 variables.edit=Modifier la variable
 variables.deletion.failed=Impossible de retirer la variable.
 variables.deletion.success=La variable a bien été retirée.
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index aee4b44edf..901690d9a0 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -369,6 +369,7 @@ auth_failed=A hitelesítés sikertelen: %v
 
 target_branch_not_exist=Cél ág nem létezik.
 
+
 [user]
 change_avatar=Profilkép megváltoztatása…
 repositories=Tárolók
@@ -1053,6 +1054,8 @@ activity.git_stats_and_deletions=és
 activity.git_stats_deletion_1=%d törlés
 activity.git_stats_deletion_n=%d törlés
 
+contributors.contribution_type.commits=Commit-ok
+
 search=Keresés
 search.search_repo=Tároló keresés
 search.results=`"%s" találatok keresése itt: <a href="%s">%s</a>`
@@ -1168,6 +1171,8 @@ topic.done=Kész
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Szervezet neve
 org_full_name_holder=Szervezet teljes neve
@@ -1572,6 +1577,7 @@ notices.desc=Leírás
 notices.op=Op.
 notices.delete_success=A rendszer-értesítések törölve lettek.
 
+
 [action]
 create_repo=létrehozott tárolót: <a href="%s"> %s</a>
 rename_repo=átnevezte a(z) <code>%[1]s</code> tárolót <a href="%[2]s">%[3]s</a>-ra/re
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 4dd7c299df..1aee871b67 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -293,6 +293,7 @@ auth_failed=Otentikasi gagal: %v
 
 target_branch_not_exist=Target cabang tidak ada.
 
+
 [user]
 change_avatar=Ganti avatar anda…
 repositories=Repositori
@@ -838,6 +839,8 @@ activity.title.releases_n=%d Rilis
 activity.title.releases_published_by=%s dikeluarkan oleh %s
 activity.published_release_label=Dikeluarkan
 
+contributors.contribution_type.commits=Melakukan
+
 search=Cari
 search.search_repo=Cari repositori
 search.results=Cari hasil untuk "%s" dalam <a href="%s">%s</a>
@@ -953,6 +956,8 @@ branch.deleted_by=Dihapus oleh %s
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Nama Organisasi
 org_full_name_holder=Organisasi Nama Lengkap
@@ -1262,6 +1267,7 @@ notices.desc=Deskripsi
 notices.op=Op.
 notices.delete_success=Laporan sistem telah dihapus.
 
+
 [action]
 create_repo=repositori dibuat <a href="%s">%s</a>
 rename_repo=ganti nama gudang penyimpanan dari <code>%[1]s</code> ke <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index 2ba623dc12..f67541fe73 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -401,6 +401,7 @@ team_not_exist=Liðið er ekki til.
 
 
 
+
 [user]
 change_avatar=Breyttu notandamyndinni þinni…
 repositories=Hugbúnaðarsöfn
@@ -989,6 +990,8 @@ activity.git_stats_and_deletions=og
 activity.git_stats_deletion_1=%d eyðing
 activity.git_stats_deletion_n=%d eyðingar
 
+contributors.contribution_type.commits=Framlög
+
 search=Leita
 search.fuzzy=Óljóst
 search.code_no_results=Enginn samsvarandi frumkóði fannst eftur þínum leitarorðum.
@@ -1112,6 +1115,8 @@ topic.done=Í lagi
 
 
 
+[graphs]
+
 [org]
 repo_updated=Uppfært
 members=Meðlimar
@@ -1278,6 +1283,7 @@ notices.type_1=Hugbúnaðarsafn
 notices.type_2=Verkefni
 notices.desc=Lýsing
 
+
 [action]
 create_issue=`opnaði vandamál <a href="%[1]s">%[3]s#%[2]s</a>`
 reopen_issue=`enduropnaði vandamál <a href="%[1]s">%[3]s#%[2]s</a>`
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index a30232dd10..0e38c1ffb9 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -490,6 +490,7 @@ auth_failed=Autenticazione non riuscita: %v
 
 target_branch_not_exist=Il ramo (branch) di destinazione non esiste.
 
+
 [user]
 change_avatar=Modifica il tuo avatar…
 repositories=Repository
@@ -1623,6 +1624,8 @@ activity.git_stats_and_deletions=e
 activity.git_stats_deletion_1=%d cancellazione
 activity.git_stats_deletion_n=%d cancellazioni
 
+contributors.contribution_type.commits=Commit
+
 search=Ricerca
 search.search_repo=Ricerca repository
 search.fuzzy=Fuzzy
@@ -2117,6 +2120,8 @@ error.csv.too_large=Impossibile visualizzare questo file perché è troppo grand
 error.csv.unexpected=Impossibile visualizzare questo file perché contiene un carattere inatteso alla riga %d e alla colonna %d.
 error.csv.invalid_field_count=Impossibile visualizzare questo file perché ha un numero errato di campi alla riga %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nome dell'Organizzazione
 org_full_name_holder=Nome completo dell'organizzazione
@@ -2703,6 +2708,7 @@ notices.desc=Descrizione
 notices.op=Op.
 notices.delete_success=Gli avvisi di sistema sono stati eliminati.
 
+
 [action]
 create_repo=ha creato il repository <a href="%s">%s</a>
 rename_repo=repository rinominato da <code>%[1]s</code> a <a href="%[2]s">[3]s</a>
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 9216277955..5d9e21703e 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -588,6 +588,7 @@ org_still_own_packages=組織はまだ1つ以上のパッケージを所有し
 
 target_branch_not_exist=ターゲットのブランチが存在していません。
 
+
 [user]
 change_avatar=アバターを変更…
 joined_on=%sに登録
@@ -1966,6 +1967,8 @@ activity.git_stats_and_deletions=、
 activity.git_stats_deletion_1=%d行削除
 activity.git_stats_deletion_n=%d行削除
 
+contributors.contribution_type.commits=コミット
+
 search=検索
 search.search_repo=リポジトリを検索
 search.type.tooltip=検索タイプ
@@ -2565,6 +2568,8 @@ error.csv.too_large=このファイルは大きすぎるため表示できませ
 error.csv.unexpected=このファイルは %d 行目の %d 文字目に予期しない文字が含まれているため表示できません。
 error.csv.invalid_field_count=このファイルは %d 行目のフィールドの数が正しくないため表示できません。
 
+[graphs]
+
 [org]
 org_name_holder=組織名
 org_full_name_holder=組織のフルネーム
@@ -3216,6 +3221,7 @@ notices.desc=説明
 notices.op=操作
 notices.delete_success=システム通知を削除しました。
 
+
 [action]
 create_repo=がリポジトリ <a href="%s">%s</a> を作成しました
 rename_repo=がリポジトリ名を <code>%[1]s</code> から <a href="%[2]s">%[3]s</a> へ変更しました
@@ -3400,6 +3406,8 @@ rpm.registry=このレジストリをコマンドラインからセットアッ
 rpm.distros.redhat=RedHat系ディストリビューションの場合
 rpm.distros.suse=SUSE系ディストリビューションの場合
 rpm.install=パッケージをインストールするには、次のコマンドを実行します:
+rpm.repository=リポジトリ情報
+rpm.repository.architectures=Architectures
 rubygems.install=gem を使用してパッケージをインストールするには、次のコマンドを実行します:
 rubygems.install2=または Gemfile に追加します:
 rubygems.dependencies.runtime=実行用依存関係
@@ -3532,8 +3540,6 @@ runs.actors_no_select=すべてのアクター
 runs.status_no_select=すべてのステータス
 runs.no_results=一致する結果はありません。
 runs.no_workflows=ワークフローはまだありません。
-runs.no_workflows.quick_start=Gitea Action の始め方がわからない? <a target="_blank" rel="noopener noreferrer" href="%s">クイックスタートガイド</a>をご覧ください。
-runs.no_workflows.documentation=Gitea Action の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。
 runs.no_runs=ワークフローはまだ実行されていません。
 runs.empty_commit_message=(空のコミットメッセージ)
 
@@ -3552,7 +3558,6 @@ variables.none=変数はまだありません。
 variables.deletion=変数を削除
 variables.deletion.description=変数の削除は恒久的で元に戻すことはできません。 続行しますか?
 variables.description=変数は特定のActionsに渡されます。 それ以外で読み出されることはありません。
-variables.id_not_exist=idが%dの変数は存在しません。
 variables.edit=変数の編集
 variables.deletion.failed=変数を削除できませんでした。
 variables.deletion.success=変数を削除しました。
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index 1c79ee6bc7..ed0bb897c4 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -349,6 +349,7 @@ auth_failed=인증 실패: %v
 
 target_branch_not_exist=대상 브랜치가 존재하지 않습니다.
 
+
 [user]
 change_avatar=아바타 변경
 repositories=저장소
@@ -949,6 +950,8 @@ activity.title.releases_n=%d 개의 릴리즈
 activity.title.releases_published_by=%s 가 %s 에 의하여 배포되었습니다.
 activity.published_release_label=배포됨
 
+contributors.contribution_type.commits=커밋
+
 search=검색
 search.search_repo=저장소 검색
 search.results="<a href=\"%s\">%s</a> 에서 \"%s\" 에 대한 검색 결과"
@@ -1161,6 +1164,8 @@ topic.count_prompt=25개 이상의 토픽을 선택하실 수 없습니다.
 
 
 
+[graphs]
+
 [org]
 org_name_holder=조직 이름
 org_full_name_holder=조직 전체 이름
@@ -1521,6 +1526,7 @@ notices.desc=설명
 notices.op=일.
 notices.delete_success=시스템 알림이 삭제되었습니다.
 
+
 [action]
 create_repo=저장소를 만들었습니다. <a href="%s">%s</a>
 rename_repo=<code>%[1]s에서</code>에서 <a href="%[2]s"> %[3]s</a>으로 저장소 이름을 바꾸었습니다.
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index d4a8740f79..96db89d810 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -588,6 +588,7 @@ org_still_own_packages=Šai organizācijai pieder viena vai vārākas pakotnes,
 
 target_branch_not_exist=Mērķa atzars neeksistē
 
+
 [user]
 change_avatar=Mainīt profila attēlu…
 joined_on=Pievienojās %s
@@ -1966,6 +1967,8 @@ activity.git_stats_and_deletions=un
 activity.git_stats_deletion_1=%d dzēšana
 activity.git_stats_deletion_n=%d dzēšanas
 
+contributors.contribution_type.commits=Revīzijas
+
 search=Meklēt
 search.search_repo=Meklēšana repozitorijā
 search.type.tooltip=Meklēšanas veids
@@ -2565,6 +2568,8 @@ error.csv.too_large=Nevar attēlot šo failu, jo tas ir pārāk liels.
 error.csv.unexpected=Nevar attēlot šo failu, jo tas satur neparedzētu simbolu %d. līnijas %d. kolonnā.
 error.csv.invalid_field_count=Nevar attēlot šo failu, jo tas satur nepareizu skaitu ar laukiem %d. līnijā.
 
+[graphs]
+
 [org]
 org_name_holder=Organizācijas nosaukums
 org_full_name_holder=Organizācijas pilnais nosaukums
@@ -3216,6 +3221,7 @@ notices.desc=Apraksts
 notices.op=Op.
 notices.delete_success=Sistēmas paziņojumi ir dzēsti.
 
+
 [action]
 create_repo=izveidoja repozitoriju <a href="%s">%s</a>
 rename_repo=pārsauca repozitoriju no <code>%[1]s</code> uz <a href="%[2]s">%[3]s</a>
@@ -3400,6 +3406,8 @@ rpm.registry=Konfigurējiet šo reģistru no komandrindas:
 rpm.distros.redhat=uz RedHat balstītās operētājsistēmās
 rpm.distros.suse=uz SUSE balstītās operētājsistēmās
 rpm.install=Lai uzstādītu pakotni, ir jāizpilda šī komanda:
+rpm.repository=Repozitorija informācija
+rpm.repository.architectures=Arhitektūras
 rubygems.install=Lai instalētu gem pakotni, izpildiet sekojošu komandu:
 rubygems.install2=vai pievienojiet Gemfile:
 rubygems.dependencies.runtime=Izpildlaika atkarības
@@ -3532,8 +3540,6 @@ runs.actors_no_select=Visi aktori
 runs.status_no_select=Visi stāvokļi
 runs.no_results=Netika atrasts nekas atbilstošs.
 runs.no_workflows=Vēl nav nevienas darbplūsmas.
-runs.no_workflows.quick_start=Nav skaidrs, kā sākt izmantot Gitea darbības? Skatīt <a target="_blank" rel="noopener noreferrer" href="%s">ātrās sākšanas norādes</a>.
-runs.no_workflows.documentation=Vairāk informācijas par Gitea darbībām ir skatāma <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>.
 runs.no_runs=Darbplūsmai vēl nav nevienas izpildes.
 runs.empty_commit_message=(tukšs revīzijas ziņojums)
 
@@ -3552,7 +3558,6 @@ variables.none=Vēl nav neviena mainīgā.
 variables.deletion=Noņemt mainīgo
 variables.deletion.description=Mainīgā noņemšana ir neatgriezeniska un nav atsaucama. Vai turpināt?
 variables.description=Mainīgie tiks padoti noteiktām darbībām, un citādāk tos nevar nolasīt.
-variables.id_not_exist=Mainīgais ar identifikatoru %d neeksistē.
 variables.edit=Labot mainīgo
 variables.deletion.failed=Neizdevās noņemt mainīgo.
 variables.deletion.success=Mainīgais tika noņemts.
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 43265c9c31..fc1da2b992 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -489,6 +489,7 @@ auth_failed=Verificatie mislukt: %v
 
 target_branch_not_exist=Doel branch bestaat niet
 
+
 [user]
 change_avatar=Wijzig je profielfoto…
 repositories=repositories
@@ -1618,6 +1619,8 @@ activity.git_stats_and_deletions=en
 activity.git_stats_deletion_1=%d verwijdering
 activity.git_stats_deletion_n=%d verwijderingen
 
+contributors.contribution_type.commits=Commits
+
 search=Zoek
 search.search_repo=Zoek repository
 search.fuzzy=Vergelijkbaar
@@ -2031,6 +2034,8 @@ topic.count_prompt=Je kunt niet meer dan 25 onderwerpen selecteren
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Organisatienaam
 org_full_name_holder=Volledige naam organisatie
@@ -2537,6 +2542,7 @@ notices.desc=Beschrijving
 notices.op=Op.
 notices.delete_success=De systeemmeldingen zijn verwijderd.
 
+
 [action]
 create_repo=repository aangemaakt in <a href="%s">%s</a>
 rename_repo=hernoemde repository van <code>%[1]s</code> naar <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index d713110a72..2af3ce1a11 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -474,6 +474,7 @@ auth_failed=Uwierzytelnienie się nie powiodło: %v
 
 target_branch_not_exist=Gałąź docelowa nie istnieje.
 
+
 [user]
 change_avatar=Zmień swój awatar…
 repositories=Repozytoria
@@ -1467,6 +1468,8 @@ activity.git_stats_and_deletions=i
 activity.git_stats_deletion_1=%d usunięcie
 activity.git_stats_deletion_n=%d usunięć
 
+contributors.contribution_type.commits=Commity
+
 search=Szukaj
 search.search_repo=Przeszukaj repozytorium
 search.fuzzy=Fuzzy
@@ -1899,6 +1902,8 @@ error.csv.too_large=Nie można wyświetlić tego pliku, ponieważ jest on zbyt d
 error.csv.unexpected=Nie można renderować tego pliku, ponieważ zawiera nieoczekiwany znak w wierszu %d i kolumnie %d.
 error.csv.invalid_field_count=Nie można renderować tego pliku, ponieważ ma nieprawidłową liczbę pól w wierszu %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nazwa organizacji
 org_full_name_holder=Pełna nazwa organizacji
@@ -2425,6 +2430,7 @@ notices.desc=Opis
 notices.op=Operacja
 notices.delete_success=Powiadomienia systemu zostały usunięte.
 
+
 [action]
 create_repo=tworzy repozytorium <a href="%s">%s</a>
 rename_repo=zmienia nazwę repozytorium <code>%[1]s</code> na <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index cf5fd0055c..11743f29a5 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -582,6 +582,7 @@ org_still_own_packages=Esta organização ainda possui pacotes, exclua-os primei
 
 target_branch_not_exist=O branch de destino não existe.
 
+
 [user]
 change_avatar=Altere seu avatar...
 joined_on=Inscreveu-se em %s
@@ -1927,6 +1928,8 @@ activity.git_stats_and_deletions=e
 activity.git_stats_deletion_1=%d exclusão
 activity.git_stats_deletion_n=%d exclusões
 
+contributors.contribution_type.commits=Commits
+
 search=Pesquisar
 search.search_repo=Pesquisar no repositório...
 search.type.tooltip=Tipo de pesquisa
@@ -2484,6 +2487,8 @@ error.csv.too_large=Não é possível renderizar este arquivo porque ele é muit
 error.csv.unexpected=Não é possível renderizar este arquivo porque ele contém um caractere inesperado na linha %d e coluna %d.
 error.csv.invalid_field_count=Não é possível renderizar este arquivo porque ele tem um número errado de campos na linha %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nome da organização
 org_full_name_holder=Nome completo da organização
@@ -3110,6 +3115,7 @@ notices.desc=Descrição
 notices.op=Op.
 notices.delete_success=Os avisos do sistema foram excluídos.
 
+
 [action]
 create_repo=criou o repositório <a href="%s">%s</a>
 rename_repo=renomeou o repositório <code>%[1]s</code> para <a href="%[2]s">%[3]s</a>
@@ -3293,6 +3299,8 @@ rpm.registry=Configure este registro pela linha de comando:
 rpm.distros.redhat=em distribuições baseadas no RedHat
 rpm.distros.suse=em distribuições baseadas no SUSE
 rpm.install=Para instalar o pacote, execute o seguinte comando:
+rpm.repository=Informações do repositório
+rpm.repository.architectures=Arquiteturas
 rubygems.install=Para instalar o pacote usando gem, execute o seguinte comando:
 rubygems.install2=ou adicione-o ao Gemfile:
 rubygems.dependencies.runtime=Dependências de Execução
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 863a1545c3..99165ed332 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -123,6 +123,7 @@ pin=Fixar
 unpin=Desafixar
 
 artifacts=Artefactos
+confirm_delete_artifact=Tem a certeza que quer eliminar este artefacto "%s"?
 
 archived=Arquivado
 
@@ -423,6 +424,7 @@ authorization_failed_desc=A autorização falhou porque encontrámos um pedido i
 sspi_auth_failed=Falhou a autenticação SSPI
 password_pwned=A senha utilizada está numa <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">lista de senhas roubadas</a> anteriormente expostas em fugas de dados públicas. Tente novamente com uma senha diferente e considere também mudar esta senha nos outros sítios.
 password_pwned_err=Não foi possível completar o pedido ao HaveIBeenPwned
+last_admin=Não pode remover o último administrador. Tem que existir pelo menos um administrador.
 
 [mail]
 view_it_on=Ver em %s
@@ -588,6 +590,8 @@ org_still_own_packages=Esta organização ainda possui um ou mais pacotes, elimi
 
 target_branch_not_exist=O ramo de destino não existe.
 
+admin_cannot_delete_self=Não se pode auto-remover quando tem privilégios de administração. Remova esses privilégios primeiro.
+
 [user]
 change_avatar=Mude o seu avatar…
 joined_on=Inscreveu-se em %s
@@ -967,6 +971,8 @@ issue_labels_helper=Escolha um conjunto de rótulos para as questões.
 license=Licença
 license_helper=Escolha um ficheiro de licença.
 license_helper_desc=Uma licença rege o que os outros podem, ou não, fazer com o seu código fonte. Não tem a certeza sobre qual a mais indicada para o seu trabalho? Veja: <a target="_blank" rel="noopener noreferrer" href="%s">Escolher uma licença.</a>
+object_format=Formato dos elementos
+object_format_helper=Formato dos elementos do repositório. Não poderá ser alterado mais tarde. SHA1 é o mais compatível.
 readme=README
 readme_helper=Escolha um modelo de ficheiro README.
 readme_helper_desc=Este é o sítio onde pode escrever uma descrição completa do seu trabalho.
@@ -984,6 +990,7 @@ mirror_prune=Podar
 mirror_prune_desc=Remover referências obsoletas de seguimento remoto
 mirror_interval=Intervalo entre sincronizações (as unidades de tempo válidas são 'h', 'm' e 's'). O valor zero desabilita a sincronização periódica. (Intervalo mínimo: %s)
 mirror_interval_invalid=O intervalo entre sincronizações não é válido.
+mirror_sync=sincronizado
 mirror_sync_on_commit=Sincronizar quando forem enviados cometimentos
 mirror_address=Clonar a partir do URL
 mirror_address_desc=Coloque, na secção de autorização, as credenciais que, eventualmente, sejam necessárias.
@@ -1034,6 +1041,7 @@ desc.public=Público
 desc.template=Modelo
 desc.internal=Interno
 desc.archived=Arquivado
+desc.sha256=SHA256
 
 template.items=Itens do modelo
 template.git_content=Conteúdo Git (ramo principal)
@@ -1184,6 +1192,8 @@ audio_not_supported_in_browser=O seu navegador não suporta a etiqueta 'audio' d
 stored_lfs=Armazenado com Git LFS
 symbolic_link=Ligação simbólica
 executable_file=Ficheiro executável
+vendored=Externo
+generated=Gerado
 commit_graph=Gráfico de cometimentos
 commit_graph.select=Escolher ramos
 commit_graph.hide_pr_refs=Ocultar pedidos de integração
@@ -1707,6 +1717,7 @@ pulls.select_commit_hold_shift_for_range=Escolha o comentimento. Mantenha premid
 pulls.review_only_possible_for_full_diff=A revisão só é possível ao visualizar o diff completo
 pulls.filter_changes_by_commit=Filtrar por cometimento
 pulls.nothing_to_compare=Estes ramos são iguais. Não há necessidade de criar um pedido de integração.
+pulls.nothing_to_compare_have_tag=O ramo/etiqueta escolhidos são iguais.
 pulls.nothing_to_compare_and_allow_empty_pr=Estes ramos são iguais. Este pedido de integração ficará vazio.
 pulls.has_pull_request=`Já existe um pedido de integração entre estes ramos: <a href="%[1]s">%[2]s#%[3]d</a>`
 pulls.create=Criar um pedido de integração
@@ -1765,6 +1776,7 @@ pulls.merge_pull_request=Criar um cometimento de integração
 pulls.rebase_merge_pull_request=Mudar a base e avançar rapidamente
 pulls.rebase_merge_commit_pull_request=Mudar a base e criar um cometimento de integração
 pulls.squash_merge_pull_request=Criar cometimento de compactação
+pulls.fast_forward_only_merge_pull_request=Avançar rapidamente apenas
 pulls.merge_manually=Integrado manualmente
 pulls.merge_commit_id=O ID de cometimento da integração
 pulls.require_signed_wont_sign=O ramo requer que os cometimentos sejam assinados mas esta integração não vai ser assinada
@@ -1901,6 +1913,8 @@ wiki.page_name_desc=Insira um nome para esta página Wiki. Alguns dos nomes espe
 wiki.original_git_entry_tooltip=Ver o ficheiro Git original, ao invés de usar uma ligação amigável.
 
 activity=Trabalho
+activity.navbar.pulse=Pulso
+activity.navbar.contributors=Contribuidores
 activity.period.filter_label=Período:
 activity.period.daily=1 dia
 activity.period.halfweekly=3 dias
@@ -1966,6 +1980,11 @@ activity.git_stats_and_deletions=e
 activity.git_stats_deletion_1=%d eliminação
 activity.git_stats_deletion_n=%d eliminações
 
+contributors.contribution_type.filter_label=Tipo de contribuição:
+contributors.contribution_type.commits=Cometimentos
+contributors.contribution_type.additions=Adições
+contributors.contribution_type.deletions=Eliminações
+
 search=Procurar
 search.search_repo=Procurar repositório
 search.type.tooltip=Tipo de pesquisa
@@ -2003,6 +2022,7 @@ settings.mirror_settings.docs.doc_link_title=Como é que eu replico repositório
 settings.mirror_settings.docs.doc_link_pull_section=a parte "Puxar de um repositório remoto" da documentação.
 settings.mirror_settings.docs.pulling_remote_title=Puxando a partir de um repositório remoto
 settings.mirror_settings.mirrored_repository=Repositório replicado
+settings.mirror_settings.pushed_repository=Repositório enviado
 settings.mirror_settings.direction=Sentido
 settings.mirror_settings.direction.pull=Puxada
 settings.mirror_settings.direction.push=Envio
@@ -2312,6 +2332,8 @@ settings.protect_approvals_whitelist_users=Revisores com permissão:
 settings.protect_approvals_whitelist_teams=Equipas com permissão para rever:
 settings.dismiss_stale_approvals=Descartar aprovações obsoletas
 settings.dismiss_stale_approvals_desc=Quando novos cometimentos que mudam o conteúdo do pedido de integração forem enviados para o ramo, as aprovações antigas serão descartadas.
+settings.ignore_stale_approvals=Ignorar aprovações obsoletas
+settings.ignore_stale_approvals_desc=Não contar as aprovações feitas em cometimentos mais antigos (revisões obsoletas) para o número de aprovações do pedido de integração. É irrelevante se as revisões obsoletas já forem descartadas.
 settings.require_signed_commits=Exigir cometimentos assinados
 settings.require_signed_commits_desc=Rejeitar envios para este ramo que não estejam assinados ou que não sejam validáveis.
 settings.protect_branch_name_pattern=Padrão do nome do ramo protegido
@@ -2367,6 +2389,7 @@ settings.archive.error=Ocorreu um erro enquanto decorria o processo de arquivo d
 settings.archive.error_ismirror=Não pode arquivar um repositório que tenha sido replicado.
 settings.archive.branchsettings_unavailable=As configurações dos ramos não estão disponíveis quando o repositório está arquivado.
 settings.archive.tagsettings_unavailable=As configurações sobre etiquetas não estão disponíveis quando o repositório está arquivado.
+settings.archive.mirrors_unavailable=As réplicas não estão disponíveis se o repositório estiver arquivado.
 settings.unarchive.button=Desarquivar repositório
 settings.unarchive.header=Desarquivar este repositório
 settings.unarchive.text=Desarquivar o repositório irá restaurar a capacidade de receber cometimentos e envios, assim como novas questões e pedidos de integração.
@@ -2565,6 +2588,13 @@ error.csv.too_large=Não é possível apresentar este ficheiro por ser demasiado
 error.csv.unexpected=Não é possível apresentar este ficheiro porque contém um caractere inesperado na linha %d e coluna %d.
 error.csv.invalid_field_count=Não é possível apresentar este ficheiro porque tem um número errado de campos na linha %d.
 
+[graphs]
+component_loading=A carregar %s...
+component_loading_failed=Não foi possível carregar %s
+component_loading_info=Isto pode demorar um pouco…
+component_failed_to_load=Ocorreu um erro inesperado.
+contributors.what=contribuições
+
 [org]
 org_name_holder=Nome da organização
 org_full_name_holder=Nome completo da organização
@@ -2691,6 +2721,7 @@ teams.invite.description=Clique no botão abaixo para se juntar à equipa.
 
 [admin]
 dashboard=Painel de controlo
+self_check=Auto-verificação
 identity_access=Identidade e acesso
 users=Contas de utilizador
 organizations=Organizações
@@ -2736,6 +2767,7 @@ dashboard.delete_missing_repos=Eliminar todos os repositórios que não tenham o
 dashboard.delete_missing_repos.started=Foi iniciada a tarefa de eliminação de todos os repositórios que não têm ficheiros git.
 dashboard.delete_generated_repository_avatars=Eliminar avatares gerados do repositório
 dashboard.sync_repo_branches=Sincronizar ramos perdidos de dados do git para bases de dados
+dashboard.sync_repo_tags=Sincronizar etiquetas dos dados do git para a base de dados
 dashboard.update_mirrors=Sincronizar réplicas
 dashboard.repo_health_check=Verificar a saúde de todos os repositórios
 dashboard.check_repo_stats=Verificar as estatísticas de todos os repositórios
@@ -2790,6 +2822,7 @@ dashboard.stop_endless_tasks=Parar tarefas intermináveis
 dashboard.cancel_abandoned_jobs=Cancelar trabalhos abandonados
 dashboard.start_schedule_tasks=Iniciar tarefas de agendamento
 dashboard.sync_branch.started=Sincronização de ramos iniciada
+dashboard.sync_tag.started=Sincronização de etiquetas iniciada
 dashboard.rebuild_issue_indexer=Reconstruir indexador de questões
 
 users.user_manage_panel=Gestão das contas de utilizadores
@@ -3216,6 +3249,13 @@ notices.desc=Descrição
 notices.op=Op.
 notices.delete_success=As notificações do sistema foram eliminadas.
 
+self_check.no_problem_found=Nenhum problema encontrado até agora.
+self_check.database_collation_mismatch=Supor que a base de dados usa a colação: %s
+self_check.database_collation_case_insensitive=A base de dados está a usar a colação %s, que é insensível à diferença entre maiúsculas e minúsculas. Embora o Gitea possa trabalhar com ela, pode haver alguns casos raros que não funcionem como esperado.
+self_check.database_inconsistent_collation_columns=A base de dados está a usar a colação %s, mas estas colunas estão a usar colações diferentes. Isso poderá causar alguns problemas inesperados.
+self_check.database_fix_mysql=Para utilizadores do MySQL/MariaDB, pode usar o comando "gitea doctor convert" para resolver os problemas de colação. Também pode resolver o problema com comandos SQL "ALTER ... COLLATE ..." aplicados manualmente.
+self_check.database_fix_mssql=Para utilizadores do MSSQL só pode resolver o problema aplicando comandos SQL "ALTER ... COLLATE ..." manualmente, por enquanto.
+
 [action]
 create_repo=criou o repositório <a href="%s">%s</a>
 rename_repo=renomeou o repositório de <code>%[1]s</code> para <a href="%[2]s">%[3]s</a>
@@ -3400,6 +3440,9 @@ rpm.registry=Configurar este registo usando a linha de comandos:
 rpm.distros.redhat=em distribuições baseadas no RedHat
 rpm.distros.suse=em distribuições baseadas no SUSE
 rpm.install=Para instalar o pacote, execute o seguinte comando:
+rpm.repository=Informação do repositório
+rpm.repository.architectures=Arquitecturas
+rpm.repository.multiple_groups=Este pacote está disponível em vários grupos.
 rubygems.install=Para instalar o pacote usando o gem, execute o seguinte comando:
 rubygems.install2=ou adicione-o ao ficheiro <code>Gemfile</code>:
 rubygems.dependencies.runtime=Dependências do tempo de execução (runtime)
@@ -3532,8 +3575,8 @@ runs.actors_no_select=Todos os intervenientes
 runs.status_no_select=Todos os estados
 runs.no_results=Nenhum resultado obtido.
 runs.no_workflows=Ainda não há sequências de trabalho.
-runs.no_workflows.quick_start=Não sabe como começar com o Gitea Action? Veja o <a target="_blank" rel="noopener noreferrer" href="%s">guia de iniciação rápida</a>.
-runs.no_workflows.documentation=Para mais informação sobre o Gitea Action, veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>.
+runs.no_workflows.quick_start=Não sabe como começar com o Gitea Actions? Veja o <a target="_blank" rel="noopener noreferrer" href="%s">guia de inicio rápido</a>.
+runs.no_workflows.documentation=Para mais informação sobre o Gitea Actions veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>.
 runs.no_runs=A sequência de trabalho ainda não foi executada.
 runs.empty_commit_message=(mensagem de cometimento vazia)
 
@@ -3552,7 +3595,7 @@ variables.none=Ainda não há variáveis.
 variables.deletion=Remover variável
 variables.deletion.description=Remover uma variável é permanente e não pode ser revertido. Quer continuar?
 variables.description=As variáveis serão transmitidas a certas operações e não poderão ser lidas de outra forma.
-variables.id_not_exist=A variável com o id %d não existe.
+variables.id_not_exist=A variável com o ID %d não existe.
 variables.edit=Editar variável
 variables.deletion.failed=Falha ao remover a variável.
 variables.deletion.success=A variável foi removida.
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 0a466854d0..36b9d0e39e 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -586,6 +586,7 @@ org_still_own_packages=Эта организация всё ещё владее
 
 target_branch_not_exist=Целевая ветка не существует.
 
+
 [user]
 change_avatar=Изменить свой аватар…
 joined_on=Присоединил(ся/ась) %s
@@ -1926,6 +1927,8 @@ activity.git_stats_and_deletions=и
 activity.git_stats_deletion_1=%d удаление
 activity.git_stats_deletion_n=%d удалений
 
+contributors.contribution_type.commits=коммитов
+
 search=Поиск
 search.search_repo=Поиск по репозиторию
 search.type.tooltip=Тип поиска
@@ -2515,6 +2518,8 @@ error.csv.too_large=Не удается отобразить этот файл,
 error.csv.unexpected=Не удается отобразить этот файл, потому что он содержит неожиданный символ в строке %d и столбце %d.
 error.csv.invalid_field_count=Не удается отобразить этот файл, потому что он имеет неправильное количество полей в строке %d.
 
+[graphs]
+
 [org]
 org_name_holder=Название организации
 org_full_name_holder=Полное название организации
@@ -3153,6 +3158,7 @@ notices.desc=Описание
 notices.op=Oп.
 notices.delete_success=Уведомления системы были удалены.
 
+
 [action]
 create_repo=создал(а) репозиторий <a href="%s"> %s</a>
 rename_repo=переименовал(а) репозиторий из <code>%[1]s</code> на <a href="%[2]s">%[3]s</a>
@@ -3337,6 +3343,8 @@ rpm.registry=Настроить реестр из командной строк
 rpm.distros.redhat=на дистрибутивах семейства RedHat
 rpm.distros.suse=на дистрибутивах семейства SUSE
 rpm.install=Чтобы установить пакет, выполните следующую команду:
+rpm.repository=О репозитории
+rpm.repository.architectures=Архитектуры
 rubygems.install=Чтобы установить пакет с помощью gem, выполните следующую команду:
 rubygems.install2=или добавьте его в Gemfile:
 rubygems.dependencies.runtime=Зависимости времени выполнения
@@ -3464,8 +3472,6 @@ runs.status=Статус
 runs.actors_no_select=Все акторы
 runs.no_results=Ничего не найдено.
 runs.no_workflows=Пока нет рабочих процессов.
-runs.no_workflows.quick_start=Не знаете, как начать использовать Действия Gitea? Читайте <a target="_blank" rel="noopener noreferrer" href="%s">руководство по быстрому старту</a>.
-runs.no_workflows.documentation=Чтобы узнать больше о Действиях Gitea, читайте <a target="_blank" rel="noopener noreferrer" href="%s">документацию</a>.
 runs.no_runs=Рабочий поток ещё не запускался.
 runs.empty_commit_message=(пустое сообщение коммита)
 
@@ -3484,7 +3490,6 @@ variables.none=Переменных пока нет.
 variables.deletion=Удалить переменную
 variables.deletion.description=Удаление переменной необратимо, его нельзя отменить. Продолжить?
 variables.description=Переменные будут передаваться определенным действиям и не могут быть прочитаны иначе.
-variables.id_not_exist=Переменная с идентификатором %d не существует.
 variables.edit=Изменить переменную
 variables.deletion.failed=Не удалось удалить переменную.
 variables.deletion.success=Переменная удалена.
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 6d70bc385a..fb97daf5e0 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -450,6 +450,7 @@ auth_failed=සත්යාපන අසමත් විය: %v
 
 target_branch_not_exist=ඉලක්කගත ශාඛාව නොපවතී.
 
+
 [user]
 change_avatar=ඔබගේ අවතාරය වෙනස් කරන්න…
 repositories=කෝෂ්ඨ
@@ -1459,6 +1460,8 @@ activity.git_stats_and_deletions=සහ
 activity.git_stats_deletion_1=%d මකාදැමීම
 activity.git_stats_deletion_n=%d මකාදැමීම්
 
+contributors.contribution_type.commits=විවරයන්
+
 search=සොයන්න
 search.search_repo=කෝෂ්ඨය සොයන්න
 search.fuzzy=සිනිඳු
@@ -1910,6 +1913,8 @@ error.csv.too_large=එය ඉතා විශාල නිසා මෙම ග
 error.csv.unexpected=%d පේළියේ සහ %dතීරුවේ අනපේක්ෂිත චරිතයක් අඩංගු බැවින් මෙම ගොනුව විදැහුම්කරණය කළ නොහැක.
 error.csv.invalid_field_count=මෙම ගොනුව රේඛාවේ වැරදි ක්ෂේත්ර සංඛ්යාවක් ඇති බැවින් එය විදැහුම්කරණය කළ නොහැක %d.
 
+[graphs]
+
 [org]
 org_name_holder=සංවිධානයේ නම
 org_full_name_holder=සංවිධානයේ සම්පූර්ණ නම
@@ -2456,6 +2461,7 @@ notices.desc=සවිස්තරය
 notices.op=ඔප්.
 notices.delete_success=පද්ධති දැන්වීම් මකා දමා ඇත.
 
+
 [action]
 create_repo=නිර්මිත ගබඩාව <a href="%s">%s</a>
 rename_repo=<code>%[1]s</code> සිට <a href="%[2]s">%[3]s</a>දක්වා නම් කරන ලද ගබඩාව
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index 1c3ca5ae43..4a223ee90d 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -563,6 +563,7 @@ auth_failed=Overenie zlyhalo: %v
 
 target_branch_not_exist=Cieľová vetva neexistuje.
 
+
 [user]
 change_avatar=Zmeniť svoj avatar…
 joined_on=Pripojil/a sa %s
@@ -1144,6 +1145,8 @@ activity.unresolved_conv_label=Otvoriť
 activity.git_stats_commit_1=%d commit
 activity.git_stats_commit_n=%d commity
 
+contributors.contribution_type.commits=Commitov
+
 search=Hľadať
 search.type.tooltip=Typ vyhľadávania
 search.fuzzy=Fuzzy
@@ -1246,6 +1249,8 @@ release.cancel=Zrušiť
 
 
 
+[graphs]
+
 [org]
 code=Kód
 lower_repositories=repozitáre
@@ -1328,6 +1333,7 @@ monitor.process.cancel=Zrušiť proces
 
 
 
+
 [action]
 compare_commits=Porovnať %d commitov
 compare_commits_general=Porovnať commity
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 411a83ed75..0a484c9b79 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -390,6 +390,7 @@ auth_failed=Autentisering misslyckades: %v
 
 target_branch_not_exist=Målgrenen finns inte.
 
+
 [user]
 change_avatar=Byt din avatar…
 repositories=Utvecklingskataloger
@@ -1227,6 +1228,8 @@ activity.git_stats_and_deletions=och
 activity.git_stats_deletion_1=%d borttagen
 activity.git_stats_deletion_n=%d borttagningar
 
+contributors.contribution_type.commits=Incheckningar
+
 search=Sök
 search.search_repo=Sök utvecklingskatalog
 search.results=Sökresultat för ”%s” i <a href="%s"> %s</a>
@@ -1535,6 +1538,8 @@ topic.count_prompt=Du kan inte välja fler än 25 ämnen
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Organisationsnamn
 org_full_name_holder=Organisationens Fullständiga Namn
@@ -1971,6 +1976,7 @@ notices.desc=Beskrivning
 notices.op=Op.
 notices.delete_success=Systemnotifikationer har blivit raderade.
 
+
 [action]
 create_repo=skapade utvecklingskatalog <a href="%s"> %s</a>
 rename_repo=döpte om utvecklingskalatogen från <code>%[1]s</code> till <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index ea028657db..3c8cb08726 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -588,6 +588,7 @@ org_still_own_packages=Bu organizasyon hala bir veya daha fazla pakete sahip, ö
 
 target_branch_not_exist=Hedef dal mevcut değil.
 
+
 [user]
 change_avatar=Profil resmini değiştir…
 joined_on=%s tarihinde katıldı
@@ -1966,6 +1967,8 @@ activity.git_stats_and_deletions=ve
 activity.git_stats_deletion_1=%d silme oldu
 activity.git_stats_deletion_n=%d silme oldu
 
+contributors.contribution_type.commits=İşleme
+
 search=Ara
 search.search_repo=Depo ara
 search.type.tooltip=Arama türü
@@ -2565,6 +2568,8 @@ error.csv.too_large=Bu dosya çok büyük olduğu için işlenemiyor.
 error.csv.unexpected=%d satırı ve %d sütununda beklenmeyen bir karakter içerdiğinden bu dosya işlenemiyor.
 error.csv.invalid_field_count=%d satırında yanlış sayıda alan olduğundan bu dosya işlenemiyor.
 
+[graphs]
+
 [org]
 org_name_holder=Organizasyon Adı
 org_full_name_holder=Organizasyon Tam Adı
@@ -3216,6 +3221,7 @@ notices.desc=Açıklama
 notices.op=İşlem
 notices.delete_success=Sistem bildirimleri silindi.
 
+
 [action]
 create_repo=depo <a href="%s">%s</a> oluşturuldu
 rename_repo=<code>%[1]s</code> olan depo adını <a href="%[2]s">%[3]s</a> buna çevirdi
@@ -3400,6 +3406,8 @@ rpm.registry=Bu kütüğü komut satırını kullanarak kurun:
 rpm.distros.redhat=RedHat tabanlı dağıtımlarda
 rpm.distros.suse=SUSE tabanlı dağıtımlarda
 rpm.install=Paketi kurmak için, aşağıdaki komutu çalıştırın:
+rpm.repository=Depo Bilgisi
+rpm.repository.architectures=Mimariler
 rubygems.install=Paketi gem ile kurmak için, şu komutu çalıştırın:
 rubygems.install2=veya paketi Gemfile dosyasına ekleyin:
 rubygems.dependencies.runtime=Çalışma Zamanı Bağımlılıkları
@@ -3532,8 +3540,6 @@ runs.actors_no_select=Tüm aktörler
 runs.status_no_select=Tüm durumlar
 runs.no_results=Eşleşen sonuç yok.
 runs.no_workflows=Henüz hiç bir iş akışı yok.
-runs.no_workflows.quick_start=Gitea İşlem'i nasıl başlatacağınızı bilmiyor musunuz? <a target="_blank" rel="noopener noreferrer" href="%s">Hızlı başlangıç rehberine</a> bakabilirsiniz.
-runs.no_workflows.documentation=Gitea İşlem'i hakkında daha fazla bilgi için, <a target="_blank" rel="noopener noreferrer" href="%s">belgeye</a> bakabilirsiniz.
 runs.no_runs=İş akışı henüz hiç çalıştırılmadı.
 runs.empty_commit_message=(boş işleme iletisi)
 
@@ -3552,7 +3558,6 @@ variables.none=Henüz hiçbir değişken yok.
 variables.deletion=Değişkeni kaldır
 variables.deletion.description=Bir değişkeni kaldırma kalıcıdır ve geri alınamaz. Devam edilsin mi?
 variables.description=Değişkenler belirli işlemlere aktarılacaktır, bunun dışında okunamaz.
-variables.id_not_exist=%d kimlikli değişken mevcut değil.
 variables.edit=Değişkeni Düzenle
 variables.deletion.failed=Değişken kaldırılamadı.
 variables.deletion.success=Değişken kaldırıldı.
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 4cd6c44571..9aa6d6a16e 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -466,6 +466,7 @@ auth_failed=Помилка автентифікації: %v
 
 target_branch_not_exist=Цільової гілки не існує.
 
+
 [user]
 change_avatar=Змінити свій аватар…
 repositories=Репозиторії
@@ -1508,6 +1509,8 @@ activity.git_stats_and_deletions=та
 activity.git_stats_deletion_1=%d видалений
 activity.git_stats_deletion_n=%d видалені
 
+contributors.contribution_type.commits=Коміти
+
 search=Пошук
 search.search_repo=Пошук репозиторію
 search.fuzzy=Неточний
@@ -1961,6 +1964,8 @@ error.csv.too_large=Не вдається відобразити цей файл
 error.csv.unexpected=Не вдається відобразити цей файл, тому що він містить неочікуваний символ в рядку %d і стовпці %d.
 error.csv.invalid_field_count=Не вдається відобразити цей файл, тому що він має неправильну кількість полів у рядку %d.
 
+[graphs]
+
 [org]
 org_name_holder=Назва організації
 org_full_name_holder=Повна назва організації
@@ -2510,6 +2515,7 @@ notices.desc=Опис
 notices.op=Оп.
 notices.delete_success=Сповіщення системи були видалені.
 
+
 [action]
 create_repo=створив(ла) репозиторій <a href="%s">%s</a>
 rename_repo=репозиторій перейменовано з <code>%[1]s</code> на <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 6d22468c9d..7c8153cbb1 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -123,6 +123,7 @@ pin=固定
 unpin=取消置顶
 
 artifacts=制品
+confirm_delete_artifact=您确定要删除制品'%s'吗?
 
 archived=已归档
 
@@ -423,6 +424,7 @@ authorization_failed_desc=因为检测到无效请求,授权失败。请尝试
 sspi_auth_failed=SSPI 认证失败
 password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。
 password_pwned_err=无法完成对 HaveIBeenPwned 的请求
+last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
 
 [mail]
 view_it_on=在 %s 上查看
@@ -588,6 +590,8 @@ org_still_own_packages=该组织仍然是一个或多个软件包的拥有者,
 
 target_branch_not_exist=目标分支不存在。
 
+admin_cannot_delete_self=当您是管理员时,您不能删除自己。请先移除您的管理员权限
+
 [user]
 change_avatar=修改头像
 joined_on=加入于 %s
@@ -967,6 +971,8 @@ issue_labels_helper=选择一个工单标签集
 license=授权许可
 license_helper=选择授权许可文件。
 license_helper_desc=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合你的项目?见 <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许可证</a>
+object_format=对象格式
+object_format_helper=仓库的对象格式。之后无法更改。SHA1 是最兼容的。
 readme=自述
 readme_helper=选择自述文件模板。
 readme_helper_desc=这是您可以为您的项目撰写完整描述的地方。
@@ -984,6 +990,7 @@ mirror_prune=修剪
 mirror_prune_desc=删除过时的远程跟踪引用
 mirror_interval=镜像间隔 (有效的时间单位是 'h', 'm', 's')。0 禁用自动定期同步 (最短间隔: %s)
 mirror_interval_invalid=镜像间隔无效。
+mirror_sync=已同步
 mirror_sync_on_commit=推送提交时同步
 mirror_address=从 URL 克隆
 mirror_address_desc=在授权框中输入必要的凭据。
@@ -1034,6 +1041,7 @@ desc.public=公开
 desc.template=模板
 desc.internal=内部
 desc.archived=已存档
+desc.sha256=SHA256
 
 template.items=模板选项
 template.git_content=Git数据(默认分支)
@@ -1184,6 +1192,8 @@ audio_not_supported_in_browser=您的浏览器不支持使用 HTML5 'video' 标
 stored_lfs=存储到Git LFS
 symbolic_link=符号链接
 executable_file=可执行文件
+vendored=被供应的
+generated=已生成的
 commit_graph=提交图
 commit_graph.select=选择分支
 commit_graph.hide_pr_refs=隐藏合并请求
@@ -1707,6 +1717,7 @@ pulls.select_commit_hold_shift_for_range=选择提交。按住 Shift + 单击选
 pulls.review_only_possible_for_full_diff=只有在查看全部差异时才能进行审核
 pulls.filter_changes_by_commit=按提交筛选
 pulls.nothing_to_compare=分支内容相同,无需创建合并请求。
+pulls.nothing_to_compare_have_tag=所选分支/标签相同。
 pulls.nothing_to_compare_and_allow_empty_pr=这些分支是相等的,此合并请求将为空。
 pulls.has_pull_request=这些分支之间的合并请求已存在: <a href="%[1]s">%[2]s#%[3]d</a>
 pulls.create=创建合并请求
@@ -1901,6 +1912,7 @@ wiki.page_name_desc=输入此 Wiki 页面的名称。特殊名称有:'Home', '
 wiki.original_git_entry_tooltip=查看原始的 Git 文件而不是使用友好链接。
 
 activity=动态
+activity.navbar.contributors=贡献者
 activity.period.filter_label=周期:
 activity.period.daily=1 天
 activity.period.halfweekly=3 天
@@ -1966,6 +1978,11 @@ activity.git_stats_and_deletions=和
 activity.git_stats_deletion_1=删除 %d 行
 activity.git_stats_deletion_n=删除 %d 行
 
+contributors.contribution_type.filter_label=贡献类型:
+contributors.contribution_type.commits=提交
+contributors.contribution_type.additions=更多
+contributors.contribution_type.deletions=删除
+
 search=搜索
 search.search_repo=搜索仓库...
 search.type.tooltip=搜索类型
@@ -2003,6 +2020,7 @@ settings.mirror_settings.docs.doc_link_title=如何镜像仓库?
 settings.mirror_settings.docs.doc_link_pull_section=文档中的 “从远程仓库拉取” 部分。
 settings.mirror_settings.docs.pulling_remote_title=从远程仓库拉取代码
 settings.mirror_settings.mirrored_repository=镜像库
+settings.mirror_settings.pushed_repository=推送仓库
 settings.mirror_settings.direction=方向
 settings.mirror_settings.direction.pull=拉取
 settings.mirror_settings.direction.push=推送
@@ -2312,6 +2330,8 @@ settings.protect_approvals_whitelist_users=审查者白名单:
 settings.protect_approvals_whitelist_teams=审查团队白名单:
 settings.dismiss_stale_approvals=取消过时的批准
 settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。
+settings.ignore_stale_approvals=忽略过期批准
+settings.ignore_stale_approvals_desc=对旧提交(过期审核)的批准将不计入 PR 的批准数。如果过期审查已被驳回,则与此无关。
 settings.require_signed_commits=需要签名提交
 settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支
 settings.protect_branch_name_pattern=受保护的分支名称模式
@@ -2367,6 +2387,7 @@ settings.archive.error=仓库在归档时出现异常。请通过日志获取详
 settings.archive.error_ismirror=请不要对镜像仓库归档,谢谢!
 settings.archive.branchsettings_unavailable=已归档仓库无法进行分支设置。
 settings.archive.tagsettings_unavailable=已归档仓库的Git标签设置不可用。
+settings.archive.mirrors_unavailable=如果仓库已被归档,镜像将不可用。
 settings.unarchive.button=撤销仓库归档
 settings.unarchive.header=撤销此仓库归档
 settings.unarchive.text=撤销归档将恢复仓库接收提交、推送,以及新工单和合并请求的能力。
@@ -2565,6 +2586,13 @@ error.csv.too_large=无法渲染此文件,因为它太大了。
 error.csv.unexpected=无法渲染此文件,因为它包含了意外字符,其位于第 %d 行和第 %d 列。
 error.csv.invalid_field_count=无法渲染此文件,因为它在第 %d 行中的字段数有误。
 
+[graphs]
+component_loading=正在加载 %s...
+component_loading_failed=无法加载 %s
+component_loading_info=这可能需要一点…
+component_failed_to_load=意外的错误发生了。
+contributors.what=贡献
+
 [org]
 org_name_holder=组织名称
 org_full_name_holder=组织全名
@@ -2691,6 +2719,7 @@ teams.invite.description=请点击下面的按钮加入团队。
 
 [admin]
 dashboard=管理面板
+self_check=自我检查
 identity_access=身份及认证
 users=帐户管理
 organizations=组织管理
@@ -2736,6 +2765,7 @@ dashboard.delete_missing_repos=删除所有丢失 Git 文件的仓库
 dashboard.delete_missing_repos.started=删除所有丢失 Git 文件的仓库任务已启动。
 dashboard.delete_generated_repository_avatars=删除生成的仓库头像
 dashboard.sync_repo_branches=将缺少的分支从 git 数据同步到数据库
+dashboard.sync_repo_tags=从 git 数据同步标签到数据库
 dashboard.update_mirrors=更新镜像仓库
 dashboard.repo_health_check=健康检查所有仓库
 dashboard.check_repo_stats=检查所有仓库统计
@@ -2790,6 +2820,7 @@ dashboard.stop_endless_tasks=停止永不停止的任务
 dashboard.cancel_abandoned_jobs=取消丢弃的任务
 dashboard.start_schedule_tasks=开始调度任务
 dashboard.sync_branch.started=分支同步已开始
+dashboard.sync_tag.started=标签同步已开始
 dashboard.rebuild_issue_indexer=重建工单索引
 
 users.user_manage_panel=用户帐户管理
@@ -3216,6 +3247,11 @@ notices.desc=提示描述
 notices.op=操作
 notices.delete_success=系统通知已被删除。
 
+self_check.no_problem_found=尚未发现问题。
+self_check.database_collation_mismatch=期望数据库使用的校验方式:%s
+self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。
+self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。
+
 [action]
 create_repo=创建了仓库 <a href="%s">%s</a>
 rename_repo=重命名仓库 <code>%[1]s</code> 为 <a href="%[2]s">%[3]s</a>
@@ -3400,6 +3436,9 @@ rpm.registry=从命令行设置此注册中心:
 rpm.distros.redhat=在基于 RedHat 的发行版
 rpm.distros.suse=在基于 SUSE 的发行版
 rpm.install=要安装包,请运行以下命令:
+rpm.repository=仓库信息
+rpm.repository.architectures=架构
+rpm.repository.multiple_groups=此软件包可在多个组中使用。
 rubygems.install=要使用 gem 安装软件包,请运行以下命令:
 rubygems.install2=或将它添加到 Gemfile:
 rubygems.dependencies.runtime=运行时依赖
@@ -3532,8 +3571,8 @@ runs.actors_no_select=所有操作者
 runs.status_no_select=所有状态
 runs.no_results=没有匹配的结果。
 runs.no_workflows=目前还没有工作流。
-runs.no_workflows.quick_start=不知道如何启动Gitea Action?请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">快速启动指南</a>
-runs.no_workflows.documentation=更多有关 Gitea Action 的信息,请访问 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。
+runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗?请查看 <a target="_blank" rel="noopener noreferrer" href="%s">快速启动指南</a>。
+runs.no_workflows.documentation=关于Gitea Actions的更多信息,请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。
 runs.no_runs=工作流尚未运行过。
 runs.empty_commit_message=(空白的提交消息)
 
@@ -3552,7 +3591,7 @@ variables.none=目前还没有变量。
 variables.deletion=删除变量
 variables.deletion.description=删除变量是永久性的,无法撤消。继续吗?
 variables.description=变量将被传给特定的 Actions,其它情况将不能读取
-variables.id_not_exist=ID %d 变量不存在。
+variables.id_not_exist=ID为 %d 的变量不存在。
 variables.edit=编辑变量
 variables.deletion.failed=删除变量失败。
 variables.deletion.success=变量已被删除。
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index d4074026fd..8c45e3157f 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -195,6 +195,7 @@ auth_failed=授權驗證失敗:%v
 
 target_branch_not_exist=目標分支不存在
 
+
 [user]
 repositories=儲存庫列表
 activity=公開活動
@@ -537,6 +538,8 @@ activity.merged_prs_label=已合併
 activity.closed_issue_label=已關閉
 activity.new_issues_count_1=建立問題
 
+contributors.contribution_type.commits=提交歷史
+
 search=搜尋
 
 settings=儲存庫設定
@@ -639,6 +642,8 @@ release.downloads=下載附件
 
 
 
+[graphs]
+
 [org]
 org_name_holder=組織名稱
 org_full_name_holder=組織全名
@@ -915,6 +920,7 @@ notices.desc=描述
 notices.op=操作
 notices.delete_success=已刪除系統提示。
 
+
 [action]
 create_repo=建立了儲存庫 <a href="%s">%s</a>
 rename_repo=重新命名儲存庫 <code>%[1]s</code> 為 <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index ea79c45674..09eb262212 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -555,6 +555,7 @@ org_still_own_packages=此組織仍然擁有一個以上的套件,請先刪除
 
 target_branch_not_exist=目標分支不存在
 
+
 [user]
 change_avatar=更改大頭貼...
 repositories=儲存庫
@@ -1776,6 +1777,8 @@ activity.git_stats_and_deletions=和
 activity.git_stats_deletion_1=刪除 %d 行
 activity.git_stats_deletion_n=刪除 %d 行
 
+contributors.contribution_type.commits=提交歷史
+
 search=搜尋
 search.search_repo=搜尋儲存庫
 search.type.tooltip=搜尋類型
@@ -2321,6 +2324,8 @@ error.csv.too_large=無法渲染此檔案,因為它太大了。
 error.csv.unexpected=無法渲染此檔案,因為它包含了未預期的字元,於第 %d 行第 %d 列。
 error.csv.invalid_field_count=無法渲染此檔案,因為它第 %d 行的欄位數量有誤。
 
+[graphs]
+
 [org]
 org_name_holder=組織名稱
 org_full_name_holder=組織全名
@@ -2933,6 +2938,7 @@ notices.desc=描述
 notices.op=操作
 notices.delete_success=已刪除系統提示。
 
+
 [action]
 create_repo=建立了儲存庫 <a href="%s">%s</a>
 rename_repo=重新命名儲存庫 <code>%[1]s</code> 為 <a href="%[2]s">%[3]s</a>
@@ -3109,6 +3115,8 @@ pypi.requires=需要 Python
 pypi.install=執行下列命令以使用 pip 安裝此套件:
 rpm.registry=透過下列命令設定此註冊中心:
 rpm.install=執行下列命令安裝此套件:
+rpm.repository=儲存庫資訊
+rpm.repository.architectures=架構
 rubygems.install=執行下列命令以使用 gem 安裝此套件:
 rubygems.install2=或將它加到 Gemfile:
 rubygems.dependencies.runtime=執行階段相依性

From c236e64aca42b9ab0743431bc505033a0cb78b93 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 22 Feb 2024 04:19:13 +0100
Subject: [PATCH 116/679] Don't install playwright twice (#29302)

1. `playwright/test` is already installed as part of `deps-frontend` on
CI which runs before, so it's better to not install it again (on a
potentially different version), and just use the version from
package.json and add the `deps-frontend` dependency.
2. `PLAYWRIGHT_DIR` is a undefined variable, so I removed it

```bash
$ git show c8ded77680db7344c8dc1ccee76bce0b4e02e103 | grep PLAYWRIGHT_DIR
+playwright: $(PLAYWRIGHT_DIR)
```
---
 Makefile | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Makefile b/Makefile
index 925fdcb946..7fa8193800 100644
--- a/Makefile
+++ b/Makefile
@@ -602,8 +602,7 @@ test-mssql\#%: integrations.mssql.test generate-ini-mssql
 test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
 
 .PHONY: playwright
-playwright: $(PLAYWRIGHT_DIR)
-	npm install --no-save @playwright/test
+playwright: deps-frontend
 	npx playwright install $(PLAYWRIGHT_FLAGS)
 
 .PHONY: test-e2e%

From d6811baf88ca6d58b92d4dc12b1f2a292198751f Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Thu, 22 Feb 2024 04:48:19 +0100
Subject: [PATCH 117/679] Discard unread data of `git cat-file` (#29297)

Fixes #29101
Related #29298

Discard all read data to prevent misinterpreting existing data. Some
discard calls were missing in error cases.

---------

Co-authored-by: yp05327 <576951401@qq.com>
---
 modules/git/batch_reader.go                | 40 +++++++++++-----------
 modules/git/blob_nogogit.go                | 24 ++-----------
 modules/git/commit_info_nogogit.go         |  3 ++
 modules/git/pipeline/lfs_nogogit.go        |  4 +++
 modules/git/repo_commit_nogogit.go         |  3 +-
 modules/git/repo_language_stats_nogogit.go | 23 +------------
 modules/git/repo_tag_nogogit.go            |  3 ++
 modules/git/repo_tree_nogogit.go           |  3 ++
 modules/git/tree_nogogit.go                | 16 ++-------
 modules/git/tree_test.go                   | 27 +++++++++++++++
 10 files changed, 66 insertions(+), 80 deletions(-)
 create mode 100644 modules/git/tree_test.go

diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go
index 53a9393d5f..043dbb44bd 100644
--- a/modules/git/batch_reader.go
+++ b/modules/git/batch_reader.go
@@ -203,16 +203,7 @@ headerLoop:
 	}
 
 	// Discard the rest of the tag
-	discard := size - n + 1
-	for discard > math.MaxInt32 {
-		_, err := rd.Discard(math.MaxInt32)
-		if err != nil {
-			return id, err
-		}
-		discard -= math.MaxInt32
-	}
-	_, err := rd.Discard(int(discard))
-	return id, err
+	return id, DiscardFull(rd, size-n+1)
 }
 
 // ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
@@ -238,16 +229,7 @@ headerLoop:
 	}
 
 	// Discard the rest of the commit
-	discard := size - n + 1
-	for discard > math.MaxInt32 {
-		_, err := rd.Discard(math.MaxInt32)
-		if err != nil {
-			return id, err
-		}
-		discard -= math.MaxInt32
-	}
-	_, err := rd.Discard(int(discard))
-	return id, err
+	return id, DiscardFull(rd, size-n+1)
 }
 
 // git tree files are a list:
@@ -345,3 +327,21 @@ func init() {
 	_, filename, _, _ := runtime.Caller(0)
 	callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go")
 }
+
+func DiscardFull(rd *bufio.Reader, discard int64) error {
+	if discard > math.MaxInt32 {
+		n, err := rd.Discard(math.MaxInt32)
+		discard -= int64(n)
+		if err != nil {
+			return err
+		}
+	}
+	for discard > 0 {
+		n, err := rd.Discard(int(discard))
+		discard -= int64(n)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go
index 6e8a48b1db..9e1c2a0376 100644
--- a/modules/git/blob_nogogit.go
+++ b/modules/git/blob_nogogit.go
@@ -9,7 +9,6 @@ import (
 	"bufio"
 	"bytes"
 	"io"
-	"math"
 
 	"code.gitea.io/gitea/modules/log"
 )
@@ -104,25 +103,6 @@ func (b *blobReader) Read(p []byte) (n int, err error) {
 // Close implements io.Closer
 func (b *blobReader) Close() error {
 	defer b.cancel()
-	if b.n > 0 {
-		for b.n > math.MaxInt32 {
-			n, err := b.rd.Discard(math.MaxInt32)
-			b.n -= int64(n)
-			if err != nil {
-				return err
-			}
-			b.n -= math.MaxInt32
-		}
-		n, err := b.rd.Discard(int(b.n))
-		b.n -= int64(n)
-		if err != nil {
-			return err
-		}
-	}
-	if b.n == 0 {
-		_, err := b.rd.Discard(1)
-		b.n--
-		return err
-	}
-	return nil
+
+	return DiscardFull(b.rd, b.n+1)
 }
diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go
index e469d2cab6..a5d18694f7 100644
--- a/modules/git/commit_info_nogogit.go
+++ b/modules/git/commit_info_nogogit.go
@@ -151,6 +151,9 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string,
 			return nil, err
 		}
 		if typ != "commit" {
+			if err := DiscardFull(batchReader, size+1); err != nil {
+				return nil, err
+			}
 			return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
 		}
 		c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size))
diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go
index a725f4799d..4c65249089 100644
--- a/modules/git/pipeline/lfs_nogogit.go
+++ b/modules/git/pipeline/lfs_nogogit.go
@@ -169,6 +169,10 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
 				} else {
 					break commitReadingLoop
 				}
+			default:
+				if err := git.DiscardFull(batchReader, size+1); err != nil {
+					return nil, err
+				}
 			}
 		}
 	}
diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go
index f0214e1ff8..a7031184e2 100644
--- a/modules/git/repo_commit_nogogit.go
+++ b/modules/git/repo_commit_nogogit.go
@@ -121,8 +121,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID)
 		return commit, nil
 	default:
 		log.Debug("Unknown typ: %s", typ)
-		_, err = rd.Discard(int(size) + 1)
-		if err != nil {
+		if err := DiscardFull(rd, size+1); err != nil {
 			return nil, err
 		}
 		return nil, ErrNotExist{
diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go
index 1d94ad6c00..d68d7d210a 100644
--- a/modules/git/repo_language_stats_nogogit.go
+++ b/modules/git/repo_language_stats_nogogit.go
@@ -6,10 +6,8 @@
 package git
 
 import (
-	"bufio"
 	"bytes"
 	"io"
-	"math"
 	"strings"
 
 	"code.gitea.io/gitea/modules/analyze"
@@ -168,8 +166,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 				return nil, err
 			}
 			content = contentBuf.Bytes()
-			err = discardFull(batchReader, discard)
-			if err != nil {
+			if err := DiscardFull(batchReader, discard); err != nil {
 				return nil, err
 			}
 		}
@@ -212,21 +209,3 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 
 	return mergeLanguageStats(sizes), nil
 }
-
-func discardFull(rd *bufio.Reader, discard int64) error {
-	if discard > math.MaxInt32 {
-		n, err := rd.Discard(math.MaxInt32)
-		discard -= int64(n)
-		if err != nil {
-			return err
-		}
-	}
-	for discard > 0 {
-		n, err := rd.Discard(int(discard))
-		discard -= int64(n)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go
index 5d98fadd54..cbab39f8c5 100644
--- a/modules/git/repo_tag_nogogit.go
+++ b/modules/git/repo_tag_nogogit.go
@@ -103,6 +103,9 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
 		return nil, err
 	}
 	if typ != "tag" {
+		if err := DiscardFull(rd, size+1); err != nil {
+			return nil, err
+		}
 		return nil, ErrNotExist{ID: tagID.String()}
 	}
 
diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go
index 20c92a79ed..582247b4a4 100644
--- a/modules/git/repo_tree_nogogit.go
+++ b/modules/git/repo_tree_nogogit.go
@@ -58,6 +58,9 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
 		tree.entriesParsed = true
 		return tree, nil
 	default:
+		if err := DiscardFull(rd, size+1); err != nil {
+			return nil, err
+		}
 		return nil, ErrNotExist{
 			ID: id.String(),
 		}
diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go
index 89d3aebbc0..28d02c7e81 100644
--- a/modules/git/tree_nogogit.go
+++ b/modules/git/tree_nogogit.go
@@ -7,7 +7,6 @@ package git
 
 import (
 	"io"
-	"math"
 	"strings"
 )
 
@@ -63,19 +62,8 @@ func (t *Tree) ListEntries() (Entries, error) {
 		}
 
 		// Not a tree just use ls-tree instead
-		for sz > math.MaxInt32 {
-			discarded, err := rd.Discard(math.MaxInt32)
-			sz -= int64(discarded)
-			if err != nil {
-				return nil, err
-			}
-		}
-		for sz > 0 {
-			discarded, err := rd.Discard(int(sz))
-			sz -= int64(discarded)
-			if err != nil {
-				return nil, err
-			}
+		if err := DiscardFull(rd, sz+1); err != nil {
+			return nil, err
 		}
 	}
 
diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go
new file mode 100644
index 0000000000..6d2b5c84d5
--- /dev/null
+++ b/modules/git/tree_test.go
@@ -0,0 +1,27 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSubTree_Issue29101(t *testing.T) {
+	repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
+	assert.NoError(t, err)
+	defer repo.Close()
+
+	commit, err := repo.GetCommit("ce064814f4a0d337b333e646ece456cd39fab612")
+	assert.NoError(t, err)
+
+	// old code could produce a different error if called multiple times
+	for i := 0; i < 10; i++ {
+		_, err = commit.SubTree("file1.txt")
+		assert.Error(t, err)
+		assert.True(t, IsErrNotExist(err))
+	}
+}

From 182b9c193642838dd41bf26f0fe4b2e870770f7e Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 22 Feb 2024 13:31:37 +0800
Subject: [PATCH 118/679] small cache when get user id on interation (#29296)

---
 services/agit/agit.go | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/services/agit/agit.go b/services/agit/agit.go
index bc68372570..75b561581d 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -38,6 +38,11 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 	_, forcePush = opts.GitPushOptions["force-push"]
 	objectFormat, _ := gitRepo.GetObjectFormat()
 
+	pusher, err := user_model.GetUserByID(ctx, opts.UserID)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get user. Error: %w", err)
+	}
+
 	for i := range opts.OldCommitIDs {
 		if opts.NewCommitIDs[i] == objectFormat.EmptyObjectID().String() {
 			results = append(results, private.HookProcReceiveRefResult{
@@ -116,11 +121,6 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 				description = opts.GitPushOptions["description"]
 			}
 
-			pusher, err := user_model.GetUserByID(ctx, opts.UserID)
-			if err != nil {
-				return nil, fmt.Errorf("Failed to get user. Error: %w", err)
-			}
-
 			prIssue := &issues_model.Issue{
 				RepoID:   repo.ID,
 				Title:    title,

From e9b13732f3d3b5536e43bdfdb5757dbbf484d694 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 22 Feb 2024 15:04:30 +0800
Subject: [PATCH 119/679] Refactor cmd setup and remove deadcode (#29313)

* use `setup(ctx, c.Bool("debug"))` like all other callers
* `setting.RunMode = "dev"` is a no-op.
* `if _, err := os.Stat(setting.RepoRootPath); err != nil` could be
simplified
---
 cmd/keys.go |  2 +-
 cmd/serv.go | 13 +------------
 2 files changed, 2 insertions(+), 13 deletions(-)

diff --git a/cmd/keys.go b/cmd/keys.go
index ceeec48486..7fdbe16119 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -71,7 +71,7 @@ func runKeys(c *cli.Context) error {
 	ctx, cancel := installSignals()
 	defer cancel()
 
-	setup(ctx, false)
+	setup(ctx, c.Bool("debug"))
 
 	authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
 	// do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys
diff --git a/cmd/serv.go b/cmd/serv.go
index 3cc504beb4..90190a19db 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -63,21 +63,10 @@ func setup(ctx context.Context, debug bool) {
 		setupConsoleLogger(log.FATAL, false, os.Stderr)
 	}
 	setting.MustInstalled()
-	if debug {
-		setting.RunMode = "dev"
-	}
-
-	// Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when
-	// `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection.
 	if _, err := os.Stat(setting.RepoRootPath); err != nil {
-		if os.IsNotExist(err) {
-			_ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath)
-		} else {
-			_ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err)
-		}
+		_ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err)
 		return
 	}
-
 	if err := git.InitSimple(context.Background()); err != nil {
 		_ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err)
 	}

From a70c00b80bcb5de8479e407f1b8f08dcf756019d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Br=C3=BCckner?= <code@nik.dev>
Date: Thu, 22 Feb 2024 07:29:03 +0000
Subject: [PATCH 120/679] Properly migrate automatic merge GitLab comments
 (#27873)

GitLab generates "system notes" whenever an event happens within the
platform. Unlike Gitea, those events are stored and retrieved as text
comments with no semantic details. The only way to tell whether a
comment was generated in this manner is the `system` flag on the note
type.

This PR adds detection for two specific kinds of events: Scheduling and
un-scheduling of automatic merges on a PR. When detected, they are
downloaded using Gitea's type for these events, and eventually uploaded
into Gitea in the expected format, i.e. with no text content in the
comment.

This PR also updates the template used to render comments to add support
for migrated comments of these two types.

ref:
https://gitlab.com/gitlab-org/gitlab/-/blob/11bd6dc826e0bea2832324a1d7356949a9398884/app/services/system_notes/merge_requests_service.rb#L6-L17

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/migrations/gitea_uploader.go         |  2 +
 services/migrations/gitlab.go                 | 50 +++++++-------
 services/migrations/gitlab_test.go            | 65 +++++++++++++++++++
 .../repo/issue/view_content/comments.tmpl     | 17 ++++-
 .../repo/issue/view_content/conversation.tmpl |  7 +-
 5 files changed, 111 insertions(+), 30 deletions(-)

diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 7b21d9f4d2..2891977c7c 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -487,6 +487,8 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 			if comment.Meta["NewTitle"] != nil {
 				cm.NewTitle = fmt.Sprintf("%s", comment.Meta["NewTitle"])
 			}
+		case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge:
+			cm.Content = ""
 		default:
 		}
 
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 3db10465fc..d08eaf0f84 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -14,6 +14,7 @@ import (
 	"strings"
 	"time"
 
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	base "code.gitea.io/gitea/modules/migration"
@@ -506,30 +507,8 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
 			return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err)
 		}
 		for _, comment := range comments {
-			// Flatten comment threads
-			if !comment.IndividualNote {
-				for _, note := range comment.Notes {
-					allComments = append(allComments, &base.Comment{
-						IssueIndex:  commentable.GetLocalIndex(),
-						Index:       int64(note.ID),
-						PosterID:    int64(note.Author.ID),
-						PosterName:  note.Author.Username,
-						PosterEmail: note.Author.Email,
-						Content:     note.Body,
-						Created:     *note.CreatedAt,
-					})
-				}
-			} else {
-				c := comment.Notes[0]
-				allComments = append(allComments, &base.Comment{
-					IssueIndex:  commentable.GetLocalIndex(),
-					Index:       int64(c.ID),
-					PosterID:    int64(c.Author.ID),
-					PosterName:  c.Author.Username,
-					PosterEmail: c.Author.Email,
-					Content:     c.Body,
-					Created:     *c.CreatedAt,
-				})
+			for _, note := range comment.Notes {
+				allComments = append(allComments, g.convertNoteToComment(commentable.GetLocalIndex(), note))
 			}
 		}
 		if resp.NextPage == 0 {
@@ -540,6 +519,29 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
 	return allComments, true, nil
 }
 
+func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment {
+	comment := &base.Comment{
+		IssueIndex:  localIndex,
+		Index:       int64(note.ID),
+		PosterID:    int64(note.Author.ID),
+		PosterName:  note.Author.Username,
+		PosterEmail: note.Author.Email,
+		Content:     note.Body,
+		Created:     *note.CreatedAt,
+	}
+
+	// Try to find the underlying event of system notes.
+	if note.System {
+		if strings.HasPrefix(note.Body, "enabled an automatic merge") {
+			comment.CommentType = issues_model.CommentTypePRScheduledToAutoMerge.String()
+		} else if note.Body == "canceled the automatic merge" {
+			comment.CommentType = issues_model.CommentTypePRUnScheduledToAutoMerge.String()
+		}
+	}
+
+	return comment
+}
+
 // GetPullRequests returns pull requests according page and perPage
 func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
 	if perPage > g.maxPerPage {
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 1e0aa2b025..2b87a1dfe6 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -517,6 +517,71 @@ func TestAwardsToReactions(t *testing.T) {
 	}, reactions)
 }
 
+func TestNoteToComment(t *testing.T) {
+	downloader := &GitlabDownloader{}
+
+	now := time.Now()
+	makeTestNote := func(id int, body string, system bool) gitlab.Note {
+		return gitlab.Note{
+			ID: id,
+			Author: struct {
+				ID        int    `json:"id"`
+				Username  string `json:"username"`
+				Email     string `json:"email"`
+				Name      string `json:"name"`
+				State     string `json:"state"`
+				AvatarURL string `json:"avatar_url"`
+				WebURL    string `json:"web_url"`
+			}{
+				ID:       72,
+				Email:    "test@example.com",
+				Username: "test",
+			},
+			Body:      body,
+			CreatedAt: &now,
+			System:    system,
+		}
+	}
+	notes := []gitlab.Note{
+		makeTestNote(1, "This is a regular comment", false),
+		makeTestNote(2, "enabled an automatic merge for abcd1234", true),
+		makeTestNote(3, "canceled the automatic merge", true),
+	}
+	comments := []base.Comment{{
+		IssueIndex:  17,
+		Index:       1,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
+		CommentType: "",
+		Content:     "This is a regular comment",
+		Created:     now,
+	}, {
+		IssueIndex:  17,
+		Index:       2,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
+		CommentType: "pull_scheduled_merge",
+		Content:     "enabled an automatic merge for abcd1234",
+		Created:     now,
+	}, {
+		IssueIndex:  17,
+		Index:       3,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
+		CommentType: "pull_cancel_scheduled_merge",
+		Content:     "canceled the automatic merge",
+		Created:     now,
+	}}
+
+	for i, note := range notes {
+		actualComment := *downloader.convertNoteToComment(17, &note)
+		assert.EqualValues(t, actualComment, comments[i])
+	}
+}
+
 func TestGitlabIIDResolver(t *testing.T) {
 	r := gitlabIIDResolver{}
 	r.recordIssueIID(1)
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index ed83377f5a..a10909b3fc 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -381,8 +381,9 @@
 								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 								{{.OriginalAuthor}}
 							</span>
-							<span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span>
-							<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span>
+							{{if $.Repository.OriginalURL}}
+							<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
+							{{end}}
 						{{else}}
 							{{template "shared/user/authorlink" .Poster}}
 						{{end}}
@@ -663,7 +664,17 @@
 			<div class="timeline-item event" id="{{.HashTag}}">
 				<span class="badge">{{svg "octicon-git-merge" 16}}</span>
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
+					{{if .OriginalAuthor}}
+						<span class="text black">
+							{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
+							{{.OriginalAuthor}}
+						</span>
+						{{if $.Repository.OriginalURL}}
+						<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
+						{{end}}
+					{{else}}
+						{{template "shared/user/authorlink" .Poster}}
+					{{end}}
 					{{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}}
 					{{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}}
 				</span>
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index c9e5ee6275..fc1d9865f5 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -63,12 +63,13 @@
 								{{end}}
 								<span class="text grey muted-links">
 									{{if .OriginalAuthor}}
-										<span class="text black gt-font-semibold">
+										<span class="text black">
 											{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 											{{.OriginalAuthor}}
 										</span>
-										<span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span>
-										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span>
+										{{if $.Repository.OriginalURL}}
+										<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
+										{{end}}
 									{{else}}
 										{{template "shared/user/authorlink" .Poster}}
 									{{end}}

From a4fe1cdf38f9a063e44b197ef07e4260f731c919 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Thu, 22 Feb 2024 22:47:35 +0800
Subject: [PATCH 121/679] Improve the `issue_comment` workflow trigger event
 (#29277)

Fix #29175
Replace #29207

This PR makes some improvements to the `issue_comment` workflow trigger
event.

1. Fix the bug that pull requests cannot trigger `issue_comment`
workflows
2. Previously the `issue_comment` event only supported the `created`
activity type. This PR adds support for the missing `edited` and
`deleted` activity types.
3. Some events (including `issue_comment`, `issues`, etc. ) only trigger
workflows that belong to the workflow file on the default branch. This
PR introduces the `IsDefaultBranchWorkflow` function to check for these
events.
---
 modules/actions/github.go           | 44 +++++++++++++
 modules/actions/github_test.go      |  6 ++
 services/actions/notifier.go        | 96 ++++++++++++++++++++++-------
 services/actions/notifier_helper.go | 11 ++--
 4 files changed, 130 insertions(+), 27 deletions(-)

diff --git a/modules/actions/github.go b/modules/actions/github.go
index 18917c5118..68116ec83a 100644
--- a/modules/actions/github.go
+++ b/modules/actions/github.go
@@ -25,6 +25,45 @@ const (
 	GithubEventSchedule                 = "schedule"
 )
 
+// IsDefaultBranchWorkflow returns true if the event only triggers workflows on the default branch
+func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool {
+	switch triggedEvent {
+	case webhook_module.HookEventDelete:
+		// GitHub "delete" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete
+		return true
+	case webhook_module.HookEventFork:
+		// GitHub "fork" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#fork
+		return true
+	case webhook_module.HookEventIssueComment:
+		// GitHub "issue_comment" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment
+		return true
+	case webhook_module.HookEventPullRequestComment:
+		// GitHub "pull_request_comment" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
+		return true
+	case webhook_module.HookEventWiki:
+		// GitHub "gollum" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum
+		return true
+	case webhook_module.HookEventSchedule:
+		// GitHub "schedule" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
+		return true
+	case webhook_module.HookEventIssues,
+		webhook_module.HookEventIssueAssign,
+		webhook_module.HookEventIssueLabel,
+		webhook_module.HookEventIssueMilestone:
+		// Github "issues" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues
+		return true
+	}
+
+	return false
+}
+
 // canGithubEventMatch check if the input Github event can match any Gitea event.
 func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEventType) bool {
 	switch eventName {
@@ -75,6 +114,11 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
 	case GithubEventSchedule:
 		return triggedEvent == webhook_module.HookEventSchedule
 
+	case GithubEventIssueComment:
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
+		return triggedEvent == webhook_module.HookEventIssueComment ||
+			triggedEvent == webhook_module.HookEventPullRequestComment
+
 	default:
 		return eventName == string(triggedEvent)
 	}
diff --git a/modules/actions/github_test.go b/modules/actions/github_test.go
index 4bf55ae03f..6652ff6eac 100644
--- a/modules/actions/github_test.go
+++ b/modules/actions/github_test.go
@@ -103,6 +103,12 @@ func TestCanGithubEventMatch(t *testing.T) {
 			webhook_module.HookEventCreate,
 			true,
 		},
+		{
+			"create pull request comment",
+			GithubEventIssueComment,
+			webhook_module.HookEventPullRequestComment,
+			true,
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 77848a3f58..e144484dab 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -224,37 +224,88 @@ func (n *actionsNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
 ) {
 	ctx = withMethod(ctx, "CreateIssueComment")
 
-	permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer)
-
 	if issue.IsPull {
-		if err := issue.LoadPullRequest(ctx); err != nil {
+		notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentCreated)
+		return
+	}
+	notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentCreated)
+}
+
+func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
+	ctx = withMethod(ctx, "UpdateComment")
+
+	if err := c.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return
+	}
+
+	if c.Issue.IsPull {
+		notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited)
+		return
+	}
+	notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventIssueComment, api.HookIssueCommentEdited)
+}
+
+func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) {
+	ctx = withMethod(ctx, "DeleteComment")
+
+	if err := comment.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return
+	}
+
+	if comment.Issue.IsPull {
+		notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted)
+		return
+	}
+	notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentDeleted)
+}
+
+func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) {
+	if err := comment.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return
+	}
+	if err := comment.Issue.LoadAttributes(ctx); err != nil {
+		log.Error("LoadAttributes: %v", err)
+		return
+	}
+
+	permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer)
+
+	payload := &api.IssueCommentPayload{
+		Action:     action,
+		Issue:      convert.ToAPIIssue(ctx, comment.Issue),
+		Comment:    convert.ToAPIComment(ctx, comment.Issue.Repo, comment),
+		Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission),
+		Sender:     convert.ToUser(ctx, doer, nil),
+		IsPull:     comment.Issue.IsPull,
+	}
+
+	if action == api.HookIssueCommentEdited {
+		payload.Changes = &api.ChangesPayload{
+			Body: &api.ChangesFromPayload{
+				From: oldContent,
+			},
+		}
+	}
+
+	if comment.Issue.IsPull {
+		if err := comment.Issue.LoadPullRequest(ctx); err != nil {
 			log.Error("LoadPullRequest: %v", err)
 			return
 		}
-		newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestComment).
+		newNotifyInputFromIssue(comment.Issue, event).
 			WithDoer(doer).
-			WithPayload(&api.IssueCommentPayload{
-				Action:     api.HookIssueCommentCreated,
-				Issue:      convert.ToAPIIssue(ctx, issue),
-				Comment:    convert.ToAPIComment(ctx, repo, comment),
-				Repository: convert.ToRepo(ctx, repo, permission),
-				Sender:     convert.ToUser(ctx, doer, nil),
-				IsPull:     true,
-			}).
-			WithPullRequest(issue.PullRequest).
+			WithPayload(payload).
+			WithPullRequest(comment.Issue.PullRequest).
 			Notify(ctx)
 		return
 	}
-	newNotifyInputFromIssue(issue, webhook_module.HookEventIssueComment).
+
+	newNotifyInputFromIssue(comment.Issue, event).
 		WithDoer(doer).
-		WithPayload(&api.IssueCommentPayload{
-			Action:     api.HookIssueCommentCreated,
-			Issue:      convert.ToAPIIssue(ctx, issue),
-			Comment:    convert.ToAPIComment(ctx, repo, comment),
-			Repository: convert.ToRepo(ctx, repo, permission),
-			Sender:     convert.ToUser(ctx, doer, nil),
-			IsPull:     false,
-		}).
+		WithPayload(payload).
 		Notify(ctx)
 }
 
@@ -496,7 +547,6 @@ func (n *actionsNotifier) DeleteRef(ctx context.Context, pusher *user_model.User
 	apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone})
 
 	newNotifyInput(repo, pusher, webhook_module.HookEventDelete).
-		WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name
 		WithPayload(&api.DeletePayload{
 			Ref:        refFullName.ShortName(),
 			RefType:    refFullName.RefType(),
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 8852f23c5f..c20335af6f 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -136,12 +136,15 @@ func notify(ctx context.Context, input *notifyInput) error {
 	defer gitRepo.Close()
 
 	ref := input.Ref
-	if input.Event == webhook_module.HookEventDelete {
-		// The event is deleting a reference, so it will fail to get the commit for a deleted reference.
-		// Set ref to empty string to fall back to the default branch.
-		ref = ""
+	if ref != input.Repo.DefaultBranch && actions_module.IsDefaultBranchWorkflow(input.Event) {
+		if ref != "" {
+			log.Warn("Event %q should only trigger workflows on the default branch, but its ref is %q. Will fall back to the default branch",
+				input.Event, ref)
+		}
+		ref = input.Repo.DefaultBranch
 	}
 	if ref == "" {
+		log.Warn("Ref of event %q is empty, will fall back to the default branch", input.Event)
 		ref = input.Repo.DefaultBranch
 	}
 

From f390d5eb4f4db21eeacdf2e7a093f6bd4e87c96f Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Thu, 22 Feb 2024 18:35:58 +0200
Subject: [PATCH 122/679] Remove jQuery from the image pasting functionality
 (#29324)

- Switched to plain JavaScript
- Tested the image pasting functionality and it works as before

# Demo using JavaScript without jQuery

![demo](https://github.com/go-gitea/gitea/assets/20454870/018993ff-7b09-4d5f-88e0-f276368bacd6)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/comp/ImagePaste.js | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js
index 444ab89150..b727880bc8 100644
--- a/web_src/js/features/comp/ImagePaste.js
+++ b/web_src/js/features/comp/ImagePaste.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
 import {htmlEscape} from 'escape-goat';
 import {POST} from '../../modules/fetch.js';
 import {imageInfo} from '../../utils/image.js';
@@ -93,11 +92,10 @@ class CodeMirrorEditor {
 }
 
 const uploadClipboardImage = async (editor, dropzone, e) => {
-  const $dropzone = $(dropzone);
-  const uploadUrl = $dropzone.attr('data-upload-url');
-  const $files = $dropzone.find('.files');
+  const uploadUrl = dropzone.getAttribute('data-upload-url');
+  const filesContainer = dropzone.querySelector('.files');
 
-  if (!uploadUrl || !$files.length) return;
+  if (!uploadUrl || !filesContainer) return;
 
   const pastedImages = clipboardPastedImages(e);
   if (!pastedImages || pastedImages.length === 0) {
@@ -126,8 +124,12 @@ const uploadClipboardImage = async (editor, dropzone, e) => {
     }
     editor.replacePlaceholder(placeholder, text);
 
-    const $input = $(`<input name="files" type="hidden">`).attr('id', uuid).val(uuid);
-    $files.append($input);
+    const input = document.createElement('input');
+    input.setAttribute('name', 'files');
+    input.setAttribute('type', 'hidden');
+    input.setAttribute('id', uuid);
+    input.value = uuid;
+    filesContainer.append(input);
   }
 };
 
@@ -140,7 +142,7 @@ export function initEasyMDEImagePaste(easyMDE, dropzone) {
 
 export function initTextareaImagePaste(textarea, dropzone) {
   if (!dropzone) return;
-  $(textarea).on('paste', async (e) => {
-    return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e.originalEvent);
+  textarea.addEventListener('paste', async (e) => {
+    return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e);
   });
 }

From 7a1557d2cc893030ae900c4333eeb12d84b891dc Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 23 Feb 2024 01:02:33 +0800
Subject: [PATCH 123/679] Remove unnecessary "Safe" modifier from templates
 (#29318)

Follow #29165
---
 templates/explore/repo_search.tmpl            |   2 +-
 templates/explore/user_list.tmpl              |   2 +-
 templates/install.tmpl                        |   4 +-
 templates/package/content/alpine.tmpl         |   8 +-
 templates/package/content/cargo.tmpl          |   4 +-
 templates/package/content/chef.tmpl           |   4 +-
 templates/package/content/composer.tmpl       |   4 +-
 templates/package/content/conan.tmpl          |   2 +-
 templates/package/content/conda.tmpl          |   4 +-
 templates/package/content/container.tmpl      |   2 +-
 templates/package/content/cran.tmpl           |   4 +-
 templates/package/content/debian.tmpl         |   4 +-
 templates/package/content/generic.tmpl        |   2 +-
 templates/package/content/go.tmpl             |   2 +-
 templates/package/content/helm.tmpl           |   2 +-
 templates/package/content/maven.tmpl          |   6 +-
 templates/package/content/npm.tmpl            |   4 +-
 templates/package/content/nuget.tmpl          |   2 +-
 templates/package/content/pub.tmpl            |   2 +-
 templates/package/content/pypi.tmpl           |   2 +-
 templates/package/content/rpm.tmpl            |   2 +-
 templates/package/content/rubygems.tmpl       |   4 +-
 templates/package/content/swift.tmpl          |   4 +-
 templates/package/content/vagrant.tmpl        |   2 +-
 templates/package/shared/cargo.tmpl           |   2 +-
 .../package/shared/cleanup_rules/edit.tmpl    |   2 +-
 templates/package/shared/list.tmpl            |   8 +-
 templates/package/shared/versionlist.tmpl     |   2 +-
 templates/package/view.tmpl                   |   4 +-
 templates/repo/actions/no_workflows.tmpl      |   4 +-
 .../code/recently_pushed_new_branches.tmpl    |   2 +-
 templates/repo/create.tmpl                    |   4 +-
 templates/repo/create_helper.tmpl             |   2 +-
 templates/repo/diff/comments.tmpl             |   6 +-
 templates/repo/diff/compare.tmpl              |   4 +-
 templates/repo/editor/commit_form.tmpl        |   6 +-
 templates/repo/empty.tmpl                     |   2 +-
 templates/repo/home.tmpl                      |   2 +-
 templates/repo/issue/card.tmpl                |   6 +-
 templates/repo/issue/filter_list.tmpl         |   2 +-
 .../repo/issue/labels/edit_delete_label.tmpl  |   4 +-
 templates/repo/issue/labels/label_new.tmpl    |   2 +-
 templates/repo/issue/milestone_issues.tmpl    |   4 +-
 templates/repo/issue/milestones.tmpl          |   4 +-
 templates/repo/issue/new_form.tmpl            |   2 +-
 templates/repo/issue/view_content.tmpl        |   8 +-
 .../repo/issue/view_content/comments.tmpl     | 108 +++++++++---------
 .../repo/issue/view_content/conversation.tmpl |   2 +-
 templates/repo/issue/view_content/pull.tmpl   |   6 +-
 .../view_content/pull_merge_instruction.tmpl  |   2 +-
 .../repo/issue/view_content/sidebar.tmpl      |   8 +-
 templates/repo/issue/view_title.tmpl          |   6 +-
 templates/repo/migrate/codebase.tmpl          |  12 +-
 templates/repo/migrate/git.tmpl               |   4 +-
 templates/repo/migrate/gitbucket.tmpl         |  16 +--
 templates/repo/migrate/gitea.tmpl             |  16 +--
 templates/repo/migrate/github.tmpl            |  16 +--
 templates/repo/migrate/gitlab.tmpl            |  16 +--
 templates/repo/migrate/gogs.tmpl              |  16 +--
 templates/repo/migrate/migrating.tmpl         |  10 +-
 templates/repo/migrate/onedev.tmpl            |  12 +-
 templates/repo/pulls/fork.tmpl                |   2 +-
 templates/repo/pulse.tmpl                     |   4 +-
 templates/repo/settings/deploy_keys.tmpl      |   2 +-
 templates/repo/settings/options.tmpl          |  10 +-
 templates/repo/settings/protected_branch.tmpl |   6 +-
 templates/repo/settings/tags.tmpl             |   2 +-
 templates/repo/user_cards.tmpl                |   2 +-
 templates/repo/wiki/pages.tmpl                |   2 +-
 templates/repo/wiki/revision.tmpl             |   2 +-
 templates/repo/wiki/view.tmpl                 |   4 +-
 templates/shared/actions/runner_edit.tmpl     |   2 +-
 templates/shared/issuelist.tmpl               |   6 +-
 templates/shared/searchbottom.tmpl            |   2 +-
 templates/shared/secrets/add_list.tmpl        |   2 +-
 templates/shared/user/profile_big_avatar.tmpl |   2 +-
 templates/shared/variables/variable_list.tmpl |   2 +-
 templates/status/404.tmpl                     |   2 +-
 templates/user/dashboard/milestones.tmpl      |   4 +-
 templates/user/settings/applications.tmpl     |   2 +-
 templates/user/settings/grants_oauth2.tmpl    |   2 +-
 templates/user/settings/keys_gpg.tmpl         |   4 +-
 templates/user/settings/keys_principal.tmpl   |   2 +-
 templates/user/settings/keys_ssh.tmpl         |   2 +-
 templates/user/settings/packages.tmpl         |   2 +-
 .../user/settings/security/webauthn.tmpl      |   2 +-
 86 files changed, 242 insertions(+), 242 deletions(-)

diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl
index eaf2e7a090..7ae4a4ed6f 100644
--- a/templates/explore/repo_search.tmpl
+++ b/templates/explore/repo_search.tmpl
@@ -36,7 +36,7 @@
 </div>
 {{if and .PageIsExploreRepositories .OnlyShowRelevant}}
 	<div class="ui message explore-relevancy-note">
-		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" ((printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))|Escape) | Safe}}</span>
+		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" ((printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))|Escape)}}</span>
 	</div>
 {{end}}
 <div class="divider"></div>
diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl
index 9abbff6d9c..0d661d53cb 100644
--- a/templates/explore/user_list.tmpl
+++ b/templates/explore/user_list.tmpl
@@ -21,7 +21,7 @@
 							<a href="mailto:{{.Email}}">{{.Email}}</a>
 						</span>
 					{{end}}
-					<span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}</span>
+					<span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix)}}</span>
 				</div>
 			</div>
 		</div>
diff --git a/templates/install.tmpl b/templates/install.tmpl
index e9b267fa1c..05a74cc788 100644
--- a/templates/install.tmpl
+++ b/templates/install.tmpl
@@ -8,7 +8,7 @@
 			<div class="ui attached segment">
 				{{template "base/alert" .}}
 
-				<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker" | Safe}}</p>
+				<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p>
 
 				<form class="ui form" action="{{AppSubUrl}}/" method="post">
 					<!-- Database Settings -->
@@ -72,7 +72,7 @@
 						<div class="inline required field {{if or .Err_DbPath .Err_DbSetting}}error{{end}}">
 							<label for="db_path">{{ctx.Locale.Tr "install.path"}}</label>
 							<input id="db_path" name="db_path" value="{{.db_path}}">
-							<span class="help">{{ctx.Locale.Tr "install.sqlite_helper" | Safe}}</span>
+							<span class="help">{{ctx.Locale.Tr "install.sqlite_helper"}}</span>
 						</div>
 					</div>
 
diff --git a/templates/package/content/alpine.tmpl b/templates/package/content/alpine.tmpl
index a1003cd6ff..7bc22ae382 100644
--- a/templates/package/content/alpine.tmpl
+++ b/templates/package/content/alpine.tmpl
@@ -3,12 +3,12 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></gitea-origin-url>/$branch/$repository</code></pre></div>
-				<p>{{ctx.Locale.Tr "packages.alpine.registry.info" | Safe}}</p>
+				<p>{{ctx.Locale.Tr "packages.alpine.registry.info"}}</p>
 			</div>
 			<div class="field">
-				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key" | Safe}}</label>
+				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key"}}</label>
 				<div class="markup"><pre class="code-block"><code>curl -JO <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></gitea-origin-url></code></pre></div>
 			</div>
 			<div class="field">
@@ -18,7 +18,7 @@
 				</div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Alpine" "https://docs.gitea.com/usage/packages/alpine/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Alpine" "https://docs.gitea.com/usage/packages/alpine/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/cargo.tmpl b/templates/package/content/cargo.tmpl
index 4dd7c3f731..eff6d6c3b3 100644
--- a/templates/package/content/cargo.tmpl
+++ b/templates/package/content/cargo.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cargo.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cargo.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>[registry]
 default = "gitea"
 
@@ -19,7 +19,7 @@ git-fetch-with-cli = true</code></pre></div>
 				<div class="markup"><pre class="code-block"><code>cargo add {{.PackageDescriptor.Package.Name}}@{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/chef.tmpl b/templates/package/content/chef.tmpl
index 0588c6e4b3..c8172b8126 100644
--- a/templates/package/content/chef.tmpl
+++ b/templates/package/content/chef.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></gitea-origin-url>'</code></pre></div>
 			</div>
 			<div class="field">
@@ -11,7 +11,7 @@
 				<div class="markup"><pre class="code-block"><code>knife supermarket install {{.PackageDescriptor.Package.Name}} {{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl
index 862f1c6925..70bfbc4488 100644
--- a/templates/package/content/composer.tmpl
+++ b/templates/package/content/composer.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.composer.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.composer.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>{
 	"repositories": [{
 			"type": "composer",
@@ -17,7 +17,7 @@
 				<div class="markup"><pre class="code-block"><code>composer require {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Composer" "https://docs.gitea.com/usage/packages/composer/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Composer" "https://docs.gitea.com/usage/packages/composer/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/conan.tmpl b/templates/package/content/conan.tmpl
index 55b84d12b1..c5019c6fd6 100644
--- a/templates/package/content/conan.tmpl
+++ b/templates/package/content/conan.tmpl
@@ -11,7 +11,7 @@
 				<div class="markup"><pre class="code-block"><code>conan install --remote=gitea {{.PackageDescriptor.Package.Name}}/{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conan" "https://docs.gitea.com/usage/packages/conan/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conan" "https://docs.gitea.com/usage/packages/conan/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/conda.tmpl b/templates/package/content/conda.tmpl
index 0fd0c3db3f..0172966145 100644
--- a/templates/package/content/conda.tmpl
+++ b/templates/package/content/conda.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>channel_alias: <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url>
 channels:
 &#32;&#32;- <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url>
@@ -16,7 +16,7 @@ default_channels:
 				<div class="markup"><pre class="code-block"><code>conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conda" "https://docs.gitea.com/usage/packages/conda/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conda" "https://docs.gitea.com/usage/packages/conda/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index f5ee902c94..fe393f4388 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -19,7 +19,7 @@
 				<div class="markup"><pre class="code-block"><code>{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/cran.tmpl b/templates/package/content/cran.tmpl
index f9a3f70107..3b5c741701 100644
--- a/templates/package/content/cran.tmpl
+++ b/templates/package/content/cran.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></gitea-origin-url>")))</code></pre></div>
 			</div>
 			<div class="field">
@@ -11,7 +11,7 @@
 				<div class="markup"><pre class="code-block"><code>install.packages("{{.PackageDescriptor.Package.Name}}")</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "CRAN" "https://docs.gitea.com/usage/packages/cran/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "CRAN" "https://docs.gitea.com/usage/packages/cran/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/debian.tmpl b/templates/package/content/debian.tmpl
index 1fde87f329..08b50b46ff 100644
--- a/templates/package/content/debian.tmpl
+++ b/templates/package/content/debian.tmpl
@@ -7,7 +7,7 @@
 				<div class="markup"><pre class="code-block"><code>sudo curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></gitea-origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
 echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></gitea-origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
 sudo apt update</code></pre></div>
-				<p>{{ctx.Locale.Tr "packages.debian.registry.info" | Safe}}</p>
+				<p>{{ctx.Locale.Tr "packages.debian.registry.info"}}</p>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.install"}}</label>
@@ -16,7 +16,7 @@ sudo apt update</code></pre></div>
 				</div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Debian" "https://docs.gitea.com/usage/packages/debian/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Debian" "https://docs.gitea.com/usage/packages/debian/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/generic.tmpl b/templates/package/content/generic.tmpl
index 05aa4aecad..b5a6059f75 100644
--- a/templates/package/content/generic.tmpl
+++ b/templates/package/content/generic.tmpl
@@ -11,7 +11,7 @@ curl -OJ <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescr
 				</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Generic" "https://docs.gitea.com/usage/packages/generic" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Generic" "https://docs.gitea.com/usage/packages/generic"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/go.tmpl b/templates/package/content/go.tmpl
index f98fc69fb6..c74c19095a 100644
--- a/templates/package/content/go.tmpl
+++ b/templates/package/content/go.tmpl
@@ -7,7 +7,7 @@
 				<div class="markup"><pre class="code-block"><code>GOPROXY=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></gitea-origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/helm.tmpl b/templates/package/content/helm.tmpl
index 68e53133f1..3fc217fbb0 100644
--- a/templates/package/content/helm.tmpl
+++ b/templates/package/content/helm.tmpl
@@ -12,7 +12,7 @@ helm repo update</code></pre></div>
 				<div class="markup"><pre class="code-block"><code>helm install {{.PackageDescriptor.Package.Name}} {{AppDomain}}/{{.PackageDescriptor.Package.Name}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Helm" "https://docs.gitea.com/usage/packages/helm/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Helm" "https://docs.gitea.com/usage/packages/helm/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl
index b2cd567e16..9c694094b9 100644
--- a/templates/package/content/maven.tmpl
+++ b/templates/package/content/maven.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>&lt;repositories&gt;
 	&lt;repository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
@@ -24,7 +24,7 @@
 &lt;/distributionManagement&gt;</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.install" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>&lt;dependency&gt;
 	&lt;groupId&gt;{{.PackageDescriptor.Metadata.GroupID}}&lt;/groupId&gt;
 	&lt;artifactId&gt;{{.PackageDescriptor.Metadata.ArtifactID}}&lt;/artifactId&gt;
@@ -40,7 +40,7 @@
 				<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl
index 882e999bed..bf15ec34e9 100644
--- a/templates/package/content/npm.tmpl
+++ b/templates/package/content/npm.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></gitea-origin-url></code></pre></div>
 			</div>
 			<div class="field">
@@ -15,7 +15,7 @@
 				<div class="markup"><pre class="code-block"><code>&quot;{{.PackageDescriptor.Package.Name}}&quot;: &quot;{{.PackageDescriptor.Version.Version}}&quot;</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "npm" "https://docs.gitea.com/usage/packages/npm/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "npm" "https://docs.gitea.com/usage/packages/npm/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl
index 04dac89843..f84288629d 100644
--- a/templates/package/content/nuget.tmpl
+++ b/templates/package/content/nuget.tmpl
@@ -11,7 +11,7 @@
 				<div class="markup"><pre class="code-block"><code>dotnet add package --source {{.PackageDescriptor.Owner.Name}} --version {{.PackageDescriptor.Version.Version}} {{.PackageDescriptor.Package.Name}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "NuGet" "https://docs.gitea.com/usage/packages/nuget/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "NuGet" "https://docs.gitea.com/usage/packages/nuget/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/pub.tmpl b/templates/package/content/pub.tmpl
index 8657d55dbf..e0608e533f 100644
--- a/templates/package/content/pub.tmpl
+++ b/templates/package/content/pub.tmpl
@@ -7,7 +7,7 @@
 				<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></gitea-origin-url></code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl
index ef9beb4280..d0ce2cd65d 100644
--- a/templates/package/content/pypi.tmpl
+++ b/templates/package/content/pypi.tmpl
@@ -7,7 +7,7 @@
 				<div class="markup"><pre class="code-block"><code>pip install --index-url <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></gitea-origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl
index 0f128fd3fb..28d875fca3 100644
--- a/templates/package/content/rpm.tmpl
+++ b/templates/package/content/rpm.tmpl
@@ -31,7 +31,7 @@ zypper install {{$.PackageDescriptor.Package.Name}}</code></pre>
 				</div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RPM" "https://docs.gitea.com/usage/packages/rpm/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RPM" "https://docs.gitea.com/usage/packages/rpm/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/rubygems.tmpl b/templates/package/content/rubygems.tmpl
index 180ff60f7d..e19aab7080 100644
--- a/templates/package/content/rubygems.tmpl
+++ b/templates/package/content/rubygems.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install" | Safe}}:</label>
+				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install"}}:</label>
 				<div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version &quot;{{.PackageDescriptor.Version.Version}}&quot; --source &quot;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></gitea-origin-url>&quot;</code></pre></div>
 			</div>
 			<div class="field">
@@ -13,7 +13,7 @@
 end</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RubyGems" "https://docs.gitea.com/usage/packages/rubygems/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RubyGems" "https://docs.gitea.com/usage/packages/rubygems/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl
index ca36033df9..819cc7f3d0 100644
--- a/templates/package/content/swift.tmpl
+++ b/templates/package/content/swift.tmpl
@@ -7,7 +7,7 @@
 				<div class="markup"><pre class="code-block"><code>swift package-registry set <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></gitea-origin-url></code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>dependencies: [
 	.package(id: "{{.PackageDescriptor.Package.Name}}", from:"{{.PackageDescriptor.Version.Version}}")
 ]</code></pre></div>
@@ -17,7 +17,7 @@
 				<div class="markup"><pre class="code-block"><code>swift package resolve</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Swift" "https://docs.gitea.com/usage/packages/swift/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Swift" "https://docs.gitea.com/usage/packages/swift/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl
index bbb461e4fb..cd294b4ea5 100644
--- a/templates/package/content/vagrant.tmpl
+++ b/templates/package/content/vagrant.tmpl
@@ -7,7 +7,7 @@
 				<div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></gitea-origin-url>"</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/shared/cargo.tmpl b/templates/package/shared/cargo.tmpl
index b452065881..7652231465 100644
--- a/templates/package/shared/cargo.tmpl
+++ b/templates/package/shared/cargo.tmpl
@@ -18,7 +18,7 @@
 			<button class="ui primary button">{{ctx.Locale.Tr "packages.owner.settings.cargo.rebuild"}}</button>
 		</form>
 		<div class="field">
-			<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/" | Safe}}</label>
+			<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/"}}</label>
 		</div>
 	</div>
 </div>
diff --git a/templates/package/shared/cleanup_rules/edit.tmpl b/templates/package/shared/cleanup_rules/edit.tmpl
index 8729494412..138a90791c 100644
--- a/templates/package/shared/cleanup_rules/edit.tmpl
+++ b/templates/package/shared/cleanup_rules/edit.tmpl
@@ -40,7 +40,7 @@
 		<div class="field {{if .Err_KeepPattern}}error{{end}}">
 			<label>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</label>
 			<input name="keep_pattern" type="text" value="{{.CleanupRule.KeepPattern}}">
-			<p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container" | Safe}}</p>
+			<p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container"}}</p>
 		</div>
 		<div class="divider"></div>
 		<p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.remove.title"}}</p>
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index 740a96bb8d..8c8b113c97 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -30,9 +30,9 @@
 						{{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}}
 					{{end}}
 					{{if $hasRepositoryAccess}}
-						{{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.Link (.Repository.FullName | Escape) | Safe}}
+						{{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.Link (.Repository.FullName | Escape)}}
 					{{else}}
-						{{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}}
+						{{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape)}}
 					{{end}}
 				</div>
 			</div>
@@ -45,9 +45,9 @@
 				<h2>{{ctx.Locale.Tr "packages.empty"}}</h2>
 				{{if and .Repository .CanWritePackages}}
 					{{$packagesUrl := URLJoin .Owner.HomeLink "-" "packages"}}
-					<p>{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl | Safe}}</p>
+					<p>{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl}}</p>
 				{{end}}
-				<p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/" | Safe}}</p>
+				<p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}</p>
 			</div>
 		{{else}}
 			<p class="gt-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl
index fcf3030fe6..4b22dc22b2 100644
--- a/templates/package/shared/versionlist.tmpl
+++ b/templates/package/shared/versionlist.tmpl
@@ -25,7 +25,7 @@
 			<div class="flex-item-main">
 				<a class="flex-item-title" href="{{.FullWebLink}}">{{.Version.LowerVersion}}</a>
 				<div class="flex-item-body">
-					{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape)}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 553a46cfad..65502a6e4d 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -10,9 +10,9 @@
 			<div>
 				{{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}
 				{{if .HasRepositoryAccess}}
-					{{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.Link (.PackageDescriptor.Repository.FullName | Escape) | Safe}}
+					{{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.Link (.PackageDescriptor.Repository.FullName | Escape)}}
 				{{else}}
-					{{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape)}}
 				{{end}}
 			</div>
 		</div>
diff --git a/templates/repo/actions/no_workflows.tmpl b/templates/repo/actions/no_workflows.tmpl
index af1f28e8cf..009313581e 100644
--- a/templates/repo/actions/no_workflows.tmpl
+++ b/templates/repo/actions/no_workflows.tmpl
@@ -2,7 +2,7 @@
 	{{svg "octicon-no-entry" 48}}
 	<h2>{{ctx.Locale.Tr "actions.runs.no_workflows"}}</h2>
 	{{if and .CanWriteCode .CanWriteActions}}
-		<p>{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/" | Safe}}</p>
+		<p>{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/"}}</p>
 	{{end}}
-	<p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/" | Safe}}</p>
+	<p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/"}}</p>
 </div>
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index 8910a9e5b6..73c9c45178 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -2,7 +2,7 @@
 	<div class="ui positive message gt-df gt-ac">
 		<div class="gt-f1">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
-			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince | Safe}}
+			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince}}
 		</div>
 		<a role="button" class="ui compact positive button gt-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
 			{{ctx.Locale.Tr "repo.pulls.compare_changes"}}
diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl
index 66f73fb398..d6ff22b7ab 100644
--- a/templates/repo/create.tmpl
+++ b/templates/repo/create.tmpl
@@ -51,10 +51,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 						<span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span>
diff --git a/templates/repo/create_helper.tmpl b/templates/repo/create_helper.tmpl
index 653955efc9..6ca691592c 100644
--- a/templates/repo/create_helper.tmpl
+++ b/templates/repo/create_helper.tmpl
@@ -1,3 +1,3 @@
 {{if not $.DisableMigrations}}
-	<p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" ((print AppSubUrl "/repo/migrate")|Escape) | Safe}}</p>
+	<p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" ((print AppSubUrl "/repo/migrate")|Escape)}}</p>
 {{end}}
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 2fbfe2fd6a..b3d06ed6bc 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -16,17 +16,17 @@
 						{{.OriginalAuthor}}
 					</span>
 					<span class="text grey muted-links">
-						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}}
 					</span>
 					<span class="text migrate">
 						{{if $.root.Repository.OriginalURL}}
-							({{ctx.Locale.Tr "repo.migrated_from" ($.root.Repository.OriginalURL | Escape) ($.root.Repository.GetOriginalURLHostname | Escape) | Safe}})
+							({{ctx.Locale.Tr "repo.migrated_from" ($.root.Repository.OriginalURL | Escape) ($.root.Repository.GetOriginalURLHostname | Escape)}})
 						{{end}}
 					</span>
 				{{else}}
 					<span class="text grey muted-links">
 						{{template "shared/user/namelink" .Poster}}
-						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}}
 					</span>
 				{{end}}
 			</div>
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index 15574ad988..7a618ba8e6 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -194,7 +194,7 @@
 		{{if .HasPullRequest}}
 			<div class="ui segment grid title">
 				<div class="twelve wide column issue-title">
-					{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}}
+					{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index}}
 					<h1>
 						<span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}}</span>
 						<span class="index">#{{.PullRequest.Issue.Index}}</span>
@@ -220,7 +220,7 @@
 					{{if .Repository.ArchivedUnix.IsZero}}
 						{{ctx.Locale.Tr "repo.archive.title"}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}}
+						{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}}
 					{{end}}
 				</div>
 			{{end}}
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index 34dde576a1..c8f062b5c5 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -26,7 +26,7 @@
 					<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
 					<label>
 						{{svg "octicon-git-commit"}}
-						{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" (.BranchName|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" (.BranchName|Escape)}}
 						{{if not .CanCommitToBranch.CanCommitToBranch}}
 						<div class="ui visible small warning message">
 							{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
@@ -50,9 +50,9 @@
 					<label>
 						{{svg "octicon-git-pull-request"}}
 						{{if .CanCreatePullRequest}}
-							{{ctx.Locale.Tr "repo.editor.create_new_branch" | Safe}}
+							{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.editor.create_new_branch_np" | Safe}}
+							{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
 						{{end}}
 					</label>
 				</div>
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index c1ec483b77..62194abe50 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -10,7 +10,7 @@
 						{{if .Repository.ArchivedUnix.IsZero}}
 							{{ctx.Locale.Tr "repo.archive.title"}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}}
+							{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}}
 						{{end}}
 					</div>
 				{{end}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index d91dc4394e..d4b19978d3 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -57,7 +57,7 @@
 				{{if .Repository.ArchivedUnix.IsZero}}
 					{{ctx.Locale.Tr "repo.archive.title"}}
 				{{else}}
-					{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}}
+					{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}}
 				{{end}}
 			</div>
 		{{end}}
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 14d08fc0ef..7b71bd724e 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -23,11 +23,11 @@
 				{{if not $.Page.Repository}}{{.Repo.FullName}}{{end}}#{{.Index}}
 				{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
 				{{if .OriginalAuthor}}
-					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}}
+					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape)}}
 				{{else if gt .Poster.ID 0}}
-					{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape)}}
 				{{else}}
-					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape)}}
 				{{end}}
 			</span>
 		</div>
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index 511ef7f397..9d3341cc81 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -21,7 +21,7 @@
 				</i>
 			</label>
 		</div>
-		<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude" | Safe}}</span>
+		<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
 		<div class="divider"></div>
 		<a class="{{if .AllLabels}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
 		<a class="{{if .NoLabel}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index f41b4ee2c6..7ddc38a387 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -29,9 +29,9 @@
 					<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
 				</div>
 				<br>
-				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}}</small>
+				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
 				<div class="desc gt-ml-2 gt-mt-3 gt-hidden label-exclusive-warning">
-					{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning" | Safe}}
+					{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}
 				</div>
 				<br>
 			</div>
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl
index e7fb1e5ff6..2b2b2336c4 100644
--- a/templates/repo/issue/labels/label_new.tmpl
+++ b/templates/repo/issue/labels/label_new.tmpl
@@ -17,7 +17,7 @@
 					<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
 				</div>
 				<br>
-				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}}</small>
+				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
 			</div>
 			<div class="field">
 				<label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label>
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index ea19518efa..d9495d9b77 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -31,7 +31,7 @@
 				<div classs="gt-df gt-ac">
 					{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}}
 					{{if .IsClosed}}
-						{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}}
+						{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
 					{{else}}
 
 						{{if .Milestone.DeadlineString}}
@@ -45,7 +45,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}</div>
+				<div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}</div>
 				{{if .TotalTrackedTime}}
 					<div data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
 						{{svg "octicon-clock"}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 3d4bbfd8b1..698e3fffba 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -47,14 +47,14 @@
 							{{if .UpdatedUnix}}
 								<div class="flex-text-block">
 									{{svg "octicon-clock"}}
-									{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale) | Safe}}
+									{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale)}}
 								</div>
 							{{end}}
 							<div class="flex-text-block">
 								{{if .IsClosed}}
 									{{$closedDate:= TimeSinceUnix .ClosedDateUnix ctx.Locale}}
 									{{svg "octicon-clock" 14}}
-									{{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}}
+									{{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
 								{{else}}
 									{{if .DeadlineString}}
 										<span class="flex-text-inline {{if .IsOverdue}}text red{{end}}">
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 04ae8456bb..d1cbba6873 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -13,7 +13,7 @@
 					<div class="field">
 						<input name="title" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" autofocus required maxlength="255" autocomplete="off">
 						{{if .PageIsComparePull}}
-							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div>
+							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape)}}</div>
 						{{end}}
 					</div>
 					{{if .Fields}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index ed444f6dce..793772ecd0 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -28,10 +28,10 @@
 									{{.Issue.OriginalAuthor}}
 								</span>
 								<span class="text grey muted-links">
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr}}
 								</span>
 								<span class="text migrate">
-									{{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" (.Repository.OriginalURL|Escape) (.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}
+									{{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" (.Repository.OriginalURL|Escape) (.Repository.GetOriginalURLHostname|Escape)}}){{end}}
 								</span>
 							{{else}}
 								<a class="inline-timeline-avatar" href="{{.Issue.Poster.HomeLink}}">
@@ -39,7 +39,7 @@
 								</a>
 								<span class="text grey muted-links">
 									{{template "shared/user/authorlink" .Issue.Poster}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr}}
 								</span>
 							{{end}}
 						</div>
@@ -133,7 +133,7 @@
 					</div>
 				{{else}}
 					<div class="ui warning message">
-						{{ctx.Locale.Tr "repo.issues.sign_in_require_desc" (.SignInLink|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.sign_in_require_desc" (.SignInLink|Escape)}}
 					</div>
 				{{end}}
 			{{end}}{{/* end if: .IsSigned */}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index a10909b3fc..597f025470 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -33,10 +33,10 @@
 									{{.OriginalAuthor}}
 								</span>
 								<span class="text grey muted-links">
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}} {{if $.Repository.OriginalURL}}
 								</span>
 								<span class="text migrate">
-									({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}
+									({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape)}}){{end}}
 								</span>
 							{{else}}
 								{{if gt .Poster.ID 0}}
@@ -46,7 +46,7 @@
 								{{end}}
 								<span class="text grey muted-links">
 									{{template "shared/user/authorlink" .Poster}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}}
 								</span>
 							{{end}}
 						</div>
@@ -85,9 +85,9 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if .Issue.IsPull}}
-						{{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -98,9 +98,9 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if .Issue.IsPull}}
-						{{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -138,7 +138,7 @@
 				{{if eq .RefAction 3}}<del>{{end}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}}
+					{{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom}}
 				</span>
 				{{if eq .RefAction 3}}</del>{{end}}
 
@@ -152,7 +152,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr}}
 				</span>
 				<div class="detail">
 					{{svg "octicon-git-commit"}}
@@ -167,11 +167,11 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
 						{{if and .AddedLabels (not .RemovedLabels)}}
-							{{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}}
+							{{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr}}
 						{{else if and (not .AddedLabels) .RemovedLabels}}
-							{{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}}
+							{{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}}
 						{{end}}
 					</span>
 				</div>
@@ -182,7 +182,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}}
+					{{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr}}{{end}}
 				</span>
 			</div>
 		{{else if and (eq .Type 9) (gt .AssigneeID 0)}}
@@ -193,9 +193,9 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Assignee}}
 						{{if eq .Poster.ID .Assignee.ID}}
-							{{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr}}
 						{{end}}
 					</span>
 				{{else}}
@@ -203,9 +203,9 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Assignee}}
 						{{if eq .Poster.ID .AssigneeID}}
-							{{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr}}
 						{{end}}
 					</span>
 				{{end}}
@@ -216,7 +216,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 11}}
@@ -225,7 +225,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 12}}
@@ -234,7 +234,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 13}}
@@ -243,7 +243,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}}
 				</span>
 				{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
 				<div class="detail">
@@ -262,7 +262,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}}
 				</span>
 				{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
 				<div class="detail">
@@ -281,7 +281,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 16}}
@@ -290,7 +290,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 17}}
@@ -303,7 +303,7 @@
 					{{if eq (len $parsedDeadline) 2}}
 						{{$from := DateTime "long" (index $parsedDeadline 1)}}
 						{{$to := DateTime "long" (index $parsedDeadline 0)}}
-						{{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -313,7 +313,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 19}}
@@ -322,7 +322,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr}}
 				</span>
 				{{if .DependentIssue}}
 					<div class="detail">
@@ -345,7 +345,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr}}
 				</span>
 				{{if .DependentIssue}}
 					<div class="detail">
@@ -389,13 +389,13 @@
 						{{end}}
 
 						{{if eq .Review.Type 1}}
-							{{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}}
 						{{else if eq .Review.Type 2}}
-							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}}
 						{{else if eq .Review.Type 3}}
-							{{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.reject" $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}}
 						{{end}}
 						{{if .Review.Dismissed}}
 							<div class="ui small label">{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}</div>
@@ -419,12 +419,12 @@
 											{{.OriginalAuthor}}
 										</span>
 										<span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span>
-										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span>
+										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape)}}){{end}}</span>
 									{{else}}
 										{{template "shared/user/authorlink" .Poster}}
 									{{end}}
 
-									{{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}}
+									{{ctx.Locale.Tr "repo.issues.review.left_comment"}}
 								</span>
 							</div>
 							<div class="comment-header-right actions gt-df gt-ac">
@@ -474,12 +474,12 @@
 				{{if .Content}}
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
-						{{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr}}
 					</span>
 				{{else}}
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
-						{{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr}}
 					</span>
 				{{end}}
 			</div>
@@ -489,7 +489,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 25}}
@@ -498,7 +498,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					<a{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.Name}}</a>
-					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 26}}
@@ -508,7 +508,7 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 
-					{{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr}}
 				</span>
 				<div class="detail">
 					{{svg "octicon-clock"}}
@@ -529,12 +529,12 @@
 					{{if (gt .AssigneeID 0)}}
 						{{if .RemovedAssignee}}
 							{{if eq .PosterID .AssigneeID}}
-								{{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}}
+								{{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr}}
 							{{else}}
-								{{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}}
+								{{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr}}
 							{{end}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr}}
 						{{end}}
 					{{else}}
 						<!-- If the assigned team is deleted, just displaying "Ghost Team" in the comment -->
@@ -543,9 +543,9 @@
 							{{$teamName = .AssigneeTeam.Name}}
 						{{end}}
 						{{if .RemovedAssignee}}
-							{{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr}}
 						{{end}}
 					{{end}}
 				</span>
@@ -560,9 +560,9 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if .IsForcePush}}
-						{{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr}}
 					{{else}}
-						{{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}}
+						{{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr}}
 					{{end}}
 				</span>
 				{{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}}
@@ -616,7 +616,7 @@
 						{{else}}
 							{{$reviewerName = .Review.OriginalAuthor}}
 						{{end}}
-						{{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr}}
 					</span>
 				</div>
 				{{if .Content}}
@@ -652,11 +652,11 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if and .OldRef .NewRef}}
-						{{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr}}
 					{{else if .OldRef}}
-						{{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -675,8 +675,8 @@
 					{{else}}
 						{{template "shared/user/authorlink" .Poster}}
 					{{end}}
-					{{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}}
-					{{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}}
+					{{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr}}
+					{{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr}}{{end}}
 				</span>
 			</div>
 		{{else if or (eq .Type 36) (eq .Type 37)}}
@@ -685,8 +685,8 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}}
-					{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}}
+					{{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr}}
+					{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
 				</span>
 			</div>
 		{{end}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index fc1d9865f5..1bc850d8cf 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -73,7 +73,7 @@
 									{{else}}
 										{{template "shared/user/authorlink" .Poster}}
 									{{end}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdSubStr | Safe}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdSubStr}}
 								</span>
 							</div>
 							<div class="comment-header-right actions gt-df gt-ac">
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index e86deb8915..13d49b61b7 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -88,7 +88,7 @@
 					</div>
 					{{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}}
 						<button class="ui compact button">
-							{{ctx.Locale.Tr "repo.pulls.remove_prefix" (.WorkInProgressPrefix|Escape) | Safe}}
+							{{ctx.Locale.Tr "repo.pulls.remove_prefix" (.WorkInProgressPrefix|Escape)}}
 						</button>
 					{{end}}
 				</div>
@@ -127,7 +127,7 @@
 				{{else if .IsBlockedByChangedProtectedFiles}}
 					<div class="item">
 						{{svg "octicon-x"}}
-						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n" | Safe}}
+						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}}
 					</div>
 					<ul>
 						{{range .ChangedProtectedFiles}}
@@ -334,7 +334,7 @@
 				{{else if .IsBlockedByChangedProtectedFiles}}
 					<div class="item text red">
 						{{svg "octicon-x"}}
-						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n" | Safe}}
+						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}}
 					</div>
 					<ul>
 						{{range .ChangedProtectedFiles}}
diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
index a214f29786..a2269feeaf 100644
--- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl
+++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
@@ -1,5 +1,5 @@
 <div class="divider"></div>
-<div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint" | Safe}} </div>
+<div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}} </div>
 <div class="instruct-content gt-mt-3 gt-hidden">
 	<div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}</div>
 	{{$localBranch := .PullRequest.HeadBranch}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 22f67ade7b..bb45b07421 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -101,7 +101,7 @@
 				{{range .OriginalReviews}}
 					<div class="item gt-df gt-ac gt-py-3">
 						<div class="gt-df gt-ac gt-f1">
-							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" ($.Repository.GetOriginalURLHostname|Escape) | Safe}}">
+							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" ($.Repository.GetOriginalURLHostname|Escape)}}">
 								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "gt-mr-3"}}
 								{{.OriginalAuthor}}
 							</a>
@@ -116,7 +116,7 @@
 		{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
 			<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{(index .PullRequestWorkInProgressPrefixes 0| Escape)}}" data-update-url="{{.Issue.Link}}/title">
 				<a class="muted">
-					{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}
+					{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0| Escape)}}
 				</a>
 			</div>
 		{{end}}
@@ -300,7 +300,7 @@
 					{{else}}
 						{{if .HasUserStopwatch}}
 							<div class="ui warning message">
-								{{ctx.Locale.Tr "repo.issues.tracking_already_started" (.OtherStopwatchURL|Escape) | Safe}}
+								{{ctx.Locale.Tr "repo.issues.tracking_already_started" (.OtherStopwatchURL|Escape)}}
 							</div>
 						{{end}}
 						<button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'>
@@ -332,7 +332,7 @@
 		{{if .WorkingUsers}}
 			<div class="divider"></div>
 			<div class="ui comments">
-				<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time) | Safe}}</strong></span>
+				<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
 				<div>
 					{{range $user, $trackedtime := .WorkingUsers}}
 						<div class="comment gt-mt-3">
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 582e9864fb..9b4657b634 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -104,11 +104,11 @@
 				{{$createdStr:= TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
 				<span class="time-desc">
 					{{if .Issue.OriginalAuthor}}
-						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.OriginalAuthor|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.OriginalAuthor|Escape)}}
 					{{else if gt .Issue.Poster.ID 0}}
-						{{ctx.Locale.Tr "repo.issues.opened_by" $createdStr (.Issue.Poster.HomeLink|Escape) (.Issue.Poster.GetDisplayName|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.opened_by" $createdStr (.Issue.Poster.HomeLink|Escape) (.Issue.Poster.GetDisplayName|Escape)}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.Poster.GetDisplayName|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.Poster.GetDisplayName|Escape)}}
 					{{end}}
 					·
 					{{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}}
diff --git a/templates/repo/migrate/codebase.tmpl b/templates/repo/migrate/codebase.tmpl
index a34a039f8f..439a883863 100644
--- a/templates/repo/migrate/codebase.tmpl
+++ b/templates/repo/migrate/codebase.tmpl
@@ -35,22 +35,22 @@
 							<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label>
 							</div>
 						</div>
 					</div>
@@ -90,10 +90,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl
index 7fe4fbc672..db01b8d858 100644
--- a/templates/repo/migrate/git.tmpl
+++ b/templates/repo/migrate/git.tmpl
@@ -64,10 +64,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gitbucket.tmpl b/templates/repo/migrate/gitbucket.tmpl
index d07351e727..d1f1db99ba 100644
--- a/templates/repo/migrate/gitbucket.tmpl
+++ b/templates/repo/migrate/gitbucket.tmpl
@@ -34,7 +34,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 
@@ -44,29 +44,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -106,10 +106,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gitea.tmpl b/templates/repo/migrate/gitea.tmpl
index a40886b7a5..143f220449 100644
--- a/templates/repo/migrate/gitea.tmpl
+++ b/templates/repo/migrate/gitea.tmpl
@@ -30,7 +30,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 
@@ -40,29 +40,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -102,10 +102,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/github.tmpl b/templates/repo/migrate/github.tmpl
index 07f8216fcb..dfb2b4bc46 100644
--- a/templates/repo/migrate/github.tmpl
+++ b/templates/repo/migrate/github.tmpl
@@ -33,7 +33,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 					<div id="migrate_items">
@@ -42,29 +42,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -104,10 +104,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl
index 623822df11..76c2828257 100644
--- a/templates/repo/migrate/gitlab.tmpl
+++ b/templates/repo/migrate/gitlab.tmpl
@@ -30,7 +30,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 					<div id="migrate_items">
@@ -39,29 +39,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -101,10 +101,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gogs.tmpl b/templates/repo/migrate/gogs.tmpl
index 095efd5d60..b01d0eeb67 100644
--- a/templates/repo/migrate/gogs.tmpl
+++ b/templates/repo/migrate/gogs.tmpl
@@ -30,7 +30,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 
@@ -40,18 +40,18 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 						<!-- Gogs do not support it
@@ -59,11 +59,11 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						-->
@@ -104,10 +104,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl
index 48411e2da2..1d5a231db8 100644
--- a/templates/repo/migrate/migrating.tmpl
+++ b/templates/repo/migrate/migrating.tmpl
@@ -21,14 +21,14 @@
 					<div class="ui stackable middle very relaxed page grid">
 						<div class="sixteen wide center aligned centered column">
 							<div id="repo_migrating_progress">
-								<p>{{ctx.Locale.Tr "repo.migrate.migrating" .CloneAddr | Safe}}</p>
+								<p>{{ctx.Locale.Tr "repo.migrate.migrating" .CloneAddr}}</p>
 								<p id="repo_migrating_progress_message"></p>
 							</div>
 							<div id="repo_migrating_failed" class="gt-hidden">
 								{{if .CloneAddr}}
-									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}</p>
+									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed" .CloneAddr}}</p>
 								{{else}}
-									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed_no_addr" | Safe}}</p>
+									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed_no_addr"}}</p>
 								{{end}}
 								<p id="repo_migrating_failed_error"></p>
 							</div>
@@ -57,8 +57,8 @@
 	</div>
 	<div class="content">
 		<div class="ui warning message">
-			{{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br>
-			{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName | Safe}}
+			{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
+			{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
 			{{if .Repository.NumForks}}<br>
 			{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
 			{{end}}
diff --git a/templates/repo/migrate/onedev.tmpl b/templates/repo/migrate/onedev.tmpl
index b06e6929a1..8b2a2d8730 100644
--- a/templates/repo/migrate/onedev.tmpl
+++ b/templates/repo/migrate/onedev.tmpl
@@ -35,22 +35,22 @@
 							<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 						</div>
 					</div>
@@ -90,10 +90,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl
index 94de4d78eb..f0907f409b 100644
--- a/templates/repo/pulls/fork.tmpl
+++ b/templates/repo/pulls/fork.tmpl
@@ -47,7 +47,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui disabled checkbox">
 							<input type="checkbox" disabled {{if .IsPrivate}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 						</div>
 						<span class="help">{{ctx.Locale.Tr "repo.fork_visibility_helper"}}</span>
 					</div>
diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl
index ccd7ebf6b5..e6a59ea8c6 100644
--- a/templates/repo/pulse.tmpl
+++ b/templates/repo/pulse.tmpl
@@ -33,7 +33,7 @@
 				<a class="table-cell tiny background light grey"></a>
 			</div>
 			{{end}}
-			{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
+			{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount}}
 		</div>
 	{{end}}
 	{{if .Permission.CanRead $.UnitTypeIssues}}
@@ -48,7 +48,7 @@
 				<a class="table-cell tiny background light grey"></a>
 			</div>
 			{{end}}
-			{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
+			{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount}}
 		</div>
 	{{end}}
 </div>
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index a283150c60..3ea854ef88 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -55,7 +55,7 @@
 									{{.Fingerprint}}
 								</div>
 								<div class="flex-item-body">
-									<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} —  {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}} - <span>{{ctx.Locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{ctx.Locale.Tr "settings.can_write_info"}} {{end}}</span></i>
+									<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} —  {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}} - <span>{{ctx.Locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{ctx.Locale.Tr "settings.can_write_info"}} {{end}}</span></i>
 								</div>
 							</div>
 							<div class="flex-item-trailing">
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index f7f448fdf2..7122778f06 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -32,7 +32,7 @@
 							{{else}}
 							<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}{{if and $.ForcePrivate .Repository.IsPrivate}} readonly{{end}}>
 							{{end}}
-							<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}} {{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</label>
+							<label>{{ctx.Locale.Tr "repo.visibility_helper"}} {{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</label>
 						</div>
 					</div>
 				{{end}}
@@ -933,8 +933,8 @@
 		</div>
 		<div class="content">
 			<div class="ui warning message">
-				{{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br>
-				{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName | Safe}}
+				{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
+				{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
 				{{if .Repository.NumForks}}<br>
 				{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
 				{{end}}
@@ -968,8 +968,8 @@
 		</div>
 		<div class="content">
 			<div class="ui warning message">
-				{{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br>
-				{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name | Safe}}
+				{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
+				{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}}
 			</div>
 			<form class="ui form" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index 9c0fbddf06..e2f841f758 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -10,17 +10,17 @@
 					<label>{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern"}}</label>
 					<input name="rule_name" type="text" value="{{.Rule.RuleName}}">
 					<input name="rule_id" type="hidden" value="{{.Rule.ID}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc" | Safe}}</p>
+					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc"}}</p>
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns"}}</label>
 					<input name="protected_file_patterns" type="text" value="{{.Rule.ProtectedFilePatterns}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc" | Safe}}</p>
+					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc"}}</p>
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns"}}</label>
 					<input name="unprotected_file_patterns" type="text" value="{{.Rule.UnprotectedFilePatterns}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc" | Safe}}</p>
+					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc"}}</p>
 				</div>
 
 				{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl
index ed7762acc5..e4fcf2ee6b 100644
--- a/templates/repo/settings/tags.tmpl
+++ b/templates/repo/settings/tags.tmpl
@@ -21,7 +21,7 @@
 										<div class="ui input">
 											<input class="prompt" name="name_pattern" autocomplete="off" value="{{.name_pattern}}" placeholder="v*" autofocus required>
 										</div>
-										<div class="help">{{ctx.Locale.Tr "repo.settings.tags.protection.pattern.description" | Safe}}</div>
+										<div class="help">{{ctx.Locale.Tr "repo.settings.tags.protection.pattern.description"}}</div>
 									</div>
 								</div>
 								<div class="whitelist field">
diff --git a/templates/repo/user_cards.tmpl b/templates/repo/user_cards.tmpl
index 12fb23f067..5accc2c7af 100644
--- a/templates/repo/user_cards.tmpl
+++ b/templates/repo/user_cards.tmpl
@@ -18,7 +18,7 @@
 					{{else if .Location}}
 						{{svg "octicon-location"}} {{.Location}}
 					{{else}}
-						{{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}
+						{{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix)}}
 					{{end}}
 				</div>
 			</li>
diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl
index a1bf13287c..22eb2619f9 100644
--- a/templates/repo/wiki/pages.tmpl
+++ b/templates/repo/wiki/pages.tmpl
@@ -20,7 +20,7 @@
 							<a class="wiki-git-entry" href="{{$.RepoLink}}/wiki/{{.GitEntryName | PathEscape}}" data-tooltip-content="{{ctx.Locale.Tr "repo.wiki.original_git_entry_tooltip"}}">{{svg "octicon-chevron-right"}}</a>
 						</td>
 						{{$timeSince := TimeSinceUnix .UpdatedUnix ctx.Locale}}
-						<td class="text right">{{ctx.Locale.Tr "repo.wiki.last_updated" $timeSince | Safe}}</td>
+						<td class="text right">{{ctx.Locale.Tr "repo.wiki.last_updated" $timeSince}}</td>
 					</tr>
 				{{end}}
 			</tbody>
diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
index 95b3cd0920..647c331d55 100644
--- a/templates/repo/wiki/revision.tmpl
+++ b/templates/repo/wiki/revision.tmpl
@@ -10,7 +10,7 @@
 					{{$title}}
 					<div class="ui sub header gt-word-break">
 						{{$timeSince := TimeSince .Author.When ctx.Locale}}
-						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince | Safe}}
+						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
 					</div>
 				</div>
 			</div>
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 039ff3f179..5b296dc2af 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -40,7 +40,7 @@
 					{{$title}}
 					<div class="ui sub header">
 						{{$timeSince := TimeSince .Author.When ctx.Locale}}
-						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince | Safe}}
+						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
 					</div>
 				</div>
 				<div class="eight wide right aligned column">
@@ -107,7 +107,7 @@
 		{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" ($title|Escape) | Safe}}</p>
+		<p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" ($title|Escape)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index c10901501d..fbc730b288 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -89,7 +89,7 @@
 			{{ctx.Locale.Tr "actions.runners.delete_runner_header"}}
 		</div>
 		<div class="content">
-			<p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice" | Safe}}</p>
+			<p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice"}}</p>
 		</div>
 		{{template "base/modal_actions_confirm" .}}
 	</div>
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index 8fe5aadf2b..7940234ccc 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -62,11 +62,11 @@
 					</a>
 					{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
 					{{if .OriginalAuthor}}
-						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}}
+						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape)}}
 					{{else if gt .Poster.ID 0}}
-						{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}}
+						{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape)}}
 					{{else}}
-						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}}
+						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape)}}
 					{{end}}
 					{{if .IsPull}}
 						<div class="branches flex-text-inline">
diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl
index 55b6cb2909..b123b497c7 100644
--- a/templates/shared/searchbottom.tmpl
+++ b/templates/shared/searchbottom.tmpl
@@ -6,7 +6,7 @@
 		</div>
 		<div class="gt-mr-4">
 			{{if not .result.UpdatedUnix.IsZero}}
-					<span class="ui grey text">{{ctx.Locale.Tr "explore.code_last_indexed_at" (TimeSinceUnix .result.UpdatedUnix ctx.Locale) | Safe}}</span>
+					<span class="ui grey text">{{ctx.Locale.Tr "explore.code_last_indexed_at" (TimeSinceUnix .result.UpdatedUnix ctx.Locale)}}</span>
 			{{end}}
 		</div>
 </div>
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index 7192f31fb2..4fbd8ddcfd 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -28,7 +28,7 @@
 			</div>
 			<div class="flex-item-trailing">
 				<span class="color-text-light-2">
-					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}
+					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}
 				</span>
 				<button class="ui btn interact-bg link-action gt-p-3"
 					data-url="{{$.Link}}/delete?id={{.ID}}"
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 9ea8334881..19a3b25cc5 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -81,7 +81,7 @@
 					</li>
 				{{end}}
 			{{end}}
-			<li>{{svg "octicon-calendar"}} <span>{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .ContextUser.CreatedUnix) | Safe}}</span></li>
+			<li>{{svg "octicon-calendar"}} <span>{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .ContextUser.CreatedUnix)}}</span></li>
 			{{if and .Orgs .HasOrgsVisible}}
 			<li>
 				<ul class="user-orgs">
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index fc5cd966fc..8e262d016c 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -30,7 +30,7 @@
 			</div>
 			<div class="flex-item-trailing">
 				<span class="color-text-light-2">
-					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}
+					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}
 				</span>
 				<button class="btn interact-bg gt-p-3 show-modal"
 					data-tooltip-content="{{ctx.Locale.Tr "actions.variables.edit"}}"
diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl
index 74bb8762bd..a8cd3d3290 100644
--- a/templates/status/404.tmpl
+++ b/templates/status/404.tmpl
@@ -3,7 +3,7 @@
 	{{if .IsRepo}}{{template "repo/header" .}}{{end}}
 	<div class="ui container center">
 		<p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/404.png" alt="404"></p>
-		<p>{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404" | Safe}}{{end}}</p>
+		<p>{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404"}}{{end}}</p>
 		{{if .NotFoundGoBackURL}}<a class="ui button green" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>{{end}}
 
 		<div class="divider"></div>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 390457a60a..1829021ff4 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -106,14 +106,14 @@
 									{{if .UpdatedUnix}}
 										<div class="flex-text-block">
 											{{svg "octicon-clock"}}
-											{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale) | Safe}}
+											{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale)}}
 										</div>
 									{{end}}
 									<div class="flex-text-block">
 										{{if .IsClosed}}
 											{{$closedDate:= TimeSinceUnix .ClosedDateUnix ctx.Locale}}
 											{{svg "octicon-clock" 14}}
-											{{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}}
+											{{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
 										{{else}}
 											{{if .DeadlineString}}
 												<span{{if .IsOverdue}} class="text red"{{end}}>
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 7553c798dc..e7eb6c8180 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -36,7 +36,7 @@
 								</ul>
 							</details>
 							<div class="flex-item-body">
-								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
+								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
 							</div>
 						</div>
 						<div class="flex-item-trailing">
diff --git a/templates/user/settings/grants_oauth2.tmpl b/templates/user/settings/grants_oauth2.tmpl
index 3c4c6e80d4..92fea1306f 100644
--- a/templates/user/settings/grants_oauth2.tmpl
+++ b/templates/user/settings/grants_oauth2.tmpl
@@ -14,7 +14,7 @@
 				<div class="flex-item-main">
 					<div class="flex-item-title">{{.Application.Name}}</div>
 					<div class="flex-item-body">
-						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}</i>
+						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}</i>
 					</div>
 				</div>
 				<div class="flex-item-trailing">
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index c562aaeab0..43ea667516 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -63,9 +63,9 @@
 						<b>{{ctx.Locale.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.PaddedKeyID}} {{end}}
 					</div>
 					<div class="flex-item-body">
-						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .AddedUnix) | Safe}}</i>
+						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .AddedUnix)}}</i>
 						-
-						<i>{{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateTime "short" .ExpiredUnix) | Safe}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}</i>
+						<i>{{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateTime "short" .ExpiredUnix)}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}</i>
 					</div>
 				</div>
 				<div class="flex-item-trailing">
diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl
index a7ab12dd78..b6acb63c5e 100644
--- a/templates/user/settings/keys_principal.tmpl
+++ b/templates/user/settings/keys_principal.tmpl
@@ -22,7 +22,7 @@
 					<div class="flex-item-main">
 						<div class="flex-item-title">{{.Name}}</div>
 						<div class="flex-item-body">
-							<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} —  {{svg "octicon-info" 16}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
+							<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} —  {{svg "octicon-info" 16}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
 						</div>
 					</div>
 					<div class="flex-item-trailing">
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index 91e8ccfcfa..2d3225e61e 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -53,7 +53,7 @@
 								{{.Fingerprint}}
 						</div>
 						<div class="flex-item-body">
-								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} —	{{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
+								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} —	{{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
 						</div>
 				</div>
 				<div class="flex-item-trailing">
diff --git a/templates/user/settings/packages.tmpl b/templates/user/settings/packages.tmpl
index 1de20fe729..80853eab14 100644
--- a/templates/user/settings/packages.tmpl
+++ b/templates/user/settings/packages.tmpl
@@ -16,7 +16,7 @@
 					<button class="ui primary button">{{ctx.Locale.Tr "packages.owner.settings.chef.keypair"}}</button>
 				</form>
 				<div class="field">
-					<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/" | Safe}}</label>
+					<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/"}}</label>
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/settings/security/webauthn.tmpl b/templates/user/settings/security/webauthn.tmpl
index da6e5977c6..e582b801da 100644
--- a/templates/user/settings/security/webauthn.tmpl
+++ b/templates/user/settings/security/webauthn.tmpl
@@ -12,7 +12,7 @@
 				<div class="flex-item-main">
 					<div class="flex-item-title">{{.Name}}</div>
 					<div class="flex-item-body">
-						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}</i>
+						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}</i>
 					</div>
 				</div>
 				<div class="flex-item-trailing">

From eaede2de98fbe0ac2156c9f4cd8b5899d2c7cbbf Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Thu, 22 Feb 2024 19:13:25 +0200
Subject: [PATCH 124/679] Remove jQuery from the repo commit functions (#29230)

- Switched to plain JavaScript
- Tested the commit ellipsis button functionality and it works as before
- Tested the commits statuses tippy functionality and it works as before
- Tested the last commit loader functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/465516f8-0ff3-438c-a17e-26cbab82750b)

![action](https://github.com/go-gitea/gitea/assets/20454870/968da210-9382-4b50-a4c2-09419dc86e07)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-commit.js | 96 +++++++++++++++---------------
 1 file changed, 48 insertions(+), 48 deletions(-)

diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js
index fc70ba41e4..7e2f6fa58e 100644
--- a/web_src/js/features/repo-commit.js
+++ b/web_src/js/features/repo-commit.js
@@ -1,70 +1,70 @@
-import $ from 'jquery';
 import {createTippy} from '../modules/tippy.js';
 import {toggleElem} from '../utils/dom.js';
-
-const {csrfToken} = window.config;
+import {parseDom} from '../utils.js';
+import {POST} from '../modules/fetch.js';
 
 export function initRepoEllipsisButton() {
-  $('.js-toggle-commit-body').on('click', function (e) {
-    e.preventDefault();
-    const expanded = $(this).attr('aria-expanded') === 'true';
-    toggleElem($(this).parent().find('.commit-body'));
-    $(this).attr('aria-expanded', String(!expanded));
-  });
+  for (const button of document.querySelectorAll('.js-toggle-commit-body')) {
+    button.addEventListener('click', function (e) {
+      e.preventDefault();
+      const expanded = this.getAttribute('aria-expanded') === 'true';
+      toggleElem(this.parentElement.querySelector('.commit-body'));
+      this.setAttribute('aria-expanded', String(!expanded));
+    });
+  }
 }
 
-export function initRepoCommitLastCommitLoader() {
-  const notReadyEls = document.querySelectorAll('table#repo-files-table tr.notready');
-  if (!notReadyEls.length) return;
-
+export async function initRepoCommitLastCommitLoader() {
   const entryMap = {};
-  const entries = [];
-  for (const el of notReadyEls) {
-    const entryname = el.getAttribute('data-entryname');
-    entryMap[entryname] = $(el);
-    entries.push(entryname);
-  }
 
-  const lastCommitLoaderURL = $('table#repo-files-table').data('lastCommitLoaderUrl');
+  const entries = Array.from(document.querySelectorAll('table#repo-files-table tr.notready'), (el) => {
+    const entryName = el.getAttribute('data-entryname');
+    entryMap[entryName] = el;
+    return entryName;
+  });
 
-  if (entries.length > 200) {
-    $.post(lastCommitLoaderURL, {
-      _csrf: csrfToken,
-    }, (data) => {
-      $('table#repo-files-table').replaceWith(data);
-    });
+  if (entries.length === 0) {
     return;
   }
 
-  $.post(lastCommitLoaderURL, {
-    _csrf: csrfToken,
-    'f': entries,
-  }, (data) => {
-    $(data).find('tr').each((_, row) => {
-      if (row.className === 'commit-list') {
-        $('table#repo-files-table .commit-list').replaceWith(row);
-        return;
-      }
-      // there are other <tr> rows in response (eg: <tr class="has-parent">)
-      // at the moment only the "data-entryname" rows should be processed
-      const entryName = $(row).attr('data-entryname');
-      if (entryName) {
-        entryMap[entryName].replaceWith(row);
-      }
-    });
-  });
+  const lastCommitLoaderURL = document.querySelector('table#repo-files-table').getAttribute('data-last-commit-loader-url');
+
+  if (entries.length > 200) {
+    // For more than 200 entries, replace the entire table
+    const response = await POST(lastCommitLoaderURL);
+    const data = await response.text();
+    document.querySelector('table#repo-files-table').outerHTML = data;
+    return;
+  }
+
+  // For fewer entries, update individual rows
+  const response = await POST(lastCommitLoaderURL, {data: {'f': entries}});
+  const data = await response.text();
+  const doc = parseDom(data, 'text/html');
+  for (const row of doc.querySelectorAll('tr')) {
+    if (row.className === 'commit-list') {
+      document.querySelector('table#repo-files-table .commit-list')?.replaceWith(row);
+      continue;
+    }
+    // there are other <tr> rows in response (eg: <tr class="has-parent">)
+    // at the moment only the "data-entryname" rows should be processed
+    const entryName = row.getAttribute('data-entryname');
+    if (entryName) {
+      entryMap[entryName]?.replaceWith(row);
+    }
+  }
 }
 
 export function initCommitStatuses() {
-  $('[data-tippy="commit-statuses"]').each(function () {
-    const top = $('.repository.file.list').length > 0 || $('.repository.diff').length > 0;
+  for (const element of document.querySelectorAll('[data-tippy="commit-statuses"]')) {
+    const top = document.querySelector('.repository.file.list') || document.querySelector('.repository.diff');
 
-    createTippy(this, {
-      content: this.nextElementSibling,
+    createTippy(element, {
+      content: element.nextElementSibling,
       placement: top ? 'top-start' : 'bottom-start',
       interactive: true,
       role: 'dialog',
       theme: 'box-with-header',
     });
-  });
+  }
 }

From 5ed17d9895bf678374ef5227ca37870c1c170802 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 23 Feb 2024 01:40:53 +0800
Subject: [PATCH 125/679] Ignore the linux anchor point to avoid linux migrate
 failure (#29295)

Fix #28843

This PR will bypass the pushUpdateTag to database failure when
syncAllTags. An error log will be recorded.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/repository/repo.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index fc3af04071..39bdc6adcf 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -352,7 +352,9 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
 		}
 
 		if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil {
-			return fmt.Errorf("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %w", tagName, repo.ID, repo.OwnerName, repo.Name, err)
+			// sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11
+			// this is a tree object, not a tag object which created before git
+			log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err)
 		}
 
 		return nil

From c9d0e63c202827756c637d9ca7bbde685c1984b7 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 23 Feb 2024 02:05:47 +0800
Subject: [PATCH 126/679]  Remove unnecessary "Str2html" modifier from
 templates (#29319)

Follow #29165
---
 routers/web/auth/oauth.go                     |  7 +--
 templates/admin/dashboard.tmpl                |  2 +-
 templates/code/searchcombo.tmpl               |  2 +-
 templates/home.tmpl                           |  8 ++--
 templates/mail/auth/activate.tmpl             |  4 +-
 templates/mail/auth/activate_email.tmpl       |  4 +-
 templates/mail/auth/register_notify.tmpl      |  4 +-
 templates/mail/auth/reset_passwd.tmpl         |  4 +-
 templates/mail/issue/default.tmpl             | 22 ++++-----
 templates/mail/team_invite.tmpl               |  2 +-
 templates/org/settings/delete.tmpl            |  2 +-
 templates/org/settings/labels.tmpl            |  2 +-
 templates/org/team/invite.tmpl                |  2 +-
 templates/org/team/new.tmpl                   |  4 +-
 templates/org/team/sidebar.tmpl               | 10 ++--
 templates/repo/blame.tmpl                     |  4 +-
 templates/repo/branch/list.tmpl               |  2 +-
 templates/repo/create.tmpl                    |  2 +-
 templates/repo/diff/box.tmpl                  |  2 +-
 templates/repo/diff/stats.tmpl                |  2 +-
 templates/repo/empty.tmpl                     |  2 +-
 templates/repo/issue/labels/label_list.tmpl   |  2 +-
 templates/repo/issue/view_content.tmpl        |  2 +-
 templates/repo/migrate/options.tmpl           |  2 +-
 templates/repo/release/list.tmpl              |  2 +-
 templates/repo/search.tmpl                    |  2 +-
 templates/repo/settings/deploy_keys.tmpl      |  2 +-
 templates/repo/settings/githooks.tmpl         |  2 +-
 templates/repo/settings/options.tmpl          |  6 +--
 templates/repo/settings/protected_branch.tmpl |  2 +-
 templates/repo/settings/webhook/dingtalk.tmpl |  2 +-
 templates/repo/settings/webhook/discord.tmpl  |  2 +-
 templates/repo/settings/webhook/feishu.tmpl   |  4 +-
 templates/repo/settings/webhook/gitea.tmpl    |  2 +-
 templates/repo/settings/webhook/gogs.tmpl     |  2 +-
 templates/repo/settings/webhook/matrix.tmpl   |  2 +-
 templates/repo/settings/webhook/msteams.tmpl  |  2 +-
 .../repo/settings/webhook/packagist.tmpl      |  2 +-
 templates/repo/settings/webhook/settings.tmpl |  8 ++--
 templates/repo/settings/webhook/slack.tmpl    |  2 +-
 templates/repo/settings/webhook/telegram.tmpl |  2 +-
 .../repo/settings/webhook/wechatwork.tmpl     |  2 +-
 templates/repo/unicode_escape_prompt.tmpl     |  6 +--
 templates/status/500.tmpl                     |  2 +-
 templates/user/auth/activate.tmpl             |  6 +--
 templates/user/auth/finalize_openid.tmpl      |  2 +-
 templates/user/auth/forgot_passwd.tmpl        |  2 +-
 templates/user/auth/grant.tmpl                |  4 +-
 templates/user/auth/reset_passwd.tmpl         |  6 +--
 templates/user/auth/signin_inner.tmpl         |  2 +-
 templates/user/auth/twofa.tmpl                |  2 +-
 templates/user/dashboard/feeds.tmpl           | 48 +++++++++----------
 templates/user/settings/account.tmpl          |  4 +-
 templates/user/settings/applications.tmpl     |  2 +-
 templates/user/settings/keys_gpg.tmpl         |  2 +-
 templates/user/settings/keys_ssh.tmpl         |  2 +-
 templates/user/settings/security/twofa.tmpl   |  2 +-
 .../user/settings/security/webauthn.tmpl      |  2 +-
 58 files changed, 121 insertions(+), 120 deletions(-)

diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 660fa8fe4e..ee0770ef37 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -9,6 +9,7 @@ import (
 	"errors"
 	"fmt"
 	"html"
+	"html/template"
 	"io"
 	"net/http"
 	"net/url"
@@ -499,11 +500,11 @@ func AuthorizeOAuth(ctx *context.Context) {
 	ctx.Data["Scope"] = form.Scope
 	ctx.Data["Nonce"] = form.Nonce
 	if user != nil {
-		ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name))
+		ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)))
 	} else {
-		ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName))
+		ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)))
 	}
-	ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
+	ctx.Data["ApplicationRedirectDomainHTML"] = template.HTML("<strong>" + html.EscapeString(form.RedirectURI) + "</strong>")
 	// TODO document SESSION <=> FORM
 	err = ctx.Session.Set("client_id", app.ClientID)
 	if err != nil {
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl
index 8088315f17..cc7d338589 100644
--- a/templates/admin/dashboard.tmpl
+++ b/templates/admin/dashboard.tmpl
@@ -2,7 +2,7 @@
 	<div class="admin-setting-content">
 		{{if .NeedUpdate}}
 			<div class="ui negative message flash-error">
-				<p>{{(ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer) | Str2html}}</p>
+				<p>{{ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer}}</p>
 			</div>
 		{{end}}
 		<h4 class="ui top attached header">
diff --git a/templates/code/searchcombo.tmpl b/templates/code/searchcombo.tmpl
index 48dc13b47b..d256890918 100644
--- a/templates/code/searchcombo.tmpl
+++ b/templates/code/searchcombo.tmpl
@@ -7,7 +7,7 @@
 		</div>
 	{{else if .SearchResults}}
 		<h3>
-			{{ctx.Locale.Tr "explore.code_search_results" (.Keyword|Escape) | Str2html}}
+			{{ctx.Locale.Tr "explore.code_search_results" (.Keyword|Escape)}}
 		</h3>
 		{{template "code/searchresults" .}}
 	{{else if .Keyword}}
diff --git a/templates/home.tmpl b/templates/home.tmpl
index 78364431e9..1e5369e7ee 100644
--- a/templates/home.tmpl
+++ b/templates/home.tmpl
@@ -17,7 +17,7 @@
 				{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.install_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.install_desc"}}
 			</p>
 		</div>
 		<div class="eight wide center column">
@@ -25,7 +25,7 @@
 				{{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.platform_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.platform_desc"}}
 			</p>
 		</div>
 	</div>
@@ -35,7 +35,7 @@
 				{{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.lightweight_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.lightweight_desc"}}
 			</p>
 		</div>
 		<div class="eight wide center column">
@@ -43,7 +43,7 @@
 				{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.license_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.license_desc"}}
 			</p>
 		</div>
 	</div>
diff --git a/templates/mail/auth/activate.tmpl b/templates/mail/auth/activate.tmpl
index a15afe3d49..b1bb4cb463 100644
--- a/templates/mail/auth/activate.tmpl
+++ b/templates/mail/auth/activate.tmpl
@@ -8,8 +8,8 @@
 
 {{$activate_url := printf "%suser/activate?code=%s" AppUrl (QueryEscape .Code)}}
 <body>
-	<p>{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName | Str2html}}</p><br>
-	<p>{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives | Str2html}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
+	<p>{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName}}</p><br>
+	<p>{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
diff --git a/templates/mail/auth/activate_email.tmpl b/templates/mail/auth/activate_email.tmpl
index b15cc2a68a..3d32f80a4e 100644
--- a/templates/mail/auth/activate_email.tmpl
+++ b/templates/mail/auth/activate_email.tmpl
@@ -8,8 +8,8 @@
 
 {{$activate_url := printf "%suser/activate_email?code=%s&email=%s" AppUrl (QueryEscape .Code) (QueryEscape .Email)}}
 <body>
-	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br>
-	<p>{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives | Str2html}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
+	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
+	<p>{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
diff --git a/templates/mail/auth/register_notify.tmpl b/templates/mail/auth/register_notify.tmpl
index 3cdb456fb3..ec3e09dd5f 100644
--- a/templates/mail/auth/register_notify.tmpl
+++ b/templates/mail/auth/register_notify.tmpl
@@ -8,10 +8,10 @@
 
 {{$set_pwd_url := printf "%[1]suser/forgot_password" AppUrl}}
 <body>
-	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br>
+	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
 	<p>{{.locale.Tr "mail.register_notify.text_1" AppName}}</p><br>
 	<p>{{.locale.Tr "mail.register_notify.text_2" .Username}}</p><p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p><br>
-	<p>{{.locale.Tr "mail.register_notify.text_3" ($set_pwd_url | Escape) | Str2html}}</p><br>
+	<p>{{.locale.Tr "mail.register_notify.text_3" ($set_pwd_url | Escape)}}</p><br>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
 </body>
diff --git a/templates/mail/auth/reset_passwd.tmpl b/templates/mail/auth/reset_passwd.tmpl
index 172844c954..55b1ecec3f 100644
--- a/templates/mail/auth/reset_passwd.tmpl
+++ b/templates/mail/auth/reset_passwd.tmpl
@@ -8,8 +8,8 @@
 
 {{$recover_url := printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}}
 <body>
-	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br>
-	<p>{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives | Str2html}}</p><p><a href="{{$recover_url}}">{{$recover_url}}</a></p><br>
+	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
+	<p>{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}</p><p><a href="{{$recover_url}}">{{$recover_url}}</a></p><br>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index b5a7ab95cf..54ae726d71 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -16,7 +16,7 @@
 </head>
 
 <body>
-	{{if .IsMention}}<p>{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name | Str2html}}</p>{{end}}
+	{{if .IsMention}}<p>{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name}}</p>{{end}}
 	{{if eq .ActionName "push"}}
 		<p>
 			{{if .Comment.IsForcePush}}
@@ -30,32 +30,32 @@
 
 				{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch ($oldCommitLink|Safe) ($newCommitLink|Safe)}}
 			{{else}}
-				{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}}
+				{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits)}}
 			{{end}}
 		</p>
 	{{end}}
 	<p>
 		{{if eq .ActionName "close"}}
-			{{.locale.Tr "mail.issue.action.close" (Escape .Doer.Name) .Issue.Index | Str2html}}
+			{{.locale.Tr "mail.issue.action.close" (Escape .Doer.Name) .Issue.Index}}
 		{{else if eq .ActionName "reopen"}}
-			{{.locale.Tr "mail.issue.action.reopen" (Escape .Doer.Name) .Issue.Index | Str2html}}
+			{{.locale.Tr "mail.issue.action.reopen" (Escape .Doer.Name) .Issue.Index}}
 		{{else if eq .ActionName "merge"}}
-			{{.locale.Tr "mail.issue.action.merge" (Escape .Doer.Name) .Issue.Index (Escape .Issue.PullRequest.BaseBranch) | Str2html}}
+			{{.locale.Tr "mail.issue.action.merge" (Escape .Doer.Name) .Issue.Index (Escape .Issue.PullRequest.BaseBranch)}}
 		{{else if eq .ActionName "approve"}}
-			{{.locale.Tr "mail.issue.action.approve" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.approve" (Escape .Doer.Name)}}
 		{{else if eq .ActionName "reject"}}
-			{{.locale.Tr "mail.issue.action.reject" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.reject" (Escape .Doer.Name)}}
 		{{else if eq .ActionName "review"}}
-			{{.locale.Tr "mail.issue.action.review" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.review" (Escape .Doer.Name)}}
 		{{else if eq .ActionName "review_dismissed"}}
-			{{.locale.Tr "mail.issue.action.review_dismissed" (Escape .Doer.Name) (Escape .Comment.Review.Reviewer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.review_dismissed" (Escape .Doer.Name) (Escape .Comment.Review.Reviewer.Name)}}
 		{{else if eq .ActionName "ready_for_review"}}
-			{{.locale.Tr "mail.issue.action.ready_for_review" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.ready_for_review" (Escape .Doer.Name)}}
 		{{end}}
 
 		{{- if eq .Body ""}}
 			{{if eq .ActionName "new"}}
-				{{.locale.Tr "mail.issue.action.new" (Escape .Doer.Name) .Issue.Index | Str2html}}
+				{{.locale.Tr "mail.issue.action.new" (Escape .Doer.Name) .Issue.Index}}
 			{{end}}
 		{{else}}
 			{{.Body | Str2html}}
diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl
index d21b7843ec..cb0c0c0a50 100644
--- a/templates/mail/team_invite.tmpl
+++ b/templates/mail/team_invite.tmpl
@@ -5,7 +5,7 @@
 	<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
 </head>
 <body>
-	<p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}</p>
+	<p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName)}}</p>
 	<p>{{.locale.Tr "mail.team_invite.text_2"}}</p><p><a href="{{.InviteURL}}">{{.InviteURL}}</a></p>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 	<p>{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}</p>
diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl
index 2cf8238f57..e1ef471e34 100644
--- a/templates/org/settings/delete.tmpl
+++ b/templates/org/settings/delete.tmpl
@@ -6,7 +6,7 @@
 				</h4>
 				<div class="ui attached error segment">
 					<div class="ui red message">
-						<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt" | Str2html}}</p>
+						<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}</p>
 					</div>
 					<form class="ui form ignore-dirty" id="delete-form" action="{{.Link}}" method="post">
 						{{.CsrfTokenHtml}}
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl
index b12ea8d9f4..8eb7b4584e 100644
--- a/templates/org/settings/labels.tmpl
+++ b/templates/org/settings/labels.tmpl
@@ -2,7 +2,7 @@
 				<div class="org-setting-content">
 					<div class="gt-df gt-ac">
 						<div class="gt-f1">
-							{{ctx.Locale.Tr "org.settings.labels_desc" | Str2html}}
+							{{ctx.Locale.Tr "org.settings.labels_desc"}}
 						</div>
 						<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
 					</div>
diff --git a/templates/org/team/invite.tmpl b/templates/org/team/invite.tmpl
index e003d14757..1167828d14 100644
--- a/templates/org/team/invite.tmpl
+++ b/templates/org/team/invite.tmpl
@@ -7,7 +7,7 @@
 				{{ctx.AvatarUtils.Avatar .Organization 140}}
 			</div>
 			<div class="content">
-				<div class="header">{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name | Str2html}}</div>
+				<div class="header">{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name}}</div>
 				<div class="meta">{{ctx.Locale.Tr "org.teams.invite.by" .Inviter.Name}}</div>
 				<div class="description">{{ctx.Locale.Tr "org.teams.invite.description"}}</div>
 			</div>
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl
index 0178a20fbb..50ef53b91b 100644
--- a/templates/org/team/new.tmpl
+++ b/templates/org/team/new.tmpl
@@ -32,14 +32,14 @@
 									<div class="ui radio checkbox">
 										<input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}>
 										<label>{{ctx.Locale.Tr "org.teams.specific_repositories"}}</label>
-										<span class="help">{{ctx.Locale.Tr "org.teams.specific_repositories_helper" | Str2html}}</span>
+										<span class="help">{{ctx.Locale.Tr "org.teams.specific_repositories_helper"}}</span>
 									</div>
 								</div>
 								<div class="field">
 									<div class="ui radio checkbox">
 										<input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}>
 										<label>{{ctx.Locale.Tr "org.teams.all_repositories"}}</label>
-										<span class="help">{{ctx.Locale.Tr "org.teams.all_repositories_helper" | Str2html}}</span>
+										<span class="help">{{ctx.Locale.Tr "org.teams.all_repositories_helper"}}</span>
 									</div>
 								</div>
 
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl
index 37550ab71f..440fa11dc9 100644
--- a/templates/org/team/sidebar.tmpl
+++ b/templates/org/team/sidebar.tmpl
@@ -27,16 +27,16 @@
 		</div>
 		{{if eq .Team.LowerName "owners"}}
 			<div class="item">
-				{{ctx.Locale.Tr "org.teams.owners_permission_desc" | Str2html}}
+				{{ctx.Locale.Tr "org.teams.owners_permission_desc"}}
 			</div>
 		{{else}}
 			<div class="item">
 				<h3>{{ctx.Locale.Tr "org.team_access_desc"}}</h3>
 				<ul>
 					{{if .Team.IncludesAllRepositories}}
-						<li>{{ctx.Locale.Tr "org.teams.all_repositories" | Str2html}}</li>
+						<li>{{ctx.Locale.Tr "org.teams.all_repositories"}}</li>
 					{{else}}
-						<li>{{ctx.Locale.Tr "org.teams.specific_repositories" | Str2html}}</li>
+						<li>{{ctx.Locale.Tr "org.teams.specific_repositories"}}</li>
 					{{end}}
 					{{if .Team.CanCreateOrgRepo}}
 						<li>{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</li>
@@ -44,10 +44,10 @@
 				</ul>
 				{{if (eq .Team.AccessMode 2)}}
 					<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
-					{{ctx.Locale.Tr "org.teams.write_permission_desc" | Str2html}}
+					{{ctx.Locale.Tr "org.teams.write_permission_desc"}}
 				{{else if (eq .Team.AccessMode 3)}}
 					<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
-					{{ctx.Locale.Tr "org.teams.admin_permission_desc" | Str2html}}
+					{{ctx.Locale.Tr "org.teams.admin_permission_desc"}}
 				{{else}}
 					<table class="ui table">
 						<thead>
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 31cd5b23f6..4df7b18c44 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -2,11 +2,11 @@
 	{{$revsFileLink := URLJoin .RepoLink "src" .BranchNameSubURL "/.git-blame-ignore-revs"}}
 	{{if .UsesIgnoreRevs}}
 		<div class="ui info message">
-			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true") | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true")}}</p>
 		</div>
 	{{else}}
 		<div class="ui error message">
-			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink}}</p>
 		</div>
 	{{end}}
 {{end}}
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 8ae7301c4a..46503cb5df 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -210,7 +210,7 @@
 		{{ctx.Locale.Tr "repo.branch.delete_html"}} <span class="name"></span>
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "repo.branch.delete_desc" | Str2html}}</p>
+		<p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl
index d6ff22b7ab..73c9ca6a1f 100644
--- a/templates/repo/create.tmpl
+++ b/templates/repo/create.tmpl
@@ -158,7 +158,7 @@
 									{{end}}
 								</div>
 							</div>
-							<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/" | Str2html}}</span>
+							<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span>
 						</div>
 
 						<div class="inline field">
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 5960decc06..abeeacead0 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -19,7 +19,7 @@
 			{{end}}
 			{{if not .DiffNotAvailable}}
 				<div class="diff-detail-stats gt-df gt-ac gt-fw">
-					{{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
+					{{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}}
 				</div>
 			{{end}}
 		</div>
diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl
index db468ab6c8..b7acb3d49b 100644
--- a/templates/repo/diff/stats.tmpl
+++ b/templates/repo/diff/stats.tmpl
@@ -1,5 +1,5 @@
 {{Eval .file.Addition "+" .file.Deletion}}
-<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}">
+<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion}}">
 	{{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
 	<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .file.Addition "/" "(" .file.Addition "+" .file.Deletion "+" 0.0 ")"}}%"></div>
 </span>
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index 62194abe50..f171cd8d5c 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -24,7 +24,7 @@
 					</h4>
 					<div class="ui attached guide table segment empty-repo-guide">
 						<div class="item">
-							<h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository" | Str2html}}</small></h3>
+							<h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository"}}</small></h3>
 
 							<div class="repo-button-row">
 								{{if and .CanWriteCode (not .Repository.IsArchived)}}
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index 9a6065a407..9b0061b60e 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -61,7 +61,7 @@
 			<li class="item">
 				<div class="ui grid middle aligned">
 					<div class="ten wide column">
-						{{ctx.Locale.Tr "repo.org_labels_desc" | Str2html}}
+						{{ctx.Locale.Tr "repo.org_labels_desc"}}
 						{{if .IsOrganizationOwner}}
 							<a href="{{.OrganizationLink}}/settings/labels">({{ctx.Locale.Tr "repo.org_labels_desc_manage"}})</a>:
 						{{end}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 793772ecd0..906f880140 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -181,7 +181,7 @@
 		{{ctx.Locale.Tr "repo.branch.delete" .HeadTarget}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "repo.branch.delete_desc" | Str2html}}</p>
+		<p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/repo/migrate/options.tmpl b/templates/repo/migrate/options.tmpl
index 1bc30b886d..1cf8600749 100644
--- a/templates/repo/migrate/options.tmpl
+++ b/templates/repo/migrate/options.tmpl
@@ -17,7 +17,7 @@
 	<span id="lfs_settings" class="gt-hidden">(<a id="lfs_settings_show" href="#">{{ctx.Locale.Tr "repo.settings.advanced_settings"}}</a>)</span>
 </div>
 <div id="lfs_endpoint" class="gt-hidden">
-	<span class="help">{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | Str2html}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}</span>
+	<span class="help">{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}</span>
 	<div class="inline field {{if .Err_LFSEndpoint}}error{{end}}">
 		<label>{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.label"}}</label>
 		<input name="lfs_endpoint" value="{{.lfs_endpoint}}" placeholder="{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.placeholder"}}">
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 6dbeb741db..5b747c2bf9 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -54,7 +54,7 @@
 								<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>
+								| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{$release.TagName | PathEscapeSegments}}...{{$release.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}}</span>
 							{{end}}
 						</p>
 						<div class="markup desc">
diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl
index b616b4de32..495620300f 100644
--- a/templates/repo/search.tmpl
+++ b/templates/repo/search.tmpl
@@ -24,7 +24,7 @@
 			</div>
 		{{else if .Keyword}}
 			<h3>
-				{{ctx.Locale.Tr "repo.search.results" (.Keyword|Escape) (.RepoLink|Escape) (.RepoName|Escape) | Str2html}}
+				{{ctx.Locale.Tr "repo.search.results" (.Keyword|Escape) (.RepoLink|Escape) (.RepoName|Escape)}}
 			</h3>
 			{{if .SearchResults}}
 				<div class="flex-text-block gt-fw">
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index 3ea854ef88..a79a196825 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -31,7 +31,7 @@
 							<label for="is_writable">
 								{{ctx.Locale.Tr "repo.settings.is_writable"}}
 							</label>
-							<small style="padding-left: 26px;">{{ctx.Locale.Tr "repo.settings.is_writable_info" | Str2html}}</small>
+							<small style="padding-left: 26px;">{{ctx.Locale.Tr "repo.settings.is_writable_info"}}</small>
 						</div>
 					</div>
 					<button class="ui primary button">
diff --git a/templates/repo/settings/githooks.tmpl b/templates/repo/settings/githooks.tmpl
index 389d381f30..bdbfb40ca1 100644
--- a/templates/repo/settings/githooks.tmpl
+++ b/templates/repo/settings/githooks.tmpl
@@ -6,7 +6,7 @@
 		<div class="ui attached segment">
 			<div class="ui list">
 				<div class="item">
-					{{ctx.Locale.Tr "repo.settings.githooks_desc" | Str2html}}
+					{{ctx.Locale.Tr "repo.settings.githooks_desc"}}
 				</div>
 				{{range .Hooks}}
 					<div class="item truncated-item-container">
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 7122778f06..6d01b227ff 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -191,7 +191,7 @@
 										<div class="field {{if .Err_LFSEndpoint}}error{{end}}">
 											<label for="mirror_lfs_endpoint">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint"}}</label>
 											<input id="mirror_lfs_endpoint" name="mirror_lfs_endpoint" value="{{.PullMirror.LFSEndpoint}}" placeholder="{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.placeholder"}}">
-											<p class="help">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | Str2html}}</p>
+											<p class="help">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}</p>
 										</div>
 										{{end}}
 										<div class="field">
@@ -409,7 +409,7 @@
 						<div class="field">
 							<label for="tracker_url_format">{{ctx.Locale.Tr "repo.settings.tracker_url_format"}}</label>
 							<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}">
-							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc" | Str2html}}</p>
+							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
 						</div>
 						<div class="inline fields">
 							<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
@@ -437,7 +437,7 @@
 						<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box">
 							<label for="external_tracker_regexp_pattern">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
 							<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
-							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}</p>
+							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}</p>
 						</div>
 					</div>
 				</div>
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index e2f841f758..e57509abc0 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -2,7 +2,7 @@
 	<div class="repo-setting-content">
 		<form class="ui form" action="{{.Link}}" method="post">
 			<h4 class="ui top attached header">
-				{{ctx.Locale.Tr "repo.settings.branch_protection" (.Rule.RuleName|Escape) | Str2html}}
+				{{ctx.Locale.Tr "repo.settings.branch_protection" (.Rule.RuleName|Escape)}}
 			</h4>
 			<div class="ui attached segment branch-protection">
 				<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_patterns"}}</h5>
diff --git a/templates/repo/settings/webhook/dingtalk.tmpl b/templates/repo/settings/webhook/dingtalk.tmpl
index 32ca0d0807..0ba99e98ee 100644
--- a/templates/repo/settings/webhook/dingtalk.tmpl
+++ b/templates/repo/settings/webhook/dingtalk.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "dingtalk"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/dingtalk/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl
index 25dc219ee1..104346e042 100644
--- a/templates/repo/settings/webhook/discord.tmpl
+++ b/templates/repo/settings/webhook/discord.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "discord"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/discord/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/feishu.tmpl b/templates/repo/settings/webhook/feishu.tmpl
index 9683427fbf..d80deab26f 100644
--- a/templates/repo/settings/webhook/feishu.tmpl
+++ b/templates/repo/settings/webhook/feishu.tmpl
@@ -1,6 +1,6 @@
 {{if eq .HookType "feishu"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu") | Str2html}}</p>
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu")}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/feishu/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl
index 4fda6a7b39..e6eb61ea92 100644
--- a/templates/repo/settings/webhook/gitea.tmpl
+++ b/templates/repo/settings/webhook/gitea.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "gitea"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/gitea/{{or .Webhook.ID "new"}}" method="post">
 		{{template "base/disable_form_autofill"}}
 		{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl
index d2bd98c32c..e91a3279e4 100644
--- a/templates/repo/settings/webhook/gogs.tmpl
+++ b/templates/repo/settings/webhook/gogs.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "gogs"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/gogs/{{or .Webhook.ID "new"}}" method="post">
 		{{template "base/disable_form_autofill"}}
 		{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/matrix.tmpl b/templates/repo/settings/webhook/matrix.tmpl
index a2a9921d7b..7f1c9f08e6 100644
--- a/templates/repo/settings/webhook/matrix.tmpl
+++ b/templates/repo/settings/webhook/matrix.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "matrix"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/matrix/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_HomeserverURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/msteams.tmpl b/templates/repo/settings/webhook/msteams.tmpl
index 0097209db1..62ea24e763 100644
--- a/templates/repo/settings/webhook/msteams.tmpl
+++ b/templates/repo/settings/webhook/msteams.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "msteams"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/msteams/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/packagist.tmpl b/templates/repo/settings/webhook/packagist.tmpl
index fc373951d1..25aba2a435 100644
--- a/templates/repo/settings/webhook/packagist.tmpl
+++ b/templates/repo/settings/webhook/packagist.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "packagist"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/packagist/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_Username}}error{{end}}">
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index 8e2387067e..f636108b37 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -5,19 +5,19 @@
 		<div class="field">
 			<div class="ui radio non-events checkbox">
 				<input name="events" type="radio" value="push_only" {{if or $isNew .Webhook.PushOnly}}checked{{end}}>
-				<label>{{ctx.Locale.Tr "repo.settings.event_push_only" | Str2html}}</label>
+				<label>{{ctx.Locale.Tr "repo.settings.event_push_only"}}</label>
 			</div>
 		</div>
 		<div class="field">
 			<div class="ui radio non-events checkbox">
 				<input name="events" type="radio" value="send_everything" {{if .Webhook.SendEverything}}checked{{end}}>
-				<label>{{ctx.Locale.Tr "repo.settings.event_send_everything" | Str2html}}</label>
+				<label>{{ctx.Locale.Tr "repo.settings.event_send_everything"}}</label>
 			</div>
 		</div>
 		<div class="field">
 			<div class="ui radio events checkbox">
 				<input name="events" type="radio" value="choose_events" {{if .Webhook.ChooseEvents}}checked{{end}}>
-				<label>{{ctx.Locale.Tr "repo.settings.event_choose" | Str2html}}</label>
+				<label>{{ctx.Locale.Tr "repo.settings.event_choose"}}</label>
 			</div>
 		</div>
 	</div>
@@ -255,7 +255,7 @@
 <div class="field">
 	<label for="branch_filter">{{ctx.Locale.Tr "repo.settings.branch_filter"}}</label>
 	<input id="branch_filter" name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
-	<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" | Str2html}}</span>
+	<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc"}}</span>
 </div>
 
 <!-- Authorization Header -->
diff --git a/templates/repo/settings/webhook/slack.tmpl b/templates/repo/settings/webhook/slack.tmpl
index b367aed5ec..e7cae92d4b 100644
--- a/templates/repo/settings/webhook/slack.tmpl
+++ b/templates/repo/settings/webhook/slack.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "slack"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/slack/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/telegram.tmpl b/templates/repo/settings/webhook/telegram.tmpl
index 92bbbef3fd..f92c2be0db 100644
--- a/templates/repo/settings/webhook/telegram.tmpl
+++ b/templates/repo/settings/webhook/telegram.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "telegram"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/telegram/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_BotToken}}error{{end}}">
diff --git a/templates/repo/settings/webhook/wechatwork.tmpl b/templates/repo/settings/webhook/wechatwork.tmpl
index 65f12998b1..78a1617123 100644
--- a/templates/repo/settings/webhook/wechatwork.tmpl
+++ b/templates/repo/settings/webhook/wechatwork.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "wechatwork"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/wechatwork/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl
index d0730f23c1..c9f8cd38c1 100644
--- a/templates/repo/unicode_escape_prompt.tmpl
+++ b/templates/repo/unicode_escape_prompt.tmpl
@@ -5,9 +5,9 @@
 			<div class="header">
 				{{ctx.Locale.Tr "repo.invisible_runes_header"}}
 			</div>
-			<p>{{ctx.Locale.Tr "repo.invisible_runes_description" | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p>
 			{{if .EscapeStatus.HasAmbiguous}}
-				<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description" | Str2html}}</p>
+				<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
 			{{end}}
 		</div>
 	{{else if .EscapeStatus.HasAmbiguous}}
@@ -16,7 +16,7 @@
 			<div class="header">
 				{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
 			</div>
-			<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description" | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
 		</div>
 	{{end}}
 {{end}}
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index edcb90f9a4..d6cff28174 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -42,7 +42,7 @@
 				{{end}}
 				<div class="center gt-mt-5">
 					{{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
-					{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message" | Str2html}}</p>{{end}}
+					{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message"}}</p>{{end}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl
index 1b06719753..589899f9d3 100644
--- a/templates/user/auth/activate.tmpl
+++ b/templates/user/auth/activate.tmpl
@@ -15,7 +15,7 @@
 						{{else if .ResendLimited}}
 							<p class="center">{{ctx.Locale.Tr "auth.resent_limit_prompt"}}</p>
 						{{else}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.SignedUser.Email|Escape) .ActiveCodeLives | Str2html}}</p>
+							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.SignedUser.Email|Escape) .ActiveCodeLives}}</p>
 						{{end}}
 					{{else}}
 						{{if .NeedsPassword}}
@@ -29,7 +29,7 @@
 							</div>
 							<input id="code" name="code" type="hidden" value="{{.Code}}">
 						{{else if .IsSendRegisterMail}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.Email|Escape) .ActiveCodeLives | Str2html}}</p>
+							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.Email|Escape) .ActiveCodeLives}}</p>
 						{{else if .IsCodeInvalid}}
 							<p>{{ctx.Locale.Tr "auth.invalid_code"}}</p>
 						{{else if .IsPasswordInvalid}}
@@ -37,7 +37,7 @@
 						{{else if .ManualActivationOnly}}
 							<p class="center">{{ctx.Locale.Tr "auth.manual_activation_only"}}</p>
 						{{else}}
-							<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" (.SignedUser.Name|Escape) (.SignedUser.Email|Escape) | Str2html}}</p>
+							<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" (.SignedUser.Name|Escape) (.SignedUser.Email|Escape)}}</p>
 							<div class="divider"></div>
 							<div class="text right">
 								<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
diff --git a/templates/user/auth/finalize_openid.tmpl b/templates/user/auth/finalize_openid.tmpl
index 7449e3beda..1c1dcdb825 100644
--- a/templates/user/auth/finalize_openid.tmpl
+++ b/templates/user/auth/finalize_openid.tmpl
@@ -35,7 +35,7 @@
 					{{if .ShowRegistrationButton}}
 						<div class="inline field">
 							<label></label>
-							<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a>
+							<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
 						</div>
 					{{end}}
 					</form>
diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl
index dde4c8f6fe..03621ea87f 100644
--- a/templates/user/auth/forgot_passwd.tmpl
+++ b/templates/user/auth/forgot_passwd.tmpl
@@ -10,7 +10,7 @@
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
 					{{if .IsResetSent}}
-						<p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" (Escape .Email) .ResetPwdCodeLives | Str2html}}</p>
+						<p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" (Escape .Email) .ResetPwdCodeLives}}</p>
 					{{else if .IsResetRequest}}
 						<div class="required inline field {{if .Err_Email}}error{{end}}">
 							<label for="email">{{ctx.Locale.Tr "email"}}</label>
diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl
index 9c0bf33e28..cb9bba8749 100644
--- a/templates/user/auth/grant.tmpl
+++ b/templates/user/auth/grant.tmpl
@@ -9,11 +9,11 @@
 				{{template "base/alert" .}}
 				<p>
 					<b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br>
-					{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML | Str2html}}
+					{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}
 				</p>
 			</div>
 			<div class="ui attached segment">
-				<p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML | Str2html}}</p>
+				<p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML}}</p>
 			</div>
 			<div class="ui attached segment">
 				<form method="post" action="{{AppSubUrl}}/login/oauth/grant">
diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl
index 2f470df441..9fee30f554 100644
--- a/templates/user/auth/reset_passwd.tmpl
+++ b/templates/user/auth/reset_passwd.tmpl
@@ -34,7 +34,7 @@
 						<h4 class="ui dividing header">
 							{{ctx.Locale.Tr "twofa"}}
 						</h4>
-						<div class="ui warning visible message">{{ctx.Locale.Tr "settings.twofa_is_enrolled" | Str2html}}</div>
+						<div class="ui warning visible message">{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}</div>
 						{{if .scratch_code}}
 						<div class="required inline field {{if .Err_Token}}error{{end}}">
 							<label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label>
@@ -53,11 +53,11 @@
 							<label></label>
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.reset_password_helper"}}</button>
 							{{if and .has_two_factor (not .scratch_code)}}
-								<a href="{{.Link}}?code={{.Code}}&amp;scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code" | Str2html}}</a>
+								<a href="{{.Link}}?code={{.Code}}&amp;scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
 							{{end}}
 						</div>
 					{{else}}
-						<p class="center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl) | Str2html}}</p>
+						<p class="center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl)}}</p>
 					{{end}}
 				</div>
 			</form>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 40e54ec8fa..0d0064b02a 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -48,7 +48,7 @@
 	{{if .ShowRegistrationButton}}
 		<div class="inline field">
 			<label></label>
-			<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a>
+			<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
 		</div>
 	{{end}}
 
diff --git a/templates/user/auth/twofa.tmpl b/templates/user/auth/twofa.tmpl
index d325114155..5260178d13 100644
--- a/templates/user/auth/twofa.tmpl
+++ b/templates/user/auth/twofa.tmpl
@@ -17,7 +17,7 @@
 					<div class="inline field">
 						<label></label>
 						<button class="ui primary button">{{ctx.Locale.Tr "auth.verify"}}</button>
-						<a href="{{AppSubUrl}}/user/two_factor/scratch">{{ctx.Locale.Tr "auth.use_scratch_code" | Str2html}}</a>
+						<a href="{{AppSubUrl}}/user/two_factor/scratch">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
 					</div>
 				</div>
 			</form>
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index a51365e4d6..bb619a5f18 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -12,71 +12,71 @@
 						{{.ShortActUserName ctx}}
 					{{end}}
 					{{if .GetOpType.InActions "create_repo"}}
-						{{ctx.Locale.Tr "action.create_repo" ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.create_repo" ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "rename_repo"}}
-						{{ctx.Locale.Tr "action.rename_repo" (.GetContent|Escape) ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.rename_repo" (.GetContent|Escape) ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "commit_repo"}}
 						{{if .Content}}
-							{{ctx.Locale.Tr "action.commit_repo" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+							{{ctx.Locale.Tr "action.commit_repo" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape)}}
 						{{else}}
-							{{ctx.Locale.Tr "action.create_branch" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+							{{ctx.Locale.Tr "action.create_branch" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape)}}
 						{{end}}
 					{{else if .GetOpType.InActions "create_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.create_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.create_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "create_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.create_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.create_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "transfer_repo"}}
-						{{ctx.Locale.Tr "action.transfer_repo" .GetContent ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.transfer_repo" .GetContent ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "push_tag"}}
-						{{ctx.Locale.Tr "action.push_tag" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.push_tag" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "comment_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.comment_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.comment_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "merge_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.merge_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.merge_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "close_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.close_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.close_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "reopen_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reopen_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.reopen_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "close_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.close_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.close_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "reopen_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reopen_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.reopen_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "delete_tag"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.delete_tag" ((.GetRepoLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.delete_tag" ((.GetRepoLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "delete_branch"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.delete_branch" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.delete_branch" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "mirror_sync_push"}}
-						{{ctx.Locale.Tr "action.mirror_sync_push" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.mirror_sync_push" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "mirror_sync_create"}}
-						{{ctx.Locale.Tr "action.mirror_sync_create" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.mirror_sync_create" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "mirror_sync_delete"}}
-						{{ctx.Locale.Tr "action.mirror_sync_delete" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.mirror_sync_delete" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "approve_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.approve_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.approve_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "reject_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reject_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.reject_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "comment_pull"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.comment_pull" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.comment_pull" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
 					{{else if .GetOpType.InActions "publish_release"}}
 						{{$linkText := .Content | RenderEmoji $.Context}}
-						{{ctx.Locale.Tr "action.publish_release" ((.GetRepoLink ctx)|Escape) ((printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag)|Escape) ((.ShortRepoPath ctx)|Escape) $linkText | Str2html}}
+						{{ctx.Locale.Tr "action.publish_release" ((.GetRepoLink ctx)|Escape) ((printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag)|Escape) ((.ShortRepoPath ctx)|Escape) $linkText}}
 					{{else if .GetOpType.InActions "review_dismissed"}}
 						{{$index := index .GetIssueInfos 0}}
 						{{$reviewer := index .GetIssueInfos 1}}
-						{{ctx.Locale.Tr "action.review_dismissed" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) $reviewer | Str2html}}
+						{{ctx.Locale.Tr "action.review_dismissed" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) $reviewer}}
 					{{end}}
 					{{TimeSince .GetCreate ctx.Locale}}
 				</div>
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index 7c6fd49a08..bfcf423d67 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -133,9 +133,9 @@
 		</h4>
 		<div class="ui attached error segment">
 			<div class="ui red message">
-				<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "settings.delete_prompt" | Str2html}}</p>
+				<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "settings.delete_prompt"}}</p>
 				{{if .UserDeleteWithComments}}
-				<p class="text left gt-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime | Str2html}}</p>
+				<p class="text left gt-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime}}</p>
 				{{end}}
 			</div>
 			<form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post">
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index e7eb6c8180..8cf76d80a5 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -75,7 +75,7 @@
 						{{ctx.Locale.Tr "settings.select_permissions"}}
 					</summary>
 					<p class="activity meta">
-						<i>{{ctx.Locale.Tr "settings.access_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i>
+						<i>{{ctx.Locale.Tr "settings.access_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`)}}</i>
 					</p>
 					<div class="scoped-access-token-mount">
 						<scoped-access-token-selector
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index 43ea667516..0dd0059511 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -43,7 +43,7 @@
 		<div class="flex-item">
 			<p>
 				{{ctx.Locale.Tr "settings.gpg_desc"}}<br>
-				{{ctx.Locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification" | Str2html}}
+				{{ctx.Locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification"}}
 			</p>
 		</div>
 		{{range .GPGKeys}}
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index 2d3225e61e..94ee2a1a55 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -31,7 +31,7 @@
 		<div class="flex-item">
 			<p>
 				{{ctx.Locale.Tr "settings.ssh_desc"}}<br>
-				{{ctx.Locale.Tr "settings.ssh_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/troubleshooting-ssh" | Str2html}}
+				{{ctx.Locale.Tr "settings.ssh_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/troubleshooting-ssh"}}
 			</p>
 		</div>
 		{{if .DisableSSH}}
diff --git a/templates/user/settings/security/twofa.tmpl b/templates/user/settings/security/twofa.tmpl
index 2f15fe13f1..adebce4265 100644
--- a/templates/user/settings/security/twofa.tmpl
+++ b/templates/user/settings/security/twofa.tmpl
@@ -4,7 +4,7 @@
 <div class="ui attached segment">
 	<p>{{ctx.Locale.Tr "settings.twofa_desc"}}</p>
 	{{if .TOTPEnrolled}}
-	<p>{{ctx.Locale.Tr "settings.twofa_is_enrolled" | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}</p>
 	<form class="ui form" action="{{AppSubUrl}}/user/settings/security/two_factor/regenerate_scratch" method="post" enctype="multipart/form-data">
 		{{.CsrfTokenHtml}}
 		<p>{{ctx.Locale.Tr "settings.regenerate_scratch_token_desc"}}</p>
diff --git a/templates/user/settings/security/webauthn.tmpl b/templates/user/settings/security/webauthn.tmpl
index e582b801da..eceee191bd 100644
--- a/templates/user/settings/security/webauthn.tmpl
+++ b/templates/user/settings/security/webauthn.tmpl
@@ -1,6 +1,6 @@
 <h4 class="ui top attached header">{{ctx.Locale.Tr "settings.webauthn"}}</h4>
 <div class="ui attached segment">
-	<p>{{ctx.Locale.Tr "settings.webauthn_desc" | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "settings.webauthn_desc"}}</p>
 	<p>{{ctx.Locale.Tr "settings.webauthn_key_loss_warning"}} {{ctx.Locale.Tr "settings.webauthn_alternative_tip"}}</p>
 	{{template "user/auth/webauthn_error" .}}
 	<div class="flex-list">

From 681c3ec7eab548d8888afcf01d57a1825b55aba4 Mon Sep 17 00:00:00 2001
From: Kyle D <kdumontnu@gmail.com>
Date: Thu, 22 Feb 2024 13:53:03 -0500
Subject: [PATCH 127/679] Remove bountysource (#29330)

[Bountysource is dead](https://github.com/bountysource/core/issues/1586). So remove them from our repo.
---
 .github/FUNDING.yml | 1 -
 README.md           | 3 ---
 README_ZH.md        | 3 ---
 3 files changed, 7 deletions(-)

diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 624a2d97db..1447a6ea32 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1 @@
 open_collective: gitea
-custom: https://www.bountysource.com/teams/gitea
diff --git a/README.md b/README.md
index adba74d8bb..94d7284c7c 100644
--- a/README.md
+++ b/README.md
@@ -45,9 +45,6 @@
   <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs">
     <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main">
   </a>
-  <a href="https://app.bountysource.com/teams/gitea" title="Bountysource">
-    <img src="https://img.shields.io/bountysource/team/gitea/activity">
-  </a>
 </p>
 
 <p align="center">
diff --git a/README_ZH.md b/README_ZH.md
index 0d9092a0fd..adfeb9a8df 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -45,9 +45,6 @@
   <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs">
     <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main">
   </a>
-  <a href="https://app.bountysource.com/teams/gitea" title="Bountysource">
-    <img src="https://img.shields.io/bountysource/team/gitea/activity">
-  </a>
 </p>
 
 <p align="center">

From 532da5ed5ee3edb45d2ee63c6ab0fad53473691f Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 22 Feb 2024 22:21:43 +0100
Subject: [PATCH 128/679] Don't show third-party JS errors in production builds
 (#29303)

So we don't get issues like
https://github.com/go-gitea/gitea/issues/29080 and
https://github.com/go-gitea/gitea/issues/29273 any more. Only active in
[production
builds](https://webpack.js.org/guides/production/#specify-the-mode), in
non-production the errors will still show.
---
 web_src/js/bootstrap.js | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
index f8d0c0cac0..e46c91e5e6 100644
--- a/web_src/js/bootstrap.js
+++ b/web_src/js/bootstrap.js
@@ -29,17 +29,26 @@ export function showGlobalErrorMessage(msg) {
  * @param {ErrorEvent} e
  */
 function processWindowErrorEvent(e) {
+  const err = e.error ?? e.reason;
+  const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin));
+
+  // error is likely from browser extension or inline script. Do not show these in production builds.
+  if (!err.stack?.includes(assetBaseUrl) && window.config?.runModeIsProd) return;
+
+  let message;
   if (e.type === 'unhandledrejection') {
-    showGlobalErrorMessage(`JavaScript promise rejection: ${e.reason}. Open browser console to see more details.`);
-    return;
+    message = `JavaScript promise rejection: ${err.message}.`;
+  } else {
+    message = `JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}).`;
   }
+
   if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
     // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
     // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0.
     return; // ignore such nonsense error event
   }
 
-  showGlobalErrorMessage(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
+  showGlobalErrorMessage(`${message} Open browser console to see more details.`);
 }
 
 function initGlobalErrorHandler() {

From c4b0cb4d0d527793296cf801e611f77666f86551 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 23 Feb 2024 00:31:24 +0100
Subject: [PATCH 129/679] Upgrade to fabric 6 (#29334)

Upgrade fabric to latest v6 beta. It works for our use case, even
thought it does not fix the upstream issue
https://github.com/fabricjs/fabric.js/issues/9679 that
https://github.com/go-gitea/gitea/issues/29326 relates to.
---
 Makefile                      |  2 +-
 build/generate-images.js      | 29 +++++++++++------------------
 public/assets/img/favicon.svg |  2 +-
 public/assets/img/logo.svg    |  2 +-
 4 files changed, 14 insertions(+), 21 deletions(-)

diff --git a/Makefile b/Makefile
index 7fa8193800..4ef02c6c54 100644
--- a/Makefile
+++ b/Makefile
@@ -969,7 +969,7 @@ generate-gitignore:
 
 .PHONY: generate-images
 generate-images: | node_modules
-	npm install --no-save --no-package-lock fabric@5 imagemin-zopfli@7
+	npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7
 	node build/generate-images.js $(TAGS)
 
 .PHONY: generate-manpage
diff --git a/build/generate-images.js b/build/generate-images.js
index 09e3e068af..db31d19e2a 100755
--- a/build/generate-images.js
+++ b/build/generate-images.js
@@ -1,20 +1,13 @@
 #!/usr/bin/env node
 import imageminZopfli from 'imagemin-zopfli';
 import {optimize} from 'svgo';
-import {fabric} from 'fabric';
+import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node';
 import {readFile, writeFile} from 'node:fs/promises';
+import {argv, exit} from 'node:process';
 
-function exit(err) {
+function doExit(err) {
   if (err) console.error(err);
-  process.exit(err ? 1 : 0);
-}
-
-function loadSvg(svg) {
-  return new Promise((resolve) => {
-    fabric.loadSVGFromString(svg, (objects, options) => {
-      resolve({objects, options});
-    });
-  });
+  exit(err ? 1 : 0);
 }
 
 async function generate(svg, path, {size, bg}) {
@@ -35,14 +28,14 @@ async function generate(svg, path, {size, bg}) {
     return;
   }
 
-  const {objects, options} = await loadSvg(svg);
-  const canvas = new fabric.Canvas();
+  const {objects, options} = await loadSVGFromString(svg);
+  const canvas = new Canvas();
   canvas.setDimensions({width: size, height: size});
   const ctx = canvas.getContext('2d');
   ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1);
 
   if (bg) {
-    canvas.add(new fabric.Rect({
+    canvas.add(new Rect({
       left: 0,
       top: 0,
       height: size * (1 / (size / options.height)),
@@ -51,7 +44,7 @@ async function generate(svg, path, {size, bg}) {
     }));
   }
 
-  canvas.add(fabric.util.groupSVGElements(objects, options));
+  canvas.add(util.groupSVGElements(objects, options));
   canvas.renderAll();
 
   let png = Buffer.from([]);
@@ -64,7 +57,7 @@ async function generate(svg, path, {size, bg}) {
 }
 
 async function main() {
-  const gitea = process.argv.slice(2).includes('gitea');
+  const gitea = argv.slice(2).includes('gitea');
   const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8');
   const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8');
 
@@ -80,7 +73,7 @@ async function main() {
 }
 
 try {
-  exit(await main());
+  doExit(await main());
 } catch (err) {
-  exit(err);
+  doExit(err);
 }
diff --git a/public/assets/img/favicon.svg b/public/assets/img/favicon.svg
index afeeacb77c..43291345df 100644
--- a/public/assets/img/favicon.svg
+++ b/public/assets/img/favicon.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32"><path style="fill:#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"/><path style="fill:#609926" d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path style="fill:#609926" d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/logo.svg b/public/assets/img/logo.svg
index afeeacb77c..43291345df 100644
--- a/public/assets/img/logo.svg
+++ b/public/assets/img/logo.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32"><path style="fill:#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"/><path style="fill:#609926" d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path style="fill:#609926" d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
\ No newline at end of file

From 5bb8d1924d77c675467694de26697b876d709a17 Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Thu, 22 Feb 2024 19:08:17 -0500
Subject: [PATCH 130/679] Support SAML authentication (#25165)

Closes https://github.com/go-gitea/gitea/issues/5512

This PR adds basic SAML support
- Adds SAML 2.0 as an auth source
- Adds SAML configuration documentation
- Adds integration test:
- Use bare-bones SAML IdP to test protocol flow and test account is
linked successfully (only runs on Postgres by default)
- Adds documentation for configuring and running SAML integration test
locally

Future PRs:
- Support group mapping
- Support auto-registration (account linking)

Co-Authored-By: @jackHay22

---------

Co-authored-by: jackHay22 <jack@allspice.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: morphelinho <morphelinho@users.noreply.github.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: silverwind <me@silverwind.io>
---
 .github/workflows/pull-db-tests.yml           |   8 +
 assets/go-licenses.json                       |  25 +++
 docs/content/usage/authentication.en-us.md    |  69 ++++++
 go.mod                                        |   5 +
 go.sum                                        |  12 ++
 models/auth/oauth2.go                         |  20 +-
 models/auth/source.go                         |  38 ++++
 options/locale/locale_en-US.ini               |  14 ++
 routers/init.go                               |   2 +
 routers/web/admin/auths.go                    |  84 ++++++++
 routers/web/auth/auth.go                      |  35 ++-
 routers/web/auth/linkaccount.go               |  45 ++--
 routers/web/auth/oauth.go                     |  19 +-
 routers/web/auth/openid.go                    |   5 +-
 routers/web/auth/saml.go                      | 172 +++++++++++++++
 routers/web/web.go                            |   5 +
 .../auth/source/saml/assert_interface_test.go |  22 ++
 services/auth/source/saml/init.go             |  29 +++
 services/auth/source/saml/name_id_format.go   |  38 ++++
 services/auth/source/saml/providers.go        | 109 ++++++++++
 services/auth/source/saml/source.go           | 202 ++++++++++++++++++
 .../auth/source/saml/source_authenticate.go   |  16 ++
 services/auth/source/saml/source_callout.go   |  89 ++++++++
 services/auth/source/saml/source_metadata.go  |  32 +++
 services/auth/source/saml/source_register.go  |  23 ++
 services/externalaccount/link.go              |  11 +-
 services/externalaccount/user.go              |  12 +-
 services/forms/auth_form.go                   |  15 +-
 templates/admin/auth/edit.tmpl                |  66 ++++++
 templates/admin/auth/new.tmpl                 |   6 +
 templates/admin/auth/source/saml.tmpl         |  62 ++++++
 templates/user/auth/signin_inner.tmpl         |  17 ++
 tests/integration/README.md                   |  17 ++
 tests/integration/saml_test.go                | 150 +++++++++++++
 web_src/js/features/admin/common.js           |   8 +-
 web_src/js/features/user-auth.js              |  21 ++
 web_src/js/index.js                           |   6 +-
 37 files changed, 1440 insertions(+), 69 deletions(-)
 create mode 100644 routers/web/auth/saml.go
 create mode 100644 services/auth/source/saml/assert_interface_test.go
 create mode 100644 services/auth/source/saml/init.go
 create mode 100644 services/auth/source/saml/name_id_format.go
 create mode 100644 services/auth/source/saml/providers.go
 create mode 100644 services/auth/source/saml/source.go
 create mode 100644 services/auth/source/saml/source_authenticate.go
 create mode 100644 services/auth/source/saml/source_callout.go
 create mode 100644 services/auth/source/saml/source_metadata.go
 create mode 100644 services/auth/source/saml/source_register.go
 create mode 100644 templates/admin/auth/source/saml.tmpl
 create mode 100644 tests/integration/saml_test.go

diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index a3886bf618..8843c6d65e 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -37,6 +37,14 @@ jobs:
           MINIO_ROOT_PASSWORD: 12345678
         ports:
           - "9000:9000"
+      simplesaml:
+        image: allspice/simple-saml
+        ports:
+          - "8080:8080"
+        env:
+          SIMPLESAMLPHP_SP_ENTITY_ID: http://localhost:3002/user/saml/test-sp/metadata
+          SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: http://localhost:3002/user/saml/test-sp/acs
+          SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: http://localhost:3002/user/saml/test-sp/acs
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-go@v5
diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 2aa60780c4..ed722b0192 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -124,6 +124,11 @@
     "path": "github.com/aymerick/douceur/LICENSE",
     "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Aymerick JEHANNE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
   },
+  {
+    "name": "github.com/beevik/etree",
+    "path": "github.com/beevik/etree/LICENSE",
+    "licenseText": "Copyright 2015-2019 Brett Vickers. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n   1. Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n\n   2. Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\nOF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
   {
     "name": "github.com/beorn7/perks/quantile",
     "path": "github.com/beorn7/perks/quantile/LICENSE",
@@ -639,6 +644,11 @@
     "path": "github.com/jhillyerd/enmime/LICENSE",
     "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2012-2016 James Hillyerd, All Rights Reserved\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
   },
+  {
+    "name": "github.com/jonboulle/clockwork",
+    "path": "github.com/jonboulle/clockwork/LICENSE",
+    "licenseText": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
+  },
   {
     "name": "github.com/josharian/intern",
     "path": "github.com/josharian/intern/license.md",
@@ -719,6 +729,11 @@
     "path": "github.com/markbates/goth/LICENSE.txt",
     "licenseText": "Copyright (c) 2014 Mark Bates\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
   },
+  {
+    "name": "github.com/mattermost/xml-roundtrip-validator",
+    "path": "github.com/mattermost/xml-roundtrip-validator/LICENSE.txt",
+    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
+  },
   {
     "name": "github.com/mattn/go-colorable",
     "path": "github.com/mattn/go-colorable/LICENSE",
@@ -904,6 +919,16 @@
     "path": "github.com/rs/xid/LICENSE",
     "licenseText": "Copyright (c) 2015 Olivier Poitrey \u003crs@dailymotion.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
   },
+  {
+    "name": "github.com/russellhaering/gosaml2",
+    "path": "github.com/russellhaering/gosaml2/LICENSE",
+    "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n"
+  },
+  {
+    "name": "github.com/russellhaering/goxmldsig",
+    "path": "github.com/russellhaering/goxmldsig/LICENSE",
+    "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n"
+  },
   {
     "name": "github.com/russross/blackfriday/v2",
     "path": "github.com/russross/blackfriday/v2/LICENSE.txt",
diff --git a/docs/content/usage/authentication.en-us.md b/docs/content/usage/authentication.en-us.md
index adc936dfbe..1838cfcc77 100644
--- a/docs/content/usage/authentication.en-us.md
+++ b/docs/content/usage/authentication.en-us.md
@@ -349,3 +349,72 @@ If set `ENABLE_REVERSE_PROXY_FULL_NAME=true`, a user full name expected in `X-WE
 You can also limit the reverse proxy's IP address range with `REVERSE_PROXY_TRUSTED_PROXIES` which default value is `127.0.0.0/8,::1/128`. By `REVERSE_PROXY_LIMIT`, you can limit trusted proxies level.
 
 Notice: Reverse Proxy Auth doesn't support the API. You still need an access token or basic auth to make API requests.
+
+## SAML
+
+### Configuring Gitea as a SAML 2.0 Service Provider
+
+- Navigate to `Site Administration > Identity & Access > Authentication Sources`.
+- Click the `Add Authentication Source` button.
+- Select `SAML` as the authentication type.
+
+#### Features Not Yet Supported
+
+Currently, auto-registration is not supported for SAML. During the external account linking process the user will be prompted to set a username and email address or link to an existing account.
+
+SAML group mapping is not supported.
+
+#### Settings
+
+- `Authentication Name` **(required)**
+
+  - The name of this authentication source (appears in the Gitea ACS and metadata URLs)
+
+- `SAML NameID Format` **(required)**
+
+  - This specifies how Identity Provider (IdP) users are mapped to Gitea users. This option will be provider specific.
+
+- `Icon URL` (optional)
+
+  - URL of an icon to display on the Sign-In page for this authentication source.
+
+- `[Insecure] Skip Assertion Signature Validation` (optional)
+
+  - This option is not recommended and disables integrity verification of IdP SAML assertions.
+
+- `Identity Provider Metadata URL` (optional if XML set)
+
+  - The URL of the IdP metadata endpoint.
+  - This field must be set if `Identity Provider Metadata XML` is left blank.
+
+- `Identity Provider Metadata XML` (optional if URL set)
+
+  - The XML returned by the IdP metadata endpoint.
+  - This field must be set if `Identity Provider Metadata URL` is left blank.
+
+- `Service Provider Certificate` (optional)
+
+  - X.509-formatted certificate (with `Service Provider Private Key`) used for signing SAML requests.
+  - A certificate will be generated if this field is left blank.
+
+- `Service Provider Private Key` (optional)
+
+  - DSA/RSA private key (with `Service Provider Certificate`) used for signing SAML requests.
+  - A private key will be generated if this field is left blank.
+
+- `Email Assertion Key` (optional)
+
+  - The SAML assertion key used for the IdP user's email (depends on provider configuration).
+
+- `Name Assertion Key` (optional)
+
+  - The SAML assertion key used for the IdP user's nickname (depends on provider configuration).
+
+- `Username Assertion Key` (optional)
+
+  - The SAML assertion key used for the IdP user's username (depends on provider configuration).
+
+### Configuring a SAML 2.0 Identity Provider to use Gitea
+
+- The service provider assertion consumer service url will look like: `http(s)://[mydomain]/user/saml/[Authentication Name]/acs`.
+- The service provider metadata url will look like: `http(s)://[mydomain]/user/saml/[Authentication Name]/metadata`.
diff --git a/go.mod b/go.mod
index 7a752ec874..012a34612f 100644
--- a/go.mod
+++ b/go.mod
@@ -91,6 +91,8 @@ require (
 	github.com/quasoft/websspi v1.1.2
 	github.com/redis/go-redis/v9 v9.4.0
 	github.com/robfig/cron/v3 v3.0.1
+	github.com/russellhaering/gosaml2 v0.9.1
+	github.com/russellhaering/goxmldsig v1.3.0
 	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
 	github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd
 	github.com/sergi/go-diff v1.3.1
@@ -143,6 +145,7 @@ require (
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
+	github.com/beevik/etree v1.1.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bits-and-blooms/bitset v1.13.0 // indirect
 	github.com/blevesearch/bleve_index_api v1.1.5 // indirect
@@ -216,6 +219,7 @@ require (
 	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/jessevdk/go-flags v1.5.0 // indirect
+	github.com/jonboulle/clockwork v0.3.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/klauspost/pgzip v1.2.6 // indirect
@@ -225,6 +229,7 @@ require (
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/markbates/going v1.0.3 // indirect
+	github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/mholt/acmez v1.2.0 // indirect
diff --git a/go.sum b/go.sum
index b3b8ad8ce4..393e10cfa0 100644
--- a/go.sum
+++ b/go.sum
@@ -130,6 +130,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bits-and-blooms/bitset v1.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE=
@@ -566,6 +568,9 @@ github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZO
 github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
+github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -634,6 +639,8 @@ github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE
 github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
 github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
 github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
+github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
+github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -766,12 +773,17 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/russellhaering/gosaml2 v0.9.1 h1:H/whrl8NuSoxyW46Ww5lKPskm+5K+qYLw9afqJ/Zef0=
+github.com/russellhaering/gosaml2 v0.9.1/go.mod h1:ja+qgbayxm+0mxBRLMSUuX3COqy+sb0RRhIGun/W2kc=
+github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM=
+github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go
index 9d53fffc78..a252458d4e 100644
--- a/models/auth/oauth2.go
+++ b/models/auth/oauth2.go
@@ -8,6 +8,7 @@ import (
 	"crypto/sha256"
 	"encoding/base32"
 	"encoding/base64"
+	"encoding/gob"
 	"fmt"
 	"net"
 	"net/url"
@@ -81,6 +82,10 @@ func Init(ctx context.Context) error {
 		builtinAllClientIDs = append(builtinAllClientIDs, clientID)
 	}
 
+	// This is needed in order to encode and store the struct in the goth/gothic session
+	// during the process of linking the external user.
+	gob.Register(LinkAccountUser{})
+
 	var registeredApps []*OAuth2Application
 	if err := db.GetEngine(ctx).In("client_id", builtinAllClientIDs).Find(&registeredApps); err != nil {
 		return err
@@ -605,21 +610,6 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error {
 	return util.ErrNotExist
 }
 
-// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
-func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
-	authSource := new(Source)
-	has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
-	if err != nil {
-		return nil, err
-	}
-
-	if !has {
-		return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
-	}
-
-	return authSource, nil
-}
-
 func DeleteOAuth2RelictsByUserID(ctx context.Context, userID int64) error {
 	deleteCond := builder.Select("id").From("oauth2_grant").Where(builder.Eq{"oauth2_grant.user_id": userID})
 
diff --git a/models/auth/source.go b/models/auth/source.go
index 1bdde8235c..bc564d35ba 100644
--- a/models/auth/source.go
+++ b/models/auth/source.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
+	"github.com/markbates/goth"
 	"xorm.io/builder"
 	"xorm.io/xorm"
 	"xorm.io/xorm/convert"
@@ -32,6 +33,7 @@ const (
 	DLDAP       // 5
 	OAuth2      // 6
 	SSPI        // 7
+	SAML        // 8
 )
 
 // String returns the string name of the LoginType
@@ -52,6 +54,7 @@ var Names = map[Type]string{
 	PAM:    "PAM",
 	OAuth2: "OAuth2",
 	SSPI:   "SPNEGO with SSPI",
+	SAML:   "SAML",
 }
 
 // Config represents login config as far as the db is concerned
@@ -121,6 +124,12 @@ type Source struct {
 	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 }
 
+// LinkAccountUser is used to link an external user with a local user
+type LinkAccountUser struct {
+	Type     Type
+	GothUser goth.User
+}
+
 // TableName xorm will read the table name from this method
 func (Source) TableName() string {
 	return "login_source"
@@ -180,6 +189,11 @@ func (source *Source) IsSSPI() bool {
 	return source.Type == SSPI
 }
 
+// IsSAML returns true of this source is of the SAML type.
+func (source *Source) IsSAML() bool {
+	return source.Type == SAML
+}
+
 // HasTLS returns true of this source supports TLS.
 func (source *Source) HasTLS() bool {
 	hasTLSer, ok := source.Cfg.(HasTLSer)
@@ -392,3 +406,27 @@ func IsErrSourceInUse(err error) bool {
 func (err ErrSourceInUse) Error() string {
 	return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID)
 }
+
+// GetActiveAuthProviderSources returns all activated sources
+func GetActiveAuthProviderSources(ctx context.Context, authType Type) ([]*Source, error) {
+	sources := make([]*Source, 0, 1)
+	if err := db.GetEngine(ctx).Where("is_active = ? and type = ?", true, authType).Find(&sources); err != nil {
+		return nil, err
+	}
+	return sources, nil
+}
+
+// GetActiveAuthSourceByName returns an AuthSource based on the given name and type
+func GetActiveAuthSourceByName(ctx context.Context, name string, authType Type) (*Source, error) {
+	authSource := new(Source)
+	has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, authType, true).Get(authSource)
+	if err != nil {
+		return nil, err
+	}
+
+	if !has {
+		return nil, fmt.Errorf("auth source not found, name: %q", name)
+	}
+
+	return authSource, nil
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 574e99e654..ae34d72e41 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -522,6 +522,9 @@ Content = Content
 SSPISeparatorReplacement = Separator
 SSPIDefaultLanguage = Default Language
 
+SAMLMetadata = Either SAML Identity Provider metadata URL or XML
+SAMLMetadataURL = SAML Identity Provider metadata URL is invalid
+
 require_error = ` cannot be empty.`
 alpha_dash_error = ` should contain only alphanumeric, dash ('-') and underscore ('_') characters.`
 alpha_dash_dot_error = ` should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.`
@@ -3026,7 +3029,18 @@ auths.sspi_separator_replacement = Separator to use instead of \, / and @
 auths.sspi_separator_replacement_helper = The character to use to replace the separators of down-level logon names (eg. the \ in "DOMAIN\user") and user principal names (eg. the @ in "user@example.org").
 auths.sspi_default_language = Default user language
 auths.sspi_default_language_helper = Default language for users automatically created by SSPI auth method. Leave empty if you prefer language to be automatically detected.
+auths.saml_nameidformat = SAML NameID Format
+auths.saml_identity_provider_metadata_url = Identity Provider Metadata URL
+auths.saml_identity_provider_metadata = Identity Provider Metadata XML
+auths.saml_insecure_skip_assertion_signature_validation = [Insecure] Skip Assertion Signature Validation
+auths.saml_service_provider_certificate = Service Provider Certificate
+auths.saml_service_provider_private_key = Service Provider Private Key
+auths.saml_identity_provider_email_assertion_key = Email Assertion Key
+auths.saml_identity_provider_name_assertion_key = Name Assertion Key
+auths.saml_identity_provider_username_assertion_key = Username Assertion Key
+auths.saml_icon_url = Icon URL
 auths.tips = Tips
+auths.tips.saml = Documentation can be found at https://docs.gitea.com/usage/authentication#saml
 auths.tips.oauth2.general = OAuth2 Authentication
 auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be:
 auths.tip.oauth2_provider = OAuth2 Provider
diff --git a/routers/init.go b/routers/init.go
index e0a7150ba3..9ae8c368a2 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -35,6 +35,7 @@ import (
 	actions_service "code.gitea.io/gitea/services/actions"
 	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/auth/source/saml"
 	"code.gitea.io/gitea/services/automerge"
 	"code.gitea.io/gitea/services/cron"
 	feed_service "code.gitea.io/gitea/services/feed"
@@ -138,6 +139,7 @@ func InitWebInstalled(ctx context.Context) {
 	log.Info("ORM engine initialization successful!")
 	mustInit(system.Init)
 	mustInitCtx(ctx, oauth2.Init)
+	mustInitCtx(ctx, saml.Init)
 
 	mustInit(release_service.Init)
 
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 7fdd18dfae..187b569d39 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -1,9 +1,12 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2024 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package admin
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
 	"net/http"
@@ -25,6 +28,7 @@ import (
 	"code.gitea.io/gitea/services/auth/source/ldap"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	pam_service "code.gitea.io/gitea/services/auth/source/pam"
+	"code.gitea.io/gitea/services/auth/source/saml"
 	"code.gitea.io/gitea/services/auth/source/smtp"
 	"code.gitea.io/gitea/services/auth/source/sspi"
 	"code.gitea.io/gitea/services/forms"
@@ -71,6 +75,7 @@ var (
 			{auth.SMTP.String(), auth.SMTP},
 			{auth.OAuth2.String(), auth.OAuth2},
 			{auth.SSPI.String(), auth.SSPI},
+			{auth.SAML.String(), auth.SAML},
 		}
 		if pam.Supported {
 			items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
@@ -83,6 +88,16 @@ var (
 		{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
 		{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
 	}
+
+	nameIDFormats = []dropdownItem{
+		{saml.NameIDFormatNames[saml.SAML20Persistent], saml.SAML20Persistent}, // use this as default value
+		{saml.NameIDFormatNames[saml.SAML11Email], saml.SAML11Email},
+		{saml.NameIDFormatNames[saml.SAML11Persistent], saml.SAML11Persistent},
+		{saml.NameIDFormatNames[saml.SAML11Unspecified], saml.SAML11Unspecified},
+		{saml.NameIDFormatNames[saml.SAML20Email], saml.SAML20Email},
+		{saml.NameIDFormatNames[saml.SAML20Transient], saml.SAML20Transient},
+		{saml.NameIDFormatNames[saml.SAML20Unspecified], saml.SAML20Unspecified},
+	}
 )
 
 // NewAuthSource render adding a new auth source page
@@ -98,6 +113,8 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.Data["is_sync_enabled"] = true
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
+	ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[saml.SAML20Persistent]
+	ctx.Data["NameIDFormats"] = nameIDFormats
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	oauth2providers := oauth2.GetSupportedOAuth2Providers()
 	ctx.Data["OAuth2Providers"] = oauth2providers
@@ -231,6 +248,52 @@ func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi
 	}, nil
 }
 
+func parseSAMLConfig(ctx *context.Context, form forms.AuthenticationForm) (*saml.Source, error) {
+	if util.IsEmptyString(form.IdentityProviderMetadata) && util.IsEmptyString(form.IdentityProviderMetadataURL) {
+		return nil, fmt.Errorf("%s %s", ctx.Tr("form.SAMLMetadata"), ctx.Tr("form.require_error"))
+	}
+
+	if !util.IsEmptyString(form.IdentityProviderMetadataURL) {
+		_, err := url.Parse(form.IdentityProviderMetadataURL)
+		if err != nil {
+			return nil, fmt.Errorf("%s", ctx.Tr("form.SAMLMetadataURL"))
+		}
+	}
+
+	// check the integrity of the certificate and private key (autogenerated if these form fields are blank)
+	if !util.IsEmptyString(form.ServiceProviderCertificate) && !util.IsEmptyString(form.ServiceProviderPrivateKey) {
+		keyPair, err := tls.X509KeyPair([]byte(form.ServiceProviderCertificate), []byte(form.ServiceProviderPrivateKey))
+		if err != nil {
+			return nil, err
+		}
+		keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		privateKey, cert, err := saml.GenerateSAMLSPKeypair()
+		if err != nil {
+			return nil, err
+		}
+
+		form.ServiceProviderPrivateKey = privateKey
+		form.ServiceProviderCertificate = cert
+	}
+
+	return &saml.Source{
+		IdentityProviderMetadata:                 form.IdentityProviderMetadata,
+		IdentityProviderMetadataURL:              form.IdentityProviderMetadataURL,
+		InsecureSkipAssertionSignatureValidation: form.InsecureSkipAssertionSignatureValidation,
+		NameIDFormat:                             saml.NameIDFormat(form.NameIDFormat),
+		ServiceProviderCertificate:               form.ServiceProviderCertificate,
+		ServiceProviderPrivateKey:                form.ServiceProviderPrivateKey,
+		EmailAssertionKey:                        form.EmailAssertionKey,
+		NameAssertionKey:                         form.NameAssertionKey,
+		UsernameAssertionKey:                     form.UsernameAssertionKey,
+		IconURL:                                  form.SAMLIconURL,
+	}, nil
+}
+
 // NewAuthSourcePost response for adding an auth source
 func NewAuthSourcePost(ctx *context.Context) {
 	form := *web.GetForm(ctx).(*forms.AuthenticationForm)
@@ -244,6 +307,8 @@ func NewAuthSourcePost(ctx *context.Context) {
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	oauth2providers := oauth2.GetSupportedOAuth2Providers()
 	ctx.Data["OAuth2Providers"] = oauth2providers
+	ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[saml.NameIDFormat(form.NameIDFormat)]
+	ctx.Data["NameIDFormats"] = nameIDFormats
 
 	ctx.Data["SSPIAutoCreateUsers"] = true
 	ctx.Data["SSPIAutoActivateUsers"] = true
@@ -290,6 +355,13 @@ func NewAuthSourcePost(ctx *context.Context) {
 			ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
 			return
 		}
+	case auth.SAML:
+		var err error
+		config, err = parseSAMLConfig(ctx, form)
+		if err != nil {
+			ctx.RenderWithErr(err.Error(), tplAuthNew, form)
+			return
+		}
 	default:
 		ctx.Error(http.StatusBadRequest)
 		return
@@ -336,6 +408,7 @@ func EditAuthSource(ctx *context.Context) {
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	oauth2providers := oauth2.GetSupportedOAuth2Providers()
 	ctx.Data["OAuth2Providers"] = oauth2providers
+	ctx.Data["NameIDFormats"] = nameIDFormats
 
 	source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
 	if err != nil {
@@ -344,6 +417,9 @@ func EditAuthSource(ctx *context.Context) {
 	}
 	ctx.Data["Source"] = source
 	ctx.Data["HasTLS"] = source.HasTLS()
+	if source.IsSAML() {
+		ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[source.Cfg.(*saml.Source).NameIDFormat]
+	}
 
 	if source.IsOAuth2() {
 		type Named interface {
@@ -378,6 +454,8 @@ func EditAuthSourcePost(ctx *context.Context) {
 	}
 	ctx.Data["Source"] = source
 	ctx.Data["HasTLS"] = source.HasTLS()
+	ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[saml.SAML20Persistent]
+	ctx.Data["NameIDFormats"] = nameIDFormats
 
 	if ctx.HasError() {
 		ctx.HTML(http.StatusOK, tplAuthEdit)
@@ -412,6 +490,12 @@ func EditAuthSourcePost(ctx *context.Context) {
 			ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
 			return
 		}
+	case auth.SAML:
+		config, err = parseSAMLConfig(ctx, form)
+		if err != nil {
+			ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
+			return
+		}
 	default:
 		ctx.Error(http.StatusBadRequest)
 		return
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 3de1f3373d..f5955ec5ff 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -28,6 +28,7 @@ import (
 	"code.gitea.io/gitea/routers/utils"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/auth/source/saml"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -170,6 +171,14 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 	ctx.Data["OAuth2Providers"] = oauth2Providers
+
+	samlProviders, err := saml.GetSAMLProviders(ctx, util.OptionalBoolTrue)
+	if err != nil {
+		ctx.ServerError("UserSignIn", err)
+		return
+	}
+	ctx.Data["SAMLProviders"] = samlProviders
+
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
 	ctx.Data["PageIsSignIn"] = true
@@ -193,6 +202,14 @@ func SignInPost(ctx *context.Context) {
 		return
 	}
 	ctx.Data["OAuth2Providers"] = oauth2Providers
+
+	samlProviders, err := saml.GetSAMLProviders(ctx, util.OptionalBoolTrue)
+	if err != nil {
+		ctx.ServerError("UserSignIn", err)
+		return
+	}
+	ctx.Data["SAMLProviders"] = samlProviders
+
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
 	ctx.Data["PageIsSignIn"] = true
@@ -504,7 +521,7 @@ func SignUpPost(ctx *context.Context) {
 		Passwd: form.Password,
 	}
 
-	if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false) {
+	if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false, auth.NoType) {
 		// error already handled
 		return
 	}
@@ -515,16 +532,16 @@ func SignUpPost(ctx *context.Context) {
 
 // createAndHandleCreatedUser calls createUserInContext and
 // then handleUserCreated.
-func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool {
-	if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) {
+func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool, authType auth.Type) bool {
+	if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink, authType) {
 		return false
 	}
-	return handleUserCreated(ctx, u, gothUser)
+	return handleUserCreated(ctx, u, gothUser, authType)
 }
 
 // createUserInContext creates a user and handles errors within a given context.
 // Optionally a template can be specified.
-func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
+func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool, authType auth.Type) (ok bool) {
 	if err := user_model.CreateUser(ctx, u, overwrites); err != nil {
 		if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
 			if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
@@ -541,10 +558,10 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
 				}
 
 				// TODO: probably we should respect 'remember' user's choice...
-				linkAccount(ctx, user, *gothUser, true)
+				linkAccount(ctx, user, *gothUser, true, authType)
 				return false // user is already created here, all redirects are handled
 			} else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
-				showLinkingLogin(ctx, *gothUser)
+				showLinkingLogin(ctx, *gothUser, authType)
 				return false // user will be created only after linking login
 			}
 		}
@@ -590,7 +607,7 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
 // handleUserCreated does additional steps after a new user is created.
 // It auto-sets admin for the only user, updates the optional external user and
 // sends a confirmation email if required.
-func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
+func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User, authType auth.Type) (ok bool) {
 	// Auto-set admin for the only user.
 	if user_model.CountUsers(ctx, nil) == 1 {
 		opts := &user_service.UpdateOptions{
@@ -606,7 +623,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
 
 	// update external user information
 	if gothUser != nil {
-		if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
+		if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser, authType); err != nil {
 			if !errors.Is(err, util.ErrNotExist) {
 				log.Error("UpdateExternalUser failed: %v", err)
 			}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 1d94e52fe3..c62ae84083 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -48,13 +48,13 @@ func LinkAccount(ctx *context.Context) {
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
-	gothUser := ctx.Session.Get("linkAccountGothUser")
-	if gothUser == nil {
+	externalLinkUser := ctx.Session.Get("linkAccountUser")
+	if externalLinkUser == nil {
 		ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
 		return
 	}
 
-	gu, _ := gothUser.(goth.User)
+	gu := externalLinkUser.(auth.LinkAccountUser).GothUser
 	uname, err := getUserName(&gu)
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
@@ -135,12 +135,14 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
-	gothUser := ctx.Session.Get("linkAccountGothUser")
-	if gothUser == nil {
+	externalLinkUserInterface := ctx.Session.Get("linkAccountUser")
+	if externalLinkUserInterface == nil {
 		ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
 		return
 	}
 
+	externalLinkUser := externalLinkUserInterface.(auth.LinkAccountUser)
+
 	if ctx.HasError() {
 		ctx.HTML(http.StatusOK, tplLinkAccount)
 		return
@@ -152,10 +154,10 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 		return
 	}
 
-	linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
+	linkAccount(ctx, u, externalLinkUser.GothUser, signInForm.Remember, externalLinkUser.Type)
 }
 
-func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) {
+func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool, authType auth.Type) {
 	updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
 
 	// If this user is enrolled in 2FA, we can't sign the user in just yet.
@@ -168,7 +170,7 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r
 			return
 		}
 
-		err = externalaccount.LinkAccountToUser(ctx, u, gothUser)
+		err = externalaccount.LinkAccountToUser(ctx, u, gothUser, authType)
 		if err != nil {
 			ctx.ServerError("UserLinkAccount", err)
 			return
@@ -222,14 +224,14 @@ func LinkAccountPostRegister(ctx *context.Context) {
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
-	gothUserInterface := ctx.Session.Get("linkAccountGothUser")
-	if gothUserInterface == nil {
+	externalLinkUser := ctx.Session.Get("linkAccountUser")
+	if externalLinkUser == nil {
 		ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
 		return
 	}
-	gothUser, ok := gothUserInterface.(goth.User)
+	linkUser, ok := externalLinkUser.(auth.LinkAccountUser)
 	if !ok {
-		ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
+		ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountUser type is %t but not goth.User", externalLinkUser))
 		return
 	}
 
@@ -275,7 +277,7 @@ func LinkAccountPostRegister(ctx *context.Context) {
 		}
 	}
 
-	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
+	authSource, err := auth.GetActiveAuthSourceByName(ctx, linkUser.GothUser.Provider, linkUser.Type)
 	if err != nil {
 		ctx.ServerError("CreateUser", err)
 		return
@@ -285,21 +287,24 @@ func LinkAccountPostRegister(ctx *context.Context) {
 		Name:        form.UserName,
 		Email:       form.Email,
 		Passwd:      form.Password,
-		LoginType:   auth.OAuth2,
+		LoginType:   authSource.Type,
 		LoginSource: authSource.ID,
-		LoginName:   gothUser.UserID,
+		LoginName:   linkUser.GothUser.UserID,
 	}
 
-	if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) {
+	if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &linkUser.GothUser, false, linkUser.Type) {
 		// error already handled
 		return
 	}
 
-	source := authSource.Cfg.(*oauth2.Source)
-	if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
-		ctx.ServerError("SyncGroupsToTeams", err)
-		return
+	if linkUser.Type == auth.OAuth2 {
+		source := authSource.Cfg.(*oauth2.Source)
+		if err := syncGroupsToTeams(ctx, source, &linkUser.GothUser, u); err != nil {
+			ctx.ServerError("SyncGroupsToTeams", err)
+			return
+		}
 	}
+	// TODO we will support some form of group mapping for SAML
 
 	handleSignIn(ctx, u, false)
 }
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index ee0770ef37..d00644dd5f 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -841,7 +841,7 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect
 func SignInOAuth(ctx *context.Context) {
 	provider := ctx.Params(":provider")
 
-	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
+	authSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.OAuth2)
 	if err != nil {
 		ctx.ServerError("SignIn", err)
 		return
@@ -892,7 +892,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 	}
 
 	// first look if the provider is still active
-	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
+	authSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.OAuth2)
 	if err != nil {
 		ctx.ServerError("SignIn", err)
 		return
@@ -935,7 +935,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 	if u == nil {
 		if ctx.Doer != nil {
 			// attach user to already logged in user
-			err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
+			err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser, auth.OAuth2)
 			if err != nil {
 				ctx.ServerError("UserLinkAccount", err)
 				return
@@ -988,7 +988,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			u.IsAdmin = isAdmin.ValueOrDefault(false)
 			u.IsRestricted = isRestricted.ValueOrDefault(false)
 
-			if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
+			if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled, auth.OAuth2) {
 				// error already handled
 				return
 			}
@@ -999,7 +999,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			}
 		} else {
 			// no existing user is found, request attach or new account
-			showLinkingLogin(ctx, gothUser)
+			showLinkingLogin(ctx, gothUser, auth.OAuth2)
 			return
 		}
 	}
@@ -1063,9 +1063,12 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g
 	return isAdmin, isRestricted
 }
 
-func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
+func showLinkingLogin(ctx *context.Context, gothUser goth.User, authType auth.Type) {
 	if err := updateSession(ctx, nil, map[string]any{
-		"linkAccountGothUser": gothUser,
+		"linkAccountUser": auth.LinkAccountUser{
+			Type:     authType,
+			GothUser: gothUser,
+		},
 	}); err != nil {
 		ctx.ServerError("updateSession", err)
 		return
@@ -1144,7 +1147,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
 		}
 
 		// update external user information
-		if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
+		if err := externalaccount.UpdateExternalUser(ctx, u, gothUser, auth.OAuth2); err != nil {
 			if !errors.Is(err, util.ErrNotExist) {
 				log.Error("UpdateExternalUser failed: %v", err)
 			}
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index 29ef772b1c..bf377b4496 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"net/url"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/openid"
 	"code.gitea.io/gitea/modules/base"
@@ -363,7 +364,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
 		Email:  form.Email,
 		Passwd: password,
 	}
-	if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false) {
+	if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false, auth_model.NoType) {
 		// error already handled
 		return
 	}
@@ -379,7 +380,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
 		return
 	}
 
-	if !handleUserCreated(ctx, u, nil) {
+	if !handleUserCreated(ctx, u, nil, auth_model.NoType) {
 		// error already handled
 		return
 	}
diff --git a/routers/web/auth/saml.go b/routers/web/auth/saml.go
new file mode 100644
index 0000000000..29d689d2e9
--- /dev/null
+++ b/routers/web/auth/saml.go
@@ -0,0 +1,172 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"code.gitea.io/gitea/models/auth"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/auth/source/saml"
+	"code.gitea.io/gitea/services/externalaccount"
+
+	"github.com/markbates/goth"
+)
+
+func SignInSAML(ctx *context.Context) {
+	provider := ctx.Params(":provider")
+
+	loginSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.SAML)
+	if err != nil || loginSource == nil {
+		ctx.NotFound("SAMLMetadata", err)
+		return
+	}
+
+	if err = loginSource.Cfg.(*saml.Source).Callout(ctx.Req, ctx.Resp); err != nil {
+		if strings.Contains(err.Error(), "no provider for ") {
+			ctx.Error(http.StatusNotFound)
+			return
+		}
+		ctx.ServerError("SignIn", err)
+	}
+}
+
+func SignInSAMLCallback(ctx *context.Context) {
+	provider := ctx.Params(":provider")
+	loginSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.SAML)
+	if err != nil || loginSource == nil {
+		ctx.NotFound("SignInSAMLCallback", err)
+		return
+	}
+
+	if loginSource == nil {
+		ctx.ServerError("SignIn", fmt.Errorf("no valid provider found, check configured callback url in provider"))
+		return
+	}
+
+	u, gothUser, err := samlUserLoginCallback(*ctx, loginSource, ctx.Req, ctx.Resp)
+	if err != nil {
+		ctx.ServerError("SignInSAMLCallback", err)
+		return
+	}
+
+	if u == nil {
+		if ctx.Doer != nil {
+			// attach user to already logged in user
+			err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser, auth.SAML)
+			if err != nil {
+				ctx.ServerError("LinkAccountToUser", err)
+				return
+			}
+
+			ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+			return
+		} else if !setting.Service.AllowOnlyInternalRegistration && false {
+			// TODO: allow auto registration from saml users (OAuth2 uses the following setting.OAuth2Client.EnableAutoRegistration)
+		} else {
+			// no existing user is found, request attach or new account
+			showLinkingLogin(ctx, gothUser, auth.SAML)
+			return
+		}
+	}
+
+	handleSamlSignIn(ctx, loginSource, u, gothUser)
+}
+
+func handleSamlSignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) {
+	if err := updateSession(ctx, nil, map[string]any{
+		"uid":   u.ID,
+		"uname": u.Name,
+	}); err != nil {
+		ctx.ServerError("updateSession", err)
+		return
+	}
+
+	// Clear whatever CSRF cookie has right now, force to generate a new one
+	ctx.Csrf.DeleteCookie(ctx)
+
+	// Register last login
+	u.SetLastLogin()
+
+	// update external user information
+	if err := externalaccount.UpdateExternalUser(ctx, u, gothUser, auth.SAML); err != nil {
+		if !errors.Is(err, util.ErrNotExist) {
+			log.Error("UpdateExternalUser failed: %v", err)
+		}
+	}
+
+	if err := resetLocale(ctx, u); err != nil {
+		ctx.ServerError("resetLocale", err)
+		return
+	}
+
+	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
+		middleware.DeleteRedirectToCookie(ctx.Resp)
+		ctx.RedirectToFirst(redirectTo)
+		return
+	}
+
+	ctx.Redirect(setting.AppSubURL + "/")
+}
+
+func samlUserLoginCallback(ctx context.Context, authSource *auth.Source, request *http.Request, response http.ResponseWriter) (*user_model.User, goth.User, error) {
+	samlSource := authSource.Cfg.(*saml.Source)
+
+	gothUser, err := samlSource.Callback(request, response)
+	if err != nil {
+		return nil, gothUser, err
+	}
+
+	user := &user_model.User{
+		LoginName:   gothUser.UserID,
+		LoginType:   auth.SAML,
+		LoginSource: authSource.ID,
+	}
+
+	hasUser, err := user_model.GetUser(ctx, user)
+	if err != nil {
+		return nil, goth.User{}, err
+	}
+
+	if hasUser {
+		return user, gothUser, nil
+	}
+
+	// search in external linked users
+	externalLoginUser := &user_model.ExternalLoginUser{
+		ExternalID:    gothUser.UserID,
+		LoginSourceID: authSource.ID,
+	}
+	hasUser, err = user_model.GetExternalLogin(ctx, externalLoginUser)
+	if err != nil {
+		return nil, goth.User{}, err
+	}
+	if hasUser {
+		user, err = user_model.GetUserByID(request.Context(), externalLoginUser.UserID)
+		return user, gothUser, err
+	}
+
+	// no user found to login
+	return nil, gothUser, nil
+}
+
+func SAMLMetadata(ctx *context.Context) {
+	provider := ctx.Params(":provider")
+	loginSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.SAML)
+	if err != nil || loginSource == nil {
+		ctx.NotFound("SAMLMetadata", err)
+		return
+	}
+	if err = loginSource.Cfg.(*saml.Source).Metadata(ctx.Req, ctx.Resp); err != nil {
+		ctx.ServerError("SAMLMetadata", err)
+	}
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 864164972e..77c8319f06 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -667,6 +667,11 @@ func registerRoutes(m *web.Route) {
 			m.Get("/{provider}", auth.SignInOAuth)
 			m.Get("/{provider}/callback", auth.SignInOAuthCallback)
 		})
+		m.Group("/saml", func() {
+			m.Get("/{provider}", auth.SignInSAML) // redir to SAML IDP
+			m.Post("/{provider}/acs", auth.SignInSAMLCallback)
+			m.Get("/{provider}/metadata", auth.SAMLMetadata)
+		})
 	})
 	// ***** END: User *****
 
diff --git a/services/auth/source/saml/assert_interface_test.go b/services/auth/source/saml/assert_interface_test.go
new file mode 100644
index 0000000000..2ca7057b8a
--- /dev/null
+++ b/services/auth/source/saml/assert_interface_test.go
@@ -0,0 +1,22 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml_test
+
+import (
+	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/saml"
+)
+
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
+type sourceInterface interface {
+	auth_model.Config
+	auth_model.SourceSettable
+	auth_model.RegisterableSource
+	auth.PasswordAuthenticator
+}
+
+var _ (sourceInterface) = &saml.Source{}
diff --git a/services/auth/source/saml/init.go b/services/auth/source/saml/init.go
new file mode 100644
index 0000000000..f1d6d9fa4b
--- /dev/null
+++ b/services/auth/source/saml/init.go
@@ -0,0 +1,29 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+import (
+	"context"
+	"sync"
+
+	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/modules/log"
+)
+
+var samlRWMutex = sync.RWMutex{}
+
+func Init(ctx context.Context) error {
+	loginSources, _ := auth.GetActiveAuthProviderSources(ctx, auth.SAML)
+	for _, source := range loginSources {
+		samlSource, ok := source.Cfg.(*Source)
+		if !ok {
+			continue
+		}
+		err := samlSource.RegisterSource()
+		if err != nil {
+			log.Error("Unable to register source: %s due to Error: %v.", source.Name, err)
+		}
+	}
+	return nil
+}
diff --git a/services/auth/source/saml/name_id_format.go b/services/auth/source/saml/name_id_format.go
new file mode 100644
index 0000000000..1ddf047729
--- /dev/null
+++ b/services/auth/source/saml/name_id_format.go
@@ -0,0 +1,38 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+type NameIDFormat int
+
+const (
+	SAML11Email NameIDFormat = iota + 1
+	SAML11Persistent
+	SAML11Unspecified
+	SAML20Email
+	SAML20Persistent
+	SAML20Transient
+	SAML20Unspecified
+)
+
+const DefaultNameIDFormat NameIDFormat = SAML20Persistent
+
+var NameIDFormatNames = map[NameIDFormat]string{
+	SAML11Email:       "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
+	SAML11Persistent:  "urn:oasis:names:tc:SAML:1.1:nameid-format:persistent",
+	SAML11Unspecified: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
+	SAML20Email:       "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress",
+	SAML20Persistent:  "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
+	SAML20Transient:   "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
+	SAML20Unspecified: "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
+}
+
+// String returns the name of the NameIDFormat
+func (n NameIDFormat) String() string {
+	return NameIDFormatNames[n]
+}
+
+// Int returns the int value of the NameIDFormat
+func (n NameIDFormat) Int() int {
+	return int(n)
+}
diff --git a/services/auth/source/saml/providers.go b/services/auth/source/saml/providers.go
new file mode 100644
index 0000000000..d0b36ff44d
--- /dev/null
+++ b/services/auth/source/saml/providers.go
@@ -0,0 +1,109 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+import (
+	"context"
+	"fmt"
+	"html"
+	"html/template"
+	"io"
+	"net/http"
+	"sort"
+	"time"
+
+	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/httplib"
+	"code.gitea.io/gitea/modules/svg"
+	"code.gitea.io/gitea/modules/util"
+)
+
+// Providers is list of known/available providers.
+type Providers map[string]Source
+
+var providers = Providers{}
+
+// Provider is an interface for describing a single SAML provider
+type Provider interface {
+	Name() string
+	IconHTML(size int) template.HTML
+}
+
+// AuthSourceProvider is a SAML provider
+type AuthSourceProvider struct {
+	sourceName, iconURL string
+}
+
+func (p *AuthSourceProvider) Name() string {
+	return p.sourceName
+}
+
+func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
+	if p.iconURL != "" {
+		return template.HTML(fmt.Sprintf(`<img class="gt-object-contain gt-mr-3" width="%d" height="%d" src="%s" alt="%s">`,
+			size,
+			size,
+			html.EscapeString(p.iconURL), html.EscapeString(p.Name()),
+		))
+	}
+	return svg.RenderHTML("gitea-lock-cog", size, "gt-mr-3")
+}
+
+func readIdentityProviderMetadata(ctx context.Context, source *Source) ([]byte, error) {
+	if source.IdentityProviderMetadata != "" {
+		return []byte(source.IdentityProviderMetadata), nil
+	}
+
+	req := httplib.NewRequest(source.IdentityProviderMetadataURL, "GET")
+	req.SetTimeout(20*time.Second, time.Minute)
+	resp, err := req.Response()
+	if err != nil {
+		return nil, fmt.Errorf("Unable to contact gitea: %v", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return nil, err
+	}
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	return data, nil
+}
+
+func createProviderFromSource(source *auth.Source) (Provider, error) {
+	samlCfg, ok := source.Cfg.(*Source)
+	if !ok {
+		return nil, fmt.Errorf("invalid SAML source config: %v", samlCfg)
+	}
+	return &AuthSourceProvider{sourceName: source.Name, iconURL: samlCfg.IconURL}, nil
+}
+
+// GetSAMLProviders returns the list of configured SAML providers
+func GetSAMLProviders(ctx context.Context, isActive util.OptionalBool) ([]Provider, error) {
+	authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
+		IsActive:  isActive,
+		LoginType: auth.SAML,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	samlProviders := make([]Provider, 0, len(authSources))
+	for _, source := range authSources {
+		p, err := createProviderFromSource(source)
+		if err != nil {
+			return nil, err
+		}
+		samlProviders = append(samlProviders, p)
+	}
+
+	sort.Slice(samlProviders, func(i, j int) bool {
+		return samlProviders[i].Name() < samlProviders[j].Name()
+	})
+
+	return samlProviders, nil
+}
diff --git a/services/auth/source/saml/source.go b/services/auth/source/saml/source.go
new file mode 100644
index 0000000000..52388646b5
--- /dev/null
+++ b/services/auth/source/saml/source.go
@@ -0,0 +1,202 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+import (
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/pem"
+	"encoding/xml"
+	"errors"
+	"fmt"
+	"math/big"
+	"net/url"
+	"time"
+
+	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	saml2 "github.com/russellhaering/gosaml2"
+	"github.com/russellhaering/gosaml2/types"
+	dsig "github.com/russellhaering/goxmldsig"
+)
+
+// Source holds configuration for the SAML login source.
+type Source struct {
+	// IdentityProviderMetadata description: The SAML Identity Provider metadata XML contents (for static configuration of the SAML Service Provider). The value of this field should be an XML document whose root element is `<EntityDescriptor>` or `<EntityDescriptors>`. To escape the value into a JSON string, you may want to use a tool like https://json-escape-text.now.sh.
+	IdentityProviderMetadata string
+	// IdentityProviderMetadataURL description: The SAML Identity Provider metadata URL (for dynamic configuration of the SAML Service Provider).
+	IdentityProviderMetadataURL string
+	// InsecureSkipAssertionSignatureValidation description: Whether the Service Provider should (insecurely) accept assertions from the Identity Provider without a valid signature.
+	InsecureSkipAssertionSignatureValidation bool
+	// NameIDFormat description: The SAML NameID format to use when performing user authentication.
+	NameIDFormat NameIDFormat
+	// ServiceProviderCertificate description: The SAML Service Provider certificate in X.509 encoding (begins with "-----BEGIN CERTIFICATE-----"). This certificate is used by the Identity Provider to validate the Service Provider's AuthnRequests and LogoutRequests. It corresponds to the Service Provider's private key (`serviceProviderPrivateKey`). To escape the value into a JSON string, you may want to use a tool like https://json-escape-text.now.sh.
+	ServiceProviderCertificate string
+	// ServiceProviderIssuer description: The SAML Service Provider name, used to identify this Service Provider. This is required if the "externalURL" field is not set (as the SAML metadata endpoint is computed as "<externalURL>.auth/saml/metadata"), or when using multiple SAML authentication providers.
+	ServiceProviderIssuer string
+	// ServiceProviderPrivateKey description: The SAML Service Provider private key in PKCS#8 encoding (begins with "-----BEGIN PRIVATE KEY-----"). This private key is used to sign AuthnRequests and LogoutRequests. It corresponds to the Service Provider's certificate (`serviceProviderCertificate`). To escape the value into a JSON string, you may want to use a tool like https://json-escape-text.now.sh.
+	ServiceProviderPrivateKey string
+
+	CallbackURL string
+	IconURL     string
+
+	// EmailAssertionKey description: Assertion key for user.Email
+	EmailAssertionKey string
+	// NameAssertionKey description: Assertion key for user.NickName
+	NameAssertionKey string
+	// UsernameAssertionKey description: Assertion key for user.Name
+	UsernameAssertionKey string
+
+	// reference to the authSource
+	authSource *auth.Source
+
+	samlSP *saml2.SAMLServiceProvider
+}
+
+func GenerateSAMLSPKeypair() (string, string, error) {
+	key, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		return "", "", err
+	}
+
+	keyBytes := x509.MarshalPKCS1PrivateKey(key)
+	keyPem := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "RSA PRIVATE KEY",
+			Bytes: keyBytes,
+		},
+	)
+
+	now := time.Now()
+
+	template := &x509.Certificate{
+		SerialNumber: big.NewInt(0),
+		NotBefore:    now.Add(-5 * time.Minute),
+		NotAfter:     now.Add(365 * 24 * time.Hour),
+
+		KeyUsage:              x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{},
+		BasicConstraintsValid: true,
+	}
+
+	certificate, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
+	if err != nil {
+		return "", "", err
+	}
+
+	certPem := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "CERTIFICATE",
+			Bytes: certificate,
+		},
+	)
+
+	return string(keyPem), string(certPem), nil
+}
+
+func (source *Source) initSAMLSp() error {
+	source.CallbackURL = setting.AppURL + "user/saml/" + url.PathEscape(source.authSource.Name) + "/acs"
+
+	idpMetadata, err := readIdentityProviderMetadata(context.Background(), source)
+	if err != nil {
+		return err
+	}
+	{
+		if source.IdentityProviderMetadataURL != "" {
+			log.Trace(fmt.Sprintf("Identity Provider metadata: %s", source.IdentityProviderMetadataURL), string(idpMetadata))
+		}
+	}
+
+	metadata := &types.EntityDescriptor{}
+	err = xml.Unmarshal(idpMetadata, metadata)
+	if err != nil {
+		return err
+	}
+
+	certStore := dsig.MemoryX509CertificateStore{
+		Roots: []*x509.Certificate{},
+	}
+
+	if metadata.IDPSSODescriptor == nil {
+		return errors.New("saml idp metadata missing IDPSSODescriptor")
+	}
+
+	for _, kd := range metadata.IDPSSODescriptor.KeyDescriptors {
+		for idx, xcert := range kd.KeyInfo.X509Data.X509Certificates {
+			if xcert.Data == "" {
+				return fmt.Errorf("metadata certificate(%d) must not be empty", idx)
+			}
+			certData, err := base64.StdEncoding.DecodeString(xcert.Data)
+			if err != nil {
+				return err
+			}
+
+			idpCert, err := x509.ParseCertificate(certData)
+			if err != nil {
+				return err
+			}
+
+			certStore.Roots = append(certStore.Roots, idpCert)
+		}
+	}
+
+	var keyStore dsig.X509KeyStore
+
+	if source.ServiceProviderCertificate != "" && source.ServiceProviderPrivateKey != "" {
+		keyPair, err := tls.X509KeyPair([]byte(source.ServiceProviderCertificate), []byte(source.ServiceProviderPrivateKey))
+		if err != nil {
+			return err
+		}
+		keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
+		if err != nil {
+			return err
+		}
+		keyStore = dsig.TLSCertKeyStore(keyPair)
+	}
+
+	source.samlSP = &saml2.SAMLServiceProvider{
+		IdentityProviderSSOURL:      metadata.IDPSSODescriptor.SingleSignOnServices[0].Location,
+		IdentityProviderIssuer:      metadata.EntityID,
+		AudienceURI:                 setting.AppURL + "user/saml/" + url.PathEscape(source.authSource.Name) + "/metadata",
+		AssertionConsumerServiceURL: source.CallbackURL,
+		SkipSignatureValidation:     source.InsecureSkipAssertionSignatureValidation,
+		NameIdFormat:                source.NameIDFormat.String(),
+		IDPCertificateStore:         &certStore,
+		SignAuthnRequests:           source.ServiceProviderCertificate != "" && source.ServiceProviderPrivateKey != "",
+		SPKeyStore:                  keyStore,
+		ServiceProviderIssuer:       setting.AppURL + "user/saml/" + url.PathEscape(source.authSource.Name) + "/metadata",
+	}
+
+	return nil
+}
+
+// FromDB fills up a SAML from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	if err := json.UnmarshalHandleDoubleEncode(bs, &source); err != nil {
+		return err
+	}
+
+	return source.initSAMLSp()
+}
+
+// ToDB exports a SAML to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	return json.Marshal(source)
+}
+
+// SetAuthSource sets the related AuthSource
+func (source *Source) SetAuthSource(authSource *auth.Source) {
+	source.authSource = authSource
+}
+
+func init() {
+	auth.RegisterTypeConfig(auth.SAML, &Source{})
+}
diff --git a/services/auth/source/saml/source_authenticate.go b/services/auth/source/saml/source_authenticate.go
new file mode 100644
index 0000000000..d118917f87
--- /dev/null
+++ b/services/auth/source/saml/source_authenticate.go
@@ -0,0 +1,16 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+import (
+	"context"
+
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/services/auth/source/db"
+)
+
+// Authenticate falls back to the db authenticator
+func (source *Source) Authenticate(ctx context.Context, user *user_model.User, login, password string) (*user_model.User, error) {
+	return db.Authenticate(ctx, user, login, password)
+}
diff --git a/services/auth/source/saml/source_callout.go b/services/auth/source/saml/source_callout.go
new file mode 100644
index 0000000000..5366f8a527
--- /dev/null
+++ b/services/auth/source/saml/source_callout.go
@@ -0,0 +1,89 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/markbates/goth"
+)
+
+// Callout redirects request/response pair to authenticate against the provider
+func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
+	samlRWMutex.RLock()
+	defer samlRWMutex.RUnlock()
+	if _, ok := providers[source.authSource.Name]; !ok {
+		return fmt.Errorf("no provider for this saml")
+	}
+
+	authURL, err := providers[source.authSource.Name].samlSP.BuildAuthURL("")
+	if err == nil {
+		http.Redirect(response, request, authURL, http.StatusTemporaryRedirect)
+	}
+	return err
+}
+
+// Callback handles SAML callback, resolve to a goth user and send back to original url
+// this will trigger a new authentication request, but because we save it in the session we can use that
+func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
+	samlRWMutex.RLock()
+	defer samlRWMutex.RUnlock()
+
+	user := goth.User{
+		Provider: source.authSource.Name,
+	}
+	samlResponse := request.FormValue("SAMLResponse")
+	assertions, err := source.samlSP.RetrieveAssertionInfo(samlResponse)
+	if err != nil {
+		return user, err
+	}
+
+	if assertions.WarningInfo.OneTimeUse {
+		return user, fmt.Errorf("SAML response contains one time use warning")
+	}
+
+	if assertions.WarningInfo.ProxyRestriction != nil {
+		return user, fmt.Errorf("SAML response contains proxy restriction warning: %v", assertions.WarningInfo.ProxyRestriction)
+	}
+
+	if assertions.WarningInfo.NotInAudience {
+		return user, fmt.Errorf("SAML response contains audience warning")
+	}
+
+	if assertions.WarningInfo.InvalidTime {
+		return user, fmt.Errorf("SAML response contains invalid time warning")
+	}
+
+	samlMap := make(map[string]string)
+	for key, value := range assertions.Values {
+		keyParsed := strings.ToLower(key[strings.LastIndex(key, "/")+1:]) // Uses the trailing slug as the key name.
+		valueParsed := value.Values[0].Value
+		samlMap[keyParsed] = valueParsed
+
+	}
+
+	user.UserID = assertions.NameID
+	if user.UserID == "" {
+		return user, fmt.Errorf("no nameID found in SAML response")
+	}
+
+	// email
+	if _, ok := samlMap[source.EmailAssertionKey]; !ok {
+		user.Email = samlMap[source.EmailAssertionKey]
+	}
+	// name
+	if _, ok := samlMap[source.NameAssertionKey]; !ok {
+		user.NickName = samlMap[source.NameAssertionKey]
+	}
+	// username
+	if _, ok := samlMap[source.UsernameAssertionKey]; !ok {
+		user.Name = samlMap[source.UsernameAssertionKey]
+	}
+
+	// TODO: utilize groups once mapping is supported
+
+	return user, nil
+}
diff --git a/services/auth/source/saml/source_metadata.go b/services/auth/source/saml/source_metadata.go
new file mode 100644
index 0000000000..9fb8c758e3
--- /dev/null
+++ b/services/auth/source/saml/source_metadata.go
@@ -0,0 +1,32 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+import (
+	"encoding/xml"
+	"fmt"
+	"net/http"
+)
+
+// Metadata redirects request/response pair to authenticate against the provider
+func (source *Source) Metadata(request *http.Request, response http.ResponseWriter) error {
+	samlRWMutex.RLock()
+	defer samlRWMutex.RUnlock()
+	if _, ok := providers[source.authSource.Name]; !ok {
+		return fmt.Errorf("provider does not exist")
+	}
+
+	metadata, err := providers[source.authSource.Name].samlSP.Metadata()
+	if err != nil {
+		return err
+	}
+	buf, err := xml.Marshal(metadata)
+	if err != nil {
+		return err
+	}
+
+	response.Header().Set("Content-Type", "application/samlmetadata+xml; charset=utf-8")
+	_, _ = response.Write(buf)
+	return nil
+}
diff --git a/services/auth/source/saml/source_register.go b/services/auth/source/saml/source_register.go
new file mode 100644
index 0000000000..93eaaa88b6
--- /dev/null
+++ b/services/auth/source/saml/source_register.go
@@ -0,0 +1,23 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package saml
+
+// RegisterSource causes an OAuth2 configuration to be registered
+func (source *Source) RegisterSource() error {
+	samlRWMutex.Lock()
+	defer samlRWMutex.Unlock()
+	if err := source.initSAMLSp(); err != nil {
+		return err
+	}
+	providers[source.authSource.Name] = *source
+	return nil
+}
+
+// UnregisterSource causes an SAML configuration to be unregistered
+func (source *Source) UnregisterSource() error {
+	samlRWMutex.Lock()
+	defer samlRWMutex.Unlock()
+	delete(providers, source.authSource.Name)
+	return nil
+}
diff --git a/services/externalaccount/link.go b/services/externalaccount/link.go
index d6e2ea7e94..1f4c6728b8 100644
--- a/services/externalaccount/link.go
+++ b/services/externalaccount/link.go
@@ -7,9 +7,8 @@ import (
 	"context"
 	"fmt"
 
+	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
-
-	"github.com/markbates/goth"
 )
 
 // Store represents a thing that stores things
@@ -21,10 +20,12 @@ type Store interface {
 
 // LinkAccountFromStore links the provided user with a stored external user
 func LinkAccountFromStore(ctx context.Context, store Store, user *user_model.User) error {
-	gothUser := store.Get("linkAccountGothUser")
-	if gothUser == nil {
+	externalLinkUserInterface := store.Get("linkAccountUser")
+	if externalLinkUserInterface == nil {
 		return fmt.Errorf("not in LinkAccount session")
 	}
 
-	return LinkAccountToUser(ctx, user, gothUser.(goth.User))
+	externalLinkUser := externalLinkUserInterface.(auth.LinkAccountUser)
+
+	return LinkAccountToUser(ctx, user, externalLinkUser.GothUser, externalLinkUser.Type)
 }
diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go
index e2de41da18..fa85a65669 100644
--- a/services/externalaccount/user.go
+++ b/services/externalaccount/user.go
@@ -16,8 +16,8 @@ import (
 	"github.com/markbates/goth"
 )
 
-func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User) (*user_model.ExternalLoginUser, error) {
-	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
+func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User, authType auth.Type) (*user_model.ExternalLoginUser, error) {
+	authSource, err := auth.GetActiveAuthSourceByName(ctx, gothUser.Provider, authType)
 	if err != nil {
 		return nil, err
 	}
@@ -43,8 +43,8 @@ func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser go
 }
 
 // LinkAccountToUser link the gothUser to the user
-func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
-	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
+func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User, authType auth.Type) error {
+	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser, authType)
 	if err != nil {
 		return err
 	}
@@ -71,8 +71,8 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
 }
 
 // UpdateExternalUser updates external user's information
-func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
-	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
+func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User, authType auth.Type) error {
+	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser, authType)
 	if err != nil {
 		return err
 	}
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 25acbbb99e..85be38b403 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -1,3 +1,4 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
 // Copyright 2014 The Gogs Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
@@ -15,7 +16,7 @@ import (
 // AuthenticationForm form for authentication
 type AuthenticationForm struct {
 	ID                            int64
-	Type                          int    `binding:"Range(2,7)"`
+	Type                          int    `binding:"Range(2,9)"`
 	Name                          string `binding:"Required;MaxSize(30)"`
 	Host                          string
 	Port                          int
@@ -82,6 +83,18 @@ type AuthenticationForm struct {
 	SSPIDefaultLanguage           string
 	GroupTeamMap                  string `binding:"ValidGroupTeamMap"`
 	GroupTeamMapRemoval           bool
+
+	// SAML Settings
+	NameIDFormat                             int
+	IdentityProviderMetadata                 string
+	IdentityProviderMetadataURL              string
+	InsecureSkipAssertionSignatureValidation bool
+	ServiceProviderCertificate               string
+	ServiceProviderPrivateKey                string
+	EmailAssertionKey                        string
+	NameAssertionKey                         string
+	UsernameAssertionKey                     string
+	SAMLIconURL                              string
 }
 
 // Validate validates fields
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 25abefae00..2182d011e9 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -367,6 +367,69 @@
 					</div>
 				{{end}}
 
+				<!-- SAML -->
+				{{if .Source.IsSAML}}
+					{{$cfg:=.Source.Cfg}}
+					<div class="inline required field">
+						<label>{{ctx.Locale.Tr "admin.auths.saml_nameidformat"}}</label>
+						<div class="ui selection type dropdown">
+							<input type="hidden" id="name_id_format" name="name_id_format" value="{{$cfg.NameIDFormat}}">
+							<div class="text">{{.CurrentNameIDFormat}}</div>
+							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+							<div class="menu">
+								{{range .NameIDFormats}}
+									<div class="item" data-value="{{.Type.Int}}">{{.Name}}</div>
+								{{end}}
+							</div>
+						</div>
+					</div>
+
+					<div class="optional field">
+						<label for="saml_icon_url">{{ctx.Locale.Tr "admin.auths.saml_icon_url"}}</label>
+						<input id="saml_icon_url" name="saml_icon_url" value="{{$cfg.IconURL}}">
+					</div>
+
+					<div class="field">
+						<label for="identity_provider_metadata_url">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata_url"}}</label>
+						<input id="identity_provider_metadata_url" name="identity_provider_metadata_url" value="{{$cfg.IdentityProviderMetadataURL}}">
+					</div>
+					<div class="field">
+						<label for="identity_provider_metadata">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata"}}</label>
+						<textarea rows=2 id="identity_provider_metadata" name="identity_provider_metadata">{{$cfg.IdentityProviderMetadata}}</textarea>
+					</div>
+
+					<div class="inline field">
+						<div class="ui checkbox">
+							<label><strong>{{ctx.Locale.Tr "admin.auths.saml_insecure_skip_assertion_signature_validation"}}</strong></label>
+							<input name="insecure_skip_assertion_signature_validation" type="checkbox" {{if $cfg.InsecureSkipAssertionSignatureValidation}}checked{{end}}>
+						</div>
+					</div>
+
+					<div class=" field">
+						<label for="service_provider_certificate">{{ctx.Locale.Tr "admin.auths.saml_service_provider_certificate"}}</label>
+						<textarea rows=2 id="service_provider_certificate" name="service_provider_certificate">{{$cfg.ServiceProviderCertificate}}</textarea>
+					</div>
+					<div class=" field">
+						<label for="service_provider_private_key">{{ctx.Locale.Tr "admin.auths.saml_service_provider_private_key"}}</label>
+						<textarea rows=2 id="service_provider_private_key" name="service_provider_private_key">{{$cfg.ServiceProviderPrivateKey}}</textarea>
+					</div>
+
+					<div class="field">
+						<label for="email_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_email_assertion_key"}}</label>
+						<input id="email_assertion_key" name="email_assertion_key" value="{{if not $cfg.EmailAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress{{else}}{{$cfg.EmailAssertionKey}}{{end}}">
+					</div>
+
+					<div class="field">
+						<label for="name_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_name_assertion_key"}}</label>
+						<input id="name_assertion_key" name="name_assertion_key" value="{{if not $cfg.NameAssertionKey}}http://schemas.xmlsoap.org/claims/CommonName{{else}}{{$cfg.NameAssertionKey}}{{end}}">
+					</div>
+
+					<div class="field">
+						<label for="username_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_username_assertion_key"}}</label>
+						<input id="username_assertion_key" name="username_assertion_key" value="{{if not $cfg.UsernameAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name{{else}}{{$cfg.UsernameAssertionKey}}{{end}}">
+					</div>
+				{{end}}
+
 				<!-- SSPI -->
 				{{if .Source.IsSSPI}}
 					{{$cfg:=.Source.Cfg}}
@@ -441,6 +504,9 @@
 			<h5>GMail Settings:</h5>
 			<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
 
+			<h5>SAML Settings:</h5>
+			<p>{{ctx.Locale.Tr "admin.auths.tips.saml"}}</p>
+
 			<h5 class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general"}}:</h5>
 			<p class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general.tip"}} <b id="oauth2-callback-url"></b></p>
 		</div>
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index f32f77d5dc..665b0e3086 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -53,6 +53,9 @@
 				<!-- SSPI -->
 				{{template "admin/auth/source/sspi" .}}
 
+				<!-- SAML -->
+				{{template "admin/auth/source/saml" .}}
+
 				<div class="ldap field">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "admin.auths.attributes_in_bind"}}</strong></label>
@@ -85,6 +88,9 @@
 			<h5>GMail Settings:</h5>
 			<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
 
+			<h5>SAML Settings:</h5>
+			<p>{{ctx.Locale.Tr "admin.auths.tips.saml"}}</p>
+
 			<h5 class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general"}}:</h5>
 			<p class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general.tip"}} <b id="oauth2-callback-url"></b></p>
 
diff --git a/templates/admin/auth/source/saml.tmpl b/templates/admin/auth/source/saml.tmpl
new file mode 100644
index 0000000000..050e22ddcc
--- /dev/null
+++ b/templates/admin/auth/source/saml.tmpl
@@ -0,0 +1,62 @@
+<div class="saml field {{if not (eq .type 8)}}gt-hidden{{end}}">
+
+	<div class="inline required field">
+		<label>{{ctx.Locale.Tr "admin.auths.saml_nameidformat"}}</label>
+		<div class="ui selection type dropdown">
+			<input type="hidden" id="name_id_format" name="name_id_format" value="{{.name_id_format}}">
+			<div class="text">{{.CurrentNameIDFormat}}</div>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="menu">
+				{{range .NameIDFormats}}
+					<div class="item" data-value="{{.Type.Int}}">{{.Name}}</div>
+				{{end}}
+			</div>
+		</div>
+	</div>
+
+	<div class="optional field">
+		<label for="saml_icon_url">{{ctx.Locale.Tr "admin.auths.saml_icon_url"}}</label>
+		<input id="saml_icon_url" name="saml_icon_url" value="{{.SAMLIconURL}}">
+	</div>
+
+	<div class="field">
+		<label for="identity_provider_metadata_url">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata_url"}}</label>
+		<input id="identity_provider_metadata_url" name="identity_provider_metadata_url" value="{{.IdentityProviderMetadataURL}}">
+	</div>
+	<div class="field">
+		<label for="identity_provider_metadata">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata"}}</label>
+		<textarea rows=2 id="identity_provider_metadata" name="identity_provider_metadata" value="{{.IdentityProviderMetadata}}"></textarea>
+	</div>
+
+	<div class="inline field">
+		<div class="ui checkbox">
+			<label><strong>{{ctx.Locale.Tr "admin.auths.saml_insecure_skip_assertion_signature_validation"}}</strong></label>
+			<input name="insecure_skip_assertion_signature_validation" type="checkbox" {{if .InsecureSkipAssertionSignatureValidation}}checked{{end}}>
+		</div>
+	</div>
+
+	<div class="field">
+		<label for="service_provider_certificate">{{ctx.Locale.Tr "admin.auths.saml_service_provider_certificate"}}</label>
+		<textarea rows=2 id="service_provider_certificate" name="service_provider_certificate" value="{{.ServiceProviderCertificate}}"></textarea>
+	</div>
+	<div class="field">
+		<label for="service_provider_private_key">{{ctx.Locale.Tr "admin.auths.saml_service_provider_private_key"}}</label>
+		<textarea rows=2 id="service_provider_private_key" name="service_provider_private_key" value="{{.ServiceProviderPrivateKey}}"></textarea>
+	</div>
+
+	<div class="field">
+		<label for="email_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_email_assertion_key"}}</label>
+		<input id="email_assertion_key" name="email_assertion_key" value="{{if not .EmailAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress{{else}}{{.EmailAssertionKey}}{{end}}">
+	</div>
+
+	<div class="field">
+		<label for="name_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_name_assertion_key"}}</label>
+		<input id="name_assertion_key" name="name_assertion_key" value="{{if not .NameAssertionKey}}http://schemas.xmlsoap.org/claims/CommonName{{else}}{{.NameAssertionKey}}{{end}}">
+	</div>
+
+	<div class="field">
+		<label for="username_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_username_assertion_key"}}</label>
+		<input id="username_assertion_key" name="username_assertion_key" value="{{if not .UsernameAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name{{else}}{{.UsernameAssertionKey}}{{end}}">
+	</div>
+
+</div>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 0d0064b02a..1b4e2b25f9 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -69,5 +69,22 @@
 		</div>
 	</div>
 	{{end}}
+	{{if .SAMLProviders}}
+	<div class="divider divider-text">
+		{{.locale.Tr "sign_in_or"}}
+	</div>
+	<div id="saml-login-navigator" class="gt-py-2">
+		<div class="gt-df gt-fc gt-jc">
+			<div id="saml-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
+				{{range $provider := .SAMLProviders}}
+					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 saml-login-link" href="{{AppSubUrl}}/user/saml/{{$provider.Name}}">
+						{{.IconHTML 28}}
+						{{ctx.Locale.Tr "sign_in_with_provider" $provider.Name}}
+					</a>
+				{{end}}
+			</div>
+		</div>
+	</div>
+	{{end}}
 	</form>
 </div>
diff --git a/tests/integration/README.md b/tests/integration/README.md
index f6f74ca21f..c691483511 100644
--- a/tests/integration/README.md
+++ b/tests/integration/README.md
@@ -110,3 +110,20 @@ SLOW_FLUSH = 5S ; 5s is the default value
 ```bash
 GITEA_SLOW_TEST_TIME="10s" GITEA_SLOW_FLUSH_TIME="5s" make test-sqlite
 ```
+
+## Running SimpleSAML for testing SAML locally
+
+```shell
+docker run \
+-p 8080:8080 \
+-p 8443:8443 \
+-e SIMPLESAMLPHP_SP_ENTITY_ID=http://localhost:3003/user/saml/test-sp/metadata \
+-e SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://localhost:3003/user/saml/test-sp/acs \
+-e SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE=http://localhost:3003/user/saml/test-sp/acs \
+--add-host=localhost:192.168.65.2 \
+-d allspice/simple-saml
+```
+
+```shell
+TEST_SIMPLESAML_URL=localhost:8080 make test-sqlite#TestSAMLRegistration
+```
diff --git a/tests/integration/saml_test.go b/tests/integration/saml_test.go
new file mode 100644
index 0000000000..585fd35c5f
--- /dev/null
+++ b/tests/integration/saml_test.go
@@ -0,0 +1,150 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/cookiejar"
+	"net/url"
+	"os"
+	"regexp"
+	"strings"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+	"code.gitea.io/gitea/services/auth/source/saml"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSAMLRegistration(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	samlURL := "localhost:8080"
+
+	if os.Getenv("CI") == "" || !setting.Database.Type.IsPostgreSQL() {
+		// Make it possible to run tests against a local simplesaml instance
+		samlURL = os.Getenv("TEST_SIMPLESAML_URL")
+		if samlURL == "" {
+			t.Skip("TEST_SIMPLESAML_URL not set and not running in CI")
+			return
+		}
+	}
+
+	privateKey, cert, err := saml.GenerateSAMLSPKeypair()
+	assert.NoError(t, err)
+
+	// verify that the keypair can be parsed
+	keyPair, err := tls.X509KeyPair([]byte(cert), []byte(privateKey))
+	assert.NoError(t, err)
+	keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
+	assert.NoError(t, err)
+
+	assert.NoError(t, auth.CreateSource(db.DefaultContext, &auth.Source{
+		Type:          auth.SAML,
+		Name:          "test-sp",
+		IsActive:      true,
+		IsSyncEnabled: false,
+		Cfg: &saml.Source{
+			IdentityProviderMetadata:                 "",
+			IdentityProviderMetadataURL:              fmt.Sprintf("http://%s/simplesaml/saml2/idp/metadata.php", samlURL),
+			InsecureSkipAssertionSignatureValidation: false,
+			NameIDFormat:                             4,
+			ServiceProviderCertificate:               "", // SimpleSAMLPhp requires that the SP certificate be specified in the server configuration rather than SP metadata
+			ServiceProviderPrivateKey:                "",
+			EmailAssertionKey:                        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+			NameAssertionKey:                         "http://schemas.xmlsoap.org/claims/CommonName",
+			UsernameAssertionKey:                     "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
+			IconURL:                                  "",
+		},
+	}))
+
+	// check the saml metadata url
+	req := NewRequest(t, "GET", "/user/saml/test-sp/metadata")
+	MakeRequest(t, req, http.StatusOK)
+
+	req = NewRequest(t, "GET", "/user/saml/test-sp")
+	resp := MakeRequest(t, req, http.StatusTemporaryRedirect)
+
+	jar, err := cookiejar.New(nil)
+	assert.NoError(t, err)
+
+	client := http.Client{
+		Timeout: 30 * time.Second,
+		Jar:     jar,
+	}
+
+	httpReq, err := http.NewRequest("GET", test.RedirectURL(resp), nil)
+	assert.NoError(t, err)
+
+	var formRedirectURL *url.URL
+	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+		// capture the redirected destination to use in POST request
+		formRedirectURL = req.URL
+		return nil
+	}
+
+	res, err := client.Do(httpReq)
+	client.CheckRedirect = nil
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, res.StatusCode)
+	assert.NotNil(t, formRedirectURL)
+
+	form := url.Values{
+		"username": {"user1"},
+		"password": {"user1pass"},
+	}
+
+	httpReq, err = http.NewRequest("POST", formRedirectURL.String(), strings.NewReader(form.Encode()))
+	assert.NoError(t, err)
+	httpReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+	res, err = client.Do(httpReq)
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, res.StatusCode)
+
+	body, err := io.ReadAll(res.Body)
+	assert.NoError(t, err)
+
+	samlResMatcher := regexp.MustCompile(`<input.*?name="SAMLResponse".*?value="([^"]+)".*?>`)
+	matches := samlResMatcher.FindStringSubmatch(string(body))
+	assert.Len(t, matches, 2)
+	assert.NoError(t, res.Body.Close())
+
+	session := emptyTestSession(t)
+
+	req = NewRequestWithValues(t, "POST", "/user/saml/test-sp/acs", map[string]string{
+		"SAMLResponse": matches[1],
+	})
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, test.RedirectURL(resp), "/user/link_account")
+
+	csrf := GetCSRF(t, session, test.RedirectURL(resp))
+
+	// link the account
+	req = NewRequestWithValues(t, "POST", "/user/link_account_signup", map[string]string{
+		"_csrf":     csrf,
+		"user_name": "samluser",
+		"email":     "saml@example.com",
+	})
+
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, test.RedirectURL(resp), "/")
+
+	// verify that the user was created
+	u, err := user_model.GetUserByEmail(db.DefaultContext, "saml@example.com")
+	assert.NoError(t, err)
+	assert.NotNil(t, u)
+	assert.Equal(t, "samluser", u.Name)
+}
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 044976ea7b..4804163971 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -103,9 +103,9 @@ export function initAdminCommon() {
   // New authentication
   if ($('.admin.new.authentication').length > 0) {
     $('#auth_type').on('change', function () {
-      hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi'));
+      hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi, .saml'));
 
-      $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]').removeAttr('required');
+      $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required], .saml input[required]').removeAttr('required');
       $('.binddnrequired').removeClass('required');
 
       const authType = $(this).val();
@@ -137,6 +137,10 @@ export function initAdminCommon() {
           showElem($('.sspi'));
           $('.sspi div.required input').attr('required', 'required');
           break;
+        case '8': // SAML
+          showElem($('.saml'));
+          $('.saml div.required input').attr('required', 'required');
+          break;
       }
       if (authType === '2' || authType === '5') {
         onSecurityProtocolChange();
diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js
index 60d186e699..3bf84e31df 100644
--- a/web_src/js/features/user-auth.js
+++ b/web_src/js/features/user-auth.js
@@ -20,3 +20,24 @@ export function initUserAuthOauth2() {
     });
   }
 }
+
+export function initUserAuthSAML() {
+  const outer = document.getElementById('saml-login-navigator');
+  if (!outer) return;
+  const inner = document.getElementById('saml-login-navigator-inner');
+
+  checkAppUrl();
+
+  for (const link of outer.querySelectorAll('.saml-login-link')) {
+    link.addEventListener('click', () => {
+      inner.classList.add('gt-invisible');
+      outer.classList.add('is-loading');
+      setTimeout(() => {
+        // recover previous content to let user try again
+        // usually redirection will be performed before this action
+        outer.classList.remove('is-loading');
+        inner.classList.remove('gt-invisible');
+      }, 5000);
+    });
+  }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 117279c3c4..ddd435f05e 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -23,7 +23,10 @@ import {initFindFileInRepo} from './features/repo-findfile.js';
 import {initCommentContent, initMarkupContent} from './markup/content.js';
 import {initPdfViewer} from './render/pdf.js';
 
-import {initUserAuthOauth2} from './features/user-auth.js';
+import {
+  initUserAuthOauth2,
+  initUserAuthSAML
+} from './features/user-auth.js';
 import {
   initRepoIssueDue,
   initRepoIssueReferenceRepositorySearch,
@@ -179,6 +182,7 @@ onDomReady(() => {
   initCaptcha();
 
   initUserAuthOauth2();
+  initUserAuthSAML();
   initUserAuthWebAuthn();
   initUserAuthWebAuthnRegister();
   initUserSettings();

From e0445042410da1fb3e5b44edebd0f5d2b7ae06b2 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Fri, 23 Feb 2024 01:24:07 +0100
Subject: [PATCH 131/679] Frontport changelogs of minor releases (#29337)

as title
---
 CHANGELOG.md | 236 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 236 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae87638f1c..9f2c69888a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,242 @@ This changelog goes through all the changes that have been made in each release
 without substantial changes to our git log; to see the highlights of what has
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.21.6](https://github.com/go-gitea/gitea/releases/tag/v1.21.6) - 2024-02-22
+
+* SECURITY
+  * Fix XSS vulnerabilities (#29336)
+  * Use general token signing secret (#29205) (#29325)
+* API
+  * Refactor issue template parsing and fix API endpoint (#29069) (#29140)
+  * Fix swift packages not resolving (#29095) (#29102)
+* ENHANCEMENTS
+  * Refactor git version functions and check compatibility (#29155) (#29157)
+  * Improve user experience for outdated comments (#29050) (#29086)
+  * Hide code links on release page if user cannot read code (#29064) (#29066)
+  * Wrap contained tags and branches again (#29021) (#29026)
+  * Fix incorrect button CSS usages (#29015) (#29023)
+  * Strip trailing newline in markdown code copy (#29019) (#29022)
+* BUGFIXES
+  * Remove SSH workaround (#27893) (#29332)
+  * Only log error when tag sync fails (#29295) (#29327)
+  * Fix SSPI user creation (#28948) (#29323)
+  * Improve the `issue_comment` workflow trigger event (#29277) (#29322)
+  * Discard unread data of `git cat-file` (#29297) (#29310)
+  * Fix error display when merging PRs (#29288) (#29309)
+  * Prevent double use of `git cat-file` session. (#29298) (#29301)
+  * Fix missing link on outgoing new release notifications (#29079) (#29300)
+  * Fix debian InRelease Acquire-By-Hash newline (#29204) (#29299)
+  * Always write proc-receive hook for all git versions (#29287) (#29291)
+  * Do not show delete button when time tracker is disabled (#29257) (#29279)
+  * Workaround to clean up old reviews on creating a new one (#28554) (#29264)
+  * Fix bug when the linked account was disactived and list the linked accounts (#29263)
+  * Do not use lower tag names to find releases/tags (#29261) (#29262)
+  * Fix missed edit issues event for actions (#29237) (#29251)
+  * Only delete scheduled workflows when needed (#29091) (#29235)
+  * Make submit event code work with both jQuery event and native event (#29223) (#29234)
+  * Fix push to create with capitalize repo name (#29090) (#29206)
+  * Use ghost user if user was not found (#29161) (#29169)
+  * Dont load Review if Comment is CommentTypeReviewRequest (#28551) (#29160)
+  * Refactor parseSignatureFromCommitLine (#29054) (#29108)
+  * Avoid showing unnecessary JS errors when there are elements with different origin on the page (#29081) (#29089)
+  * Fix gitea-origin-url with default ports (#29085) (#29088)
+  * Fix orgmode link resolving (#29024) (#29076)
+  * Fix: Elasticsearch: Request Entity Too Large #28117 (#29062) (#29075)
+  * Do not render empty comments (#29039) (#29049)
+  * Avoid sending update/delete release notice when it is draft (#29008) (#29025)
+* DOCS
+  * Rm outdated docs from some languages (#27530) (#29208)
+* MISC
+  * Implement some action notifier functions (#29173) (#29308)
+  * Fix gitea-action user avatar broken on edited menu (#29190) (#29307)
+  * Disallow merge when required checked are missing (#29143) (#29268)
+  * Convert visibility to number (#29226) (#29244)
+  * Load outdated comments when (un)resolving conversation on PR timeline (#29203) (#29221)
+  * Fix incorrect link to swift doc and swift package-registry login command (#29096) (#29103)
+  * Fix typos in the documentation (#29048) (#29056)
+  * Explained where create issue/PR template (#29035)
+
+## [1.21.5](https://github.com/go-gitea/gitea/releases/tag/v1.21.5) - 2024-01-31
+
+* SECURITY
+  * Prevent anonymous container access if `RequireSignInView` is enabled (#28877) (#28882)
+  * Update go dependencies and fix go-git (#28893) (#28934)
+* BUGFIXES
+  * Revert "Speed up loading the dashboard on mysql/mariadb (#28546)" (#29006) (#29007)
+  * Fix an actions schedule bug (#28942) (#28999)
+  * Fix update enable_prune even if mirror_interval is not provided (#28905) (#28929)
+  * Fix uploaded artifacts should be overwritten (#28726) backport v1.21 (#28832)
+  * Preserve BOM in web editor (#28935) (#28959)
+  * Strip `/` from relative links (#28932) (#28952)
+  * Don't remove all mirror repository's releases when mirroring (#28817) (#28939)
+  * Implement `MigrateRepository` for the actions notifier (#28920) (#28923)
+  * Respect branch info for relative links (#28909) (#28922)
+  * Don't reload timeline page when (un)resolving or replying conversation (#28654) (#28917)
+  * Only migrate the first 255 chars of a Github issue title (#28902) (#28912)
+  * Fix sort bug on repository issues list (#28897) (#28901)
+  * Fix `DeleteCollaboration` transaction behaviour (#28886) (#28889)
+  * Fix schedule not trigger bug because matching full ref name with short ref name (#28874) (#28888)
+  * Fix migrate storage bug (#28830) (#28867)
+  * Fix archive creating LFS hooks and breaking pull requests (#28848) (#28851)
+  * Fix reverting a merge commit failing (#28794) (#28825)
+  * Upgrade xorm to v1.3.7 to fix a resource leak problem caused by Iterate (#28891) (#28895)
+  * Fix incorrect PostgreSQL connection string for Unix sockets (#28865) (#28870)
+* ENHANCEMENTS
+  * Make loading animation less aggressive (#28955) (#28956)
+  * Avoid duplicate JS error messages on UI (#28873) (#28881)
+  * Bump `@github/relative-time-element` to 4.3.1 (#28819) (#28826)
+* MISC
+  * Warn that `DISABLE_QUERY_AUTH_TOKEN` is false only if it's explicitly defined (#28783) (#28868)
+  * Remove duplicated checkinit on git module (#28824) (#28831)
+
+## [1.21.4](https://github.com/go-gitea/gitea/releases/tag/v1.21.4) - 2024-01-16
+
+* SECURITY
+  * Update github.com/cloudflare/circl (#28789) (#28790)
+  * Require token for GET subscription endpoint (#28765) (#28768)
+* BUGFIXES
+  * Use refname:strip-2 instead of refname:short when syncing tags (#28797) (#28811)
+  * Fix links in issue card (#28806) (#28807)
+  * Fix nil pointer panic when exec some gitea cli command (#28791) (#28795)
+  * Require token for GET subscription endpoint (#28765) (#28778)
+  * Fix button size in "attached header right" (#28770) (#28774)
+  * Fix `convert.ToTeams` on empty input (#28426) (#28767)
+  * Hide code related setting options in repository when code unit is disabled (#28631) (#28749)
+  * Fix incorrect URL for "Reference in New Issue" (#28716) (#28723)
+  * Fix panic when parsing empty pgsql host (#28708) (#28709)
+  * Upgrade xorm to new version which supported update join for all supported databases (#28590) (#28668)
+  * Fix alpine package files are not rebuilt (#28638) (#28665)
+  * Avoid cycle-redirecting user/login page (#28636) (#28658)
+  * Fix empty ref for cron workflow runs (#28640) (#28647)
+  * Remove unnecessary syncbranchToDB with tests (#28624) (#28629)
+  * Use known issue IID to generate new PR index number when migrating from GitLab (#28616) (#28618)
+  * Fix flex container width (#28603) (#28605)
+  * Fix the scroll behavior for emoji/mention list (#28597) (#28601)
+  * Fix wrong due date rendering in issue list page (#28588) (#28591)
+  * Fix `status_check_contexts` matching bug (#28582) (#28589)
+  * Fix 500 error of searching commits (#28576) (#28579)
+  * Use information from previous blame parts (#28572) (#28577)
+  * Update mermaid for 1.21 (#28571)
+  * Fix 405 method not allowed CORS / OIDC (#28583) (#28586) (#28587) (#28611)
+  * Fix `GetCommitStatuses` (#28787) (#28804)
+  * Forbid removing the last admin user (#28337) (#28793)
+  * Fix schedule tasks bugs (#28691) (#28780)
+  * Fix issue dependencies (#27736) (#28776)
+  * Fix system webhooks API bug (#28531) (#28666)
+  * Fix when private user following user, private user will not be counted in his own view (#28037) (#28792)
+  * Render code block in activity tab (#28816) (#28818)
+* ENHANCEMENTS
+  * Rework markup link rendering (#26745) (#28803)
+  * Modernize merge button (#28140) (#28786)
+  * Speed up loading the dashboard on mysql/mariadb (#28546) (#28784)
+  * Assign pull request to project during creation (#28227) (#28775)
+  * Show description as tooltip instead of title for labels (#28754) (#28766)
+  * Make template `DateTime` show proper tooltip (#28677) (#28683)
+  * Switch destination directory for apt signing keys (#28639) (#28642)
+  * Include heap pprof in diagnosis report to help debugging memory leaks (#28596) (#28599)
+* DOCS
+  * Suggest to use Type=simple for systemd service (#28717) (#28722)
+  * Extend description for ARTIFACT_RETENTION_DAYS (#28626) (#28630)
+* MISC
+  * Add -F to commit search to treat keywords as strings (#28744) (#28748)
+  * Add download attribute to release attachments (#28739) (#28740)
+  * Concatenate error in `checkIfPRContentChanged` (#28731) (#28737)
+  * Improve 1.21 document for Database Preparation (#28643) (#28644)
+
+## [1.21.3](https://github.com/go-gitea/gitea/releases/tag/v1.21.3) - 2023-12-21
+
+* SECURITY
+  * Update golang.org/x/crypto (#28519)
+* API
+  * chore(api): support ignore password if login source type is LDAP for creating user API (#28491) (#28525)
+  * Add endpoint for not implemented Docker auth (#28457) (#28462)
+* ENHANCEMENTS
+  * Add option to disable ambiguous unicode characters detection (#28454) (#28499)
+  * Refactor SSH clone URL generation code (#28421) (#28480)
+  * Polyfill SubmitEvent for PaleMoon (#28441) (#28478)
+* BUGFIXES
+  * Fix the issue ref rendering for wiki (#28556) (#28559)
+  * Fix duplicate ID when deleting repo (#28520) (#28528)
+  * Only check online runner when detecting matching runners in workflows (#28286) (#28512)
+  * Initalize stroage for orphaned repository doctor (#28487) (#28490)
+  * Fix possible nil pointer access (#28428) (#28440)
+  * Don't show unnecessary citation JS error on UI (#28433) (#28437)
+* DOCS
+  * Update actions document about comparsion as Github Actions (#28560) (#28564)
+  * Fix documents for "custom/public/assets/" (#28465) (#28467)
+* MISC
+  * Fix inperformant query on retrifing review from database. (#28552) (#28562)
+  * Improve the prompt for "ssh-keygen sign" (#28509) (#28510)
+  * Update docs for DISABLE_QUERY_AUTH_TOKEN (#28485) (#28488)
+  * Fix Chinese translation of config cheat sheet[API] (#28472) (#28473)
+  * Retry SSH key verification with additional CRLF if it failed (#28392) (#28464)
+
+## [1.21.2](https://github.com/go-gitea/gitea/releases/tag/1.21.2) - 2023-12-12
+
+* SECURITY
+  * Rebuild with recently released golang version
+  * Fix missing check (#28406) (#28411)
+  * Do some missing checks (#28423) (#28432)
+* BUGFIXES
+  * Fix margin in server signed signature verification view (#28379) (#28381)
+  * Fix object does not exist error when checking citation file (#28314) (#28369)
+  * Use `filepath` instead of `path` to create SQLite3 database file (#28374) (#28378)
+  * Fix the runs will not be displayed bug when the main branch have no workflows but other branches have (#28359) (#28365)
+  * Handle repository.size column being NULL in migration v263 (#28336) (#28363)
+  * Convert git commit summary to valid UTF8. (#28356) (#28358)
+  * Fix migration panic due to an empty review comment diff (#28334) (#28362)
+  * Add `HEAD` support for rpm repo files (#28309) (#28360)
+  * Fix RPM/Debian signature key creation (#28352) (#28353)
+  * Keep profile tab when clicking on Language (#28320) (#28331)
+  * Fix missing issue search index update when changing status (#28325) (#28330)
+  * Fix wrong link in `protect_branch_name_pattern_desc` (#28313) (#28315)
+  * Read `previous` info from git blame (#28306) (#28310)
+  * Ignore "non-existing" errors when getDirectorySize calculates the size (#28276) (#28285)
+  * Use appSubUrl for OAuth2 callback URL tip (#28266) (#28275)
+  * Meilisearch: require all query terms to be matched (#28293) (#28296)
+  * Fix required error for token name (#28267) (#28284)
+  * Fix issue will be detected as pull request when checking `First-time contributor` (#28237) (#28271)
+  * Use full width for project boards (#28225) (#28245)
+  * Increase "version" when update the setting value to a same value as before (#28243) (#28244)
+  * Also sync DB branches on push if necessary (#28361) (#28403)
+  * Make gogit Repository.GetBranchNames consistent (#28348) (#28386)
+  * Recover from panic in cron task (#28409) (#28425)
+  * Deprecate query string auth tokens (#28390) (#28430)
+* ENHANCEMENTS
+  * Improve doctor cli behavior (#28422) (#28424)
+  * Fix margin in server signed signature verification view (#28379) (#28381)
+  * Refactor template empty checks (#28351) (#28354)
+  * Read `previous` info from git blame (#28306) (#28310)
+  * Use full width for project boards (#28225) (#28245)
+  * Enable system users search via the API (#28013) (#28018)
+
+## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/1.21.1) - 2023-11-26
+
+* SECURITY
+  * Fix comment permissions (#28213) (#28216)
+* BUGFIXES
+  * Fix delete-orphaned-repos (#28200) (#28202)
+  * Make CORS work for oauth2 handlers (#28184) (#28185)
+  * Fix missing buttons (#28179) (#28181)
+  * Fix no ActionTaskOutput table waring (#28149) (#28152)
+  * Fix empty action run title (#28113) (#28148)
+  * Use "is-loading" to avoid duplicate form submit for code comment (#28143) (#28147)
+  * Fix Matrix and MSTeams nil dereference (#28089) (#28105)
+  * Fix incorrect pgsql conn builder behavior (#28085) (#28098)
+  * Fix system config cache expiration timing (#28072) (#28090)
+  * Restricted users only see repos in orgs which their team was assigned to (#28025) (#28051)
+* API
+  * Fix permissions for Token DELETE endpoint to match GET and POST (#27610) (#28099)
+* ENHANCEMENTS
+  * Do not display search box when there's no packages yet (#28146) (#28159)
+  * Add missing `packages.cleanup.success` (#28129) (#28132)
+* DOCS
+  * Docs: Replace deprecated IS_TLS_ENABLED mailer setting in email setup (#28205) (#28208)
+  * Fix the description about the default setting for action in quick start document (#28160) (#28168)
+  * Add guide page to actions when there's no workflows (#28145) (#28153)
+* MISC
+  * Use full width for PR comparison (#28182) (#28186)
+
 ## [1.21.0](https://github.com/go-gitea/gitea/releases/tag/v1.21.0) - 2023-11-14
 
 * BREAKING

From 532e422027c88a4a3dc0c2968857f8d5f94d861f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Nicas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Fri, 23 Feb 2024 01:24:57 +0100
Subject: [PATCH 132/679] Unify organizations header (#29248)

Unify organizations header

before:

![image](https://github.com/go-gitea/gitea/assets/72873130/74474e0d-33c3-4bbf-9324-d130ea2c62f8)

after:

![image](https://github.com/go-gitea/gitea/assets/72873130/1c65de0d-fa0f-4b17-ab8d-067de8c7113b)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 modules/context/org.go                        | 15 +++++
 routers/web/org/home.go                       | 18 ------
 routers/web/shared/user/header.go             | 15 -----
 routers/web/user/profile.go                   |  1 +
 templates/org/header.tmpl                     | 42 +++++++++-----
 templates/org/home.tmpl                       | 33 +----------
 templates/org/member/members.tmpl             |  2 +-
 templates/org/menu.tmpl                       | 10 ++--
 templates/org/projects/list.tmpl              |  7 +--
 templates/package/settings.tmpl               | 12 +++-
 templates/user/code.tmpl                      |  5 +-
 templates/user/overview/header.tmpl           | 57 ++++++-------------
 templates/user/overview/package_versions.tmpl |  7 +--
 templates/user/overview/packages.tmpl         |  7 +--
 web_src/css/org.css                           | 18 +++---
 15 files changed, 95 insertions(+), 154 deletions(-)

diff --git a/modules/context/org.go b/modules/context/org.go
index d068646577..018b76de43 100644
--- a/modules/context/org.go
+++ b/modules/context/org.go
@@ -11,6 +11,8 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 )
@@ -255,6 +257,19 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
 	ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
 	ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
 	ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
+
+	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+	if len(ctx.ContextUser.Description) != 0 {
+		content, err := markdown.RenderString(&markup.RenderContext{
+			Metas: map[string]string{"mode": "document"},
+			Ctx:   ctx,
+		}, ctx.ContextUser.Description)
+		if err != nil {
+			ctx.ServerError("RenderString", err)
+			return
+		}
+		ctx.Data["RenderedDescription"] = content
+	}
 }
 
 // OrgAssignment returns a middleware to handle organization assignment
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 8bf02b2c42..36f543dc45 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -11,7 +11,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
@@ -46,17 +45,6 @@ func Home(ctx *context.Context) {
 
 	ctx.Data["PageIsUserProfile"] = true
 	ctx.Data["Title"] = org.DisplayName()
-	if len(org.Description) != 0 {
-		desc, err := markdown.RenderString(&markup.RenderContext{
-			Ctx:   ctx,
-			Metas: map[string]string{"mode": "document"},
-		}, org.Description)
-		if err != nil {
-			ctx.ServerError("RenderString", err)
-			return
-		}
-		ctx.Data["RenderedDescription"] = desc
-	}
 
 	var orderBy db.SearchOrderBy
 	ctx.Data["SortType"] = ctx.FormString("sort")
@@ -131,18 +119,12 @@ func Home(ctx *context.Context) {
 		return
 	}
 
-	var isFollowing bool
-	if ctx.Doer != nil {
-		isFollowing = user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
-	}
-
 	ctx.Data["Repos"] = repos
 	ctx.Data["Total"] = count
 	ctx.Data["Members"] = members
 	ctx.Data["Teams"] = ctx.Org.Teams
 	ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
 	ctx.Data["PageIsViewRepositories"] = true
-	ctx.Data["IsFollowing"] = isFollowing
 
 	err = shared_user.LoadHeaderCount(ctx)
 	if err != nil {
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index a6c66a2c70..99b701b439 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -17,8 +17,6 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/markup"
-	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
@@ -36,7 +34,6 @@ func prepareContextForCommonProfile(ctx *context.Context) {
 func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 	prepareContextForCommonProfile(ctx)
 
-	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
 	ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
 
@@ -48,18 +45,6 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 	}
 	ctx.Data["OpenIDs"] = openIDs
 
-	if len(ctx.ContextUser.Description) != 0 {
-		content, err := markdown.RenderString(&markup.RenderContext{
-			Metas: map[string]string{"mode": "document"},
-			Ctx:   ctx,
-		}, ctx.ContextUser.Description)
-		if err != nil {
-			ctx.ServerError("RenderString", err)
-			return
-		}
-		ctx.Data["RenderedDescription"] = content
-	}
-
 	showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
 	orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
 		UserID:         ctx.ContextUser.ID,
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 37ce450530..4d0ad06cba 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -324,6 +324,7 @@ func Action(ctx *context.Context) {
 		ctx.HTML(http.StatusOK, tplProfileBigAvatar)
 		return
 	} else if ctx.ContextUser.IsOrganization() {
+		ctx.Data["Org"] = ctx.ContextUser
 		ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 		ctx.HTML(http.StatusOK, tplFollowUnfollow)
 		return
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 7b912c1c56..8423fd7d3b 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -1,18 +1,32 @@
-{{with .Org}}
-	<div class="ui container">
-		<div class="ui vertically grid head">
-			<div class="column">
-				<div class="ui header gt-df gt-ac gt-word-break">
-					{{ctx.AvatarUtils.Avatar . 100}}
-					<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
-					<span class="org-visibility">
-						{{if .Visibility.IsLimited}}<div class="ui medium basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</div>{{end}}
-						{{if .Visibility.IsPrivate}}<div class="ui medium basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</div>{{end}}
-					</span>
-				</div>
-			</div>
+<div class="ui container gt-df">
+	{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}}
+	<div id="org-info" class="gt-df gt-fc">
+		<div class="ui header">
+			{{.Org.DisplayName}}
+			<span class="org-visibility">
+				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
+				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
+			</span>
+			<span class="gt-df gt-ac gt-gap-2 gt-ml-auto gt-font-16 gt-whitespace-nowrap">
+				{{if .EnableFeed}}
+					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
+						{{svg "octicon-rss" 24}}
+					</a>
+				{{end}}
+				{{if .IsSigned}}
+					{{template "org/follow_unfollow" .}}
+				{{end}}
+			</span>
+		</div>
+		{{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription | Str2html}}</div>{{end}}
+		<div class="text light meta gt-mt-2">
+			{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
+			{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
+			{{if .IsSigned}}
+				{{if .Org.Email}}<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>{{end}}
+			{{end}}
 		</div>
 	</div>
-{{end}}
+</div>
 
 {{template "org/menu" .}}
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index 81a76d3b4d..4e33b1af72 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -1,37 +1,6 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content organization profile">
-	<div class="ui container gt-df">
-		{{ctx.AvatarUtils.Avatar .Org 140 "org-avatar"}}
-		<div id="org-info">
-			<div class="ui header">
-				{{.Org.DisplayName}}
-				<span class="org-visibility">
-					{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
-					{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
-				</span>
-			</div>
-			{{if $.RenderedDescription}}<div class="render-content markup">{{$.RenderedDescription|Str2html}}</div>{{end}}
-			<div class="text grey meta">
-				{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
-				{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
-				{{if $.IsSigned}}
-					{{if .Org.Email}}<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>{{end}}
-				{{end}}
-			</div>
-		</div>
-		<div class="right menu">
-			{{if .EnableFeed}}
-			<a class="ui basic label button gt-mr-0" href="{{$.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
-				{{svg "octicon-rss" 24}}
-			</a>
-			{{end}}
-			{{if .IsSigned}}
-				{{template "org/follow_unfollow" .}}
-			{{end}}
-		</div>
-	</div>
-
-	{{template "org/menu" .}}
+	{{template "org/header" .}}
 
 	<div class="ui container">
 		<div class="ui mobile reversed stackable grid">
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl
index 03509ec93e..64f1aaa7d2 100644
--- a/templates/org/member/members.tmpl
+++ b/templates/org/member/members.tmpl
@@ -1,5 +1,5 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content organization">
+<div role="main" aria-label="{{.Title}}" class="page-content organization members">
 	{{template "org/header" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index 8a97711ce2..f07b26865a 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -15,24 +15,24 @@
 		</a>
 		{{end}}
 		{{if and .IsPackageEnabled .CanReadPackages}}
-		<a class="item" href="{{$.Org.HomeLink}}/-/packages">
+		<a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages">
 			{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
 		</a>
 		{{end}}
 		{{if and .IsRepoIndexerEnabled .CanReadCode}}
-		<a class="item" href="{{$.Org.HomeLink}}/-/code">
-			{{svg "octicon-code"}}&nbsp;{{ctx.Locale.Tr "org.code"}}
+		<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
+			{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
 		</a>
 		{{end}}
 		{{if .NumMembers}}
 			<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
-				{{svg "octicon-person"}}&nbsp;{{ctx.Locale.Tr "org.members"}}
+				{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
 				<div class="ui small label">{{.NumMembers}}</div>
 			</a>
 		{{end}}
 		{{if .IsOrganizationMember}}
 			<a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams">
-				{{svg "octicon-people"}}&nbsp;{{ctx.Locale.Tr "org.teams"}}
+				{{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}}
 				{{if .NumTeams}}
 					<div class="ui small label">{{.NumTeams}}</div>
 				{{end}}
diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl
index 689091e5e0..97cc6cf66c 100644
--- a/templates/org/projects/list.tmpl
+++ b/templates/org/projects/list.tmpl
@@ -1,10 +1,9 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization projects">
+		{{template "org/header" .}}
 		<div class="ui container">
-		{{template "user/overview/header" .}}
-		{{template "projects/list" .}}
+			{{template "projects/list" .}}
 		</div>
 	</div>
 {{else}}
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl
index 6ef62753e2..10e26c7010 100644
--- a/templates/package/settings.tmpl
+++ b/templates/package/settings.tmpl
@@ -1,8 +1,14 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content repository settings options">
-	{{template "shared/user/org_profile_avatar" .}}
+<div role="main" aria-label="{{.Title}}" class="page-content repository settings options{{if .ContextUser.IsOrganization}} organization{{end}}">
+	{{if .ContextUser.IsOrganization}}
+		{{template "org/header" .}}
+	{{else}}
+		{{template "shared/user/org_profile_avatar" .}}
+	{{end}}
 	<div class="ui container">
-		{{template "user/overview/header" .}}
+		{{if not .ContextUser.IsOrganization}}
+			{{template "user/overview/header" .}}
+		{{end}}
 		{{template "base/alert" .}}
 		<p><a href="{{.PackageDescriptor.FullWebLink}}">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p>
 		<h4 class="ui top attached header">
diff --git a/templates/user/code.tmpl b/templates/user/code.tmpl
index da9a3c3a24..f71f55c474 100644
--- a/templates/user/code.tmpl
+++ b/templates/user/code.tmpl
@@ -1,9 +1,8 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization code">
+		{{template "org/header" .}}
 		<div class="ui container">
-			{{template "user/overview/header" .}}
 			{{template "code/searchcombo" .}}
 		</div>
 	</div>
diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl
index c0cbe2561c..4fdaa70d87 100644
--- a/templates/user/overview/header.tmpl
+++ b/templates/user/overview/header.tmpl
@@ -10,7 +10,7 @@
 			<div class="ui small label">{{.RepoCount}}</div>
 		{{end}}
 	</a>
-	{{if or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadProjects)}}
+	{{if or .ContextUser.IsIndividual .CanReadProjects}}
 	<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item">
 		{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
 		{{if .ProjectCount}}
@@ -18,55 +18,30 @@
 		{{end}}
 	</a>
 	{{end}}
-	{{if and .IsPackageEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadPackages))}}
+	{{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}}
 		<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
 			{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
 		</a>
 	{{end}}
-	{{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadCode))}}
+	{{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual .CanReadCode)}}
 		<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
 			{{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}}
 		</a>
 	{{end}}
 
-	{{if .ContextUser.IsOrganization}}
-		{{if .NumMembers}}
-			<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
-				{{svg "octicon-person"}}&nbsp;{{ctx.Locale.Tr "org.members"}}
-				<div class="ui small label">{{.NumMembers}}</div>
-			</a>
-		{{end}}
-		{{if .IsOrganizationMember}}
-			<a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams">
-				{{svg "octicon-people"}}&nbsp;{{ctx.Locale.Tr "org.teams"}}
-				{{if .NumTeams}}
-					<div class="ui small label">{{.NumTeams}}</div>
-				{{end}}
-			</a>
-		{{end}}
-
-		{{if .IsOrganizationOwner}}
-			<div class="right menu">
-				<a class="item" href="{{.OrgLink}}/settings">
-				{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
-				</a>
-			</div>
-		{{end}}
-	{{else}}
-		<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
-			{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
+	<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
+		{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
+	</a>
+	{{if not .DisableStars}}
+		<a class="{{if eq .TabName "stars"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=stars">
+			{{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}}
+			{{if .ContextUser.NumStars}}
+				<div class="ui small label">{{.ContextUser.NumStars}}</div>
+			{{end}}
+		</a>
+	{{else}}
+		<a class="{{if eq .TabName "watching"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=watching">
+			{{svg "octicon-eye"}} {{ctx.Locale.Tr "user.watched"}}
 		</a>
-		{{if not .DisableStars}}
-			<a class="{{if eq .TabName "stars"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=stars">
-				{{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}}
-				{{if .ContextUser.NumStars}}
-					<div class="ui small label">{{.ContextUser.NumStars}}</div>
-				{{end}}
-			</a>
-		{{else}}
-			<a class="{{if eq .TabName "watching"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=watching">
-				{{svg "octicon-eye"}} {{ctx.Locale.Tr "user.watched"}}
-			</a>
-		{{end}}
 	{{end}}
 </div>
diff --git a/templates/user/overview/package_versions.tmpl b/templates/user/overview/package_versions.tmpl
index 6f740e0e7c..f6f963aecb 100644
--- a/templates/user/overview/package_versions.tmpl
+++ b/templates/user/overview/package_versions.tmpl
@@ -1,10 +1,9 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization packages">
+		{{template "org/header" .}}
 		<div class="ui container">
-		{{template "user/overview/header" .}}
-		{{template "package/shared/versionlist" .}}
+			{{template "package/shared/versionlist" .}}
 		</div>
 	</div>
 {{else}}
diff --git a/templates/user/overview/packages.tmpl b/templates/user/overview/packages.tmpl
index 4fd17696d1..30ff871cb2 100644
--- a/templates/user/overview/packages.tmpl
+++ b/templates/user/overview/packages.tmpl
@@ -1,10 +1,9 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization packages">
+		{{template "org/header" .}}
 		<div class="ui container">
-		{{template "user/overview/header" .}}
-		{{template "package/shared/list" .}}
+			{{template "package/shared/list" .}}
 		</div>
 	</div>
 {{else}}
diff --git a/web_src/css/org.css b/web_src/css/org.css
index d2bf0ff606..8b3684d0c0 100644
--- a/web_src/css/org.css
+++ b/web_src/css/org.css
@@ -93,46 +93,44 @@
   min-width: 300px;
 }
 
-.organization.profile .org-avatar {
-  width: 100px;
-  height: 100px;
+.page-content.organization .org-avatar {
   margin-right: 15px;
 }
 
-.organization.profile #org-info {
+.page-content.organization #org-info {
   overflow-wrap: anywhere;
   flex: 1;
   word-break: break-all;
 }
 
-.organization.profile #org-info .ui.header {
+.page-content.organization #org-info .ui.header {
   display: flex;
   align-items: center;
   font-size: 36px;
   margin-bottom: 0;
 }
 
-.organization.profile #org-info .desc {
+.page-content.organization #org-info .desc {
   font-size: 16px;
   margin-bottom: 10px;
 }
 
-.organization.profile #org-info .meta {
+.page-content.organization #org-info .meta {
   display: flex;
   align-items: center;
   flex-wrap: wrap;
   gap: 8px;
 }
 
-.organization.profile .ui.top.header .ui.right {
+.page-content.organization .ui.top.header .ui.right {
   margin-top: 0;
 }
 
-.organization.profile .teams .item {
+.page-content.organization .teams .item {
   padding: 10px 15px;
 }
 
-.organization.profile .members .ui.avatar {
+.page-content.organization .members .ui.avatar {
   width: 48px;
   height: 48px;
   margin-right: 5px;

From b748d62b461f9f23823f8772bc708b44b15a23a7 Mon Sep 17 00:00:00 2001
From: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Date: Fri, 23 Feb 2024 01:57:24 +0100
Subject: [PATCH 133/679] Add slow SQL query warning (#27545)

- Databases are one of the most important parts of Forgejo, every
interaction uses the database in one way or another. Therefore, it is
important to maintain the database and recognize when the server is not
doing well with the database. There already is the option to log *every*
SQL query along with its execution time, but monitoring becomes
impractical for larger instances and takes up unnecessary storage in the
logs.
- Add a QoL enhancement that allows instance administrators to specify a
threshold value beyond which query execution time is logged as a warning
in the xorm logger. The default value is a conservative five seconds to
avoid this becoming a source of spam in the logs.
- The use case for this patch is that with an instance the size of
Codeberg, monitoring SQL logs is not very fruitful and most of them are
uninteresting. Recently, in the context of persistent deadlock issues
(https://codeberg.org/forgejo/forgejo/issues/220), I have noticed that
certain queries hold locks on tables like comment and issue for several
seconds. This patch helps to identify which queries these are and when
they happen.
- Added unit test.

(cherry picked from commit 9cf501f1af4cd870221cef6af489618785b71186)

---------

Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: 6543 <6543@obermui.de>
---
 custom/conf/app.example.ini                   |  4 ++
 .../config-cheat-sheet.en-us.md               |  1 +
 models/db/engine.go                           | 31 ++++++++++++++
 modules/setting/database.go                   | 42 ++++++++++---------
 4 files changed, 58 insertions(+), 20 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 4aae1c497f..a360970593 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -412,6 +412,10 @@ USER = root
 ;;
 ;; Whether execute database models migrations automatically
 ;AUTO_MIGRATION = true
+;;
+;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger
+;;
+;SLOW_QUERY_THRESHOLD = 5s
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 415176d4ff..838e26b0f6 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -458,6 +458,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
 - `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
 - `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
 - `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically.
+- `SLOW_QUERY_THRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger.
 
 [^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details.
 
diff --git a/models/db/engine.go b/models/db/engine.go
index 2cd1c36c58..2a2743e927 100755
--- a/models/db/engine.go
+++ b/models/db/engine.go
@@ -11,10 +11,13 @@ import (
 	"io"
 	"reflect"
 	"strings"
+	"time"
 
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 
 	"xorm.io/xorm"
+	"xorm.io/xorm/contexts"
 	"xorm.io/xorm/names"
 	"xorm.io/xorm/schemas"
 
@@ -143,6 +146,13 @@ func InitEngine(ctx context.Context) error {
 	xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
 	xormEngine.SetDefaultContext(ctx)
 
+	if setting.Database.SlowQueryThreshold > 0 {
+		xormEngine.AddHook(&SlowQueryHook{
+			Threshold: setting.Database.SlowQueryThreshold,
+			Logger:    log.GetLogger("xorm"),
+		})
+	}
+
 	SetDefaultEngine(ctx, xormEngine)
 	return nil
 }
@@ -298,3 +308,24 @@ func SetLogSQL(ctx context.Context, on bool) {
 		sess.Engine().ShowSQL(on)
 	}
 }
+
+type SlowQueryHook struct {
+	Threshold time.Duration
+	Logger    log.Logger
+}
+
+var _ contexts.Hook = &SlowQueryHook{}
+
+func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
+	return c.Ctx, nil
+}
+
+func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
+	if c.ExecuteTime >= h.Threshold {
+		// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
+		// is being displayed (the function that ultimately wants to execute the query in the code)
+		// instead of the function of the slow query hook being called.
+		h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
+	}
+	return nil
+}
diff --git a/modules/setting/database.go b/modules/setting/database.go
index e200b15b2e..1a4bf64805 100644
--- a/modules/setting/database.go
+++ b/modules/setting/database.go
@@ -25,26 +25,27 @@ var (
 
 	// Database holds the database settings
 	Database = struct {
-		Type              DatabaseType
-		Host              string
-		Name              string
-		User              string
-		Passwd            string
-		Schema            string
-		SSLMode           string
-		Path              string
-		LogSQL            bool
-		MysqlCharset      string
-		CharsetCollation  string
-		Timeout           int // seconds
-		SQLiteJournalMode string
-		DBConnectRetries  int
-		DBConnectBackoff  time.Duration
-		MaxIdleConns      int
-		MaxOpenConns      int
-		ConnMaxLifetime   time.Duration
-		IterateBufferSize int
-		AutoMigration     bool
+		Type               DatabaseType
+		Host               string
+		Name               string
+		User               string
+		Passwd             string
+		Schema             string
+		SSLMode            string
+		Path               string
+		LogSQL             bool
+		MysqlCharset       string
+		CharsetCollation   string
+		Timeout            int // seconds
+		SQLiteJournalMode  string
+		DBConnectRetries   int
+		DBConnectBackoff   time.Duration
+		MaxIdleConns       int
+		MaxOpenConns       int
+		ConnMaxLifetime    time.Duration
+		IterateBufferSize  int
+		AutoMigration      bool
+		SlowQueryThreshold time.Duration
 	}{
 		Timeout:           500,
 		IterateBufferSize: 50,
@@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
 	Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
 	Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
 	Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
+	Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
 }
 
 // DBConnStr returns database connection string

From 7fbdb60fc1152acc9a040dc04b1b0f5a3475b081 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Fri, 23 Feb 2024 03:18:33 +0100
Subject: [PATCH 134/679] Start to migrate from `util.OptionalBool` to
 `optional.Option[bool]` (#29329)

just create transition helper and migrate two structs
---
 cmd/admin_user_create.go                      |  8 +++---
 models/git/branch_list.go                     | 10 ++++----
 models/git/branch_test.go                     |  4 +--
 models/git/protected_branch_list.go           |  4 +--
 models/user/user.go                           | 25 ++++++++++---------
 modules/context/repo.go                       |  3 ++-
 modules/optional/option_test.go               |  5 ++--
 modules/util/util.go                          | 18 +++++++++++++
 routers/api/v1/admin/user.go                  |  8 ++----
 routers/api/v1/repo/branch.go                 |  6 ++---
 routers/install/install.go                    |  6 ++---
 routers/web/admin/users.go                    |  2 +-
 routers/web/auth/oauth.go                     |  2 +-
 routers/web/repo/compare.go                   |  5 ++--
 routers/web/repo/pull.go                      |  3 ++-
 routers/web/repo/repo.go                      |  5 ++--
 services/auth/reverseproxy.go                 |  4 +--
 .../auth/source/ldap/source_authenticate.go   |  5 ++--
 services/auth/source/ldap/source_sync.go      |  5 ++--
 .../auth/source/pam/source_authenticate.go    |  4 +--
 .../auth/source/smtp/source_authenticate.go   |  3 ++-
 services/auth/sspi.go                         |  5 ++--
 services/repository/adopt.go                  |  3 ++-
 services/repository/branch.go                 |  5 ++--
 24 files changed, 84 insertions(+), 64 deletions(-)

diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index fefe18d39c..a257ce21c8 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -10,8 +10,8 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	pwd "code.gitea.io/gitea/modules/auth/password"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/urfave/cli/v2"
 )
@@ -123,10 +123,10 @@ func runCreateUser(c *cli.Context) error {
 		changePassword = c.Bool("must-change-password")
 	}
 
-	restricted := util.OptionalBoolNone
+	restricted := optional.None[bool]()
 
 	if c.IsSet("restricted") {
-		restricted = util.OptionalBoolOf(c.Bool("restricted"))
+		restricted = optional.Some(c.Bool("restricted"))
 	}
 
 	// default user visibility in app.ini
@@ -142,7 +142,7 @@ func runCreateUser(c *cli.Context) error {
 	}
 
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive:     util.OptionalBoolTrue,
+		IsActive:     optional.Some(true),
 		IsRestricted: restricted,
 	}
 
diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 0e8d28038a..8319e5ecd0 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 )
@@ -67,7 +67,7 @@ type FindBranchOptions struct {
 	db.ListOptions
 	RepoID             int64
 	ExcludeBranchNames []string
-	IsDeletedBranch    util.OptionalBool
+	IsDeletedBranch    optional.Option[bool]
 	OrderBy            string
 	Keyword            string
 }
@@ -81,8 +81,8 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
 	if len(opts.ExcludeBranchNames) > 0 {
 		cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames))
 	}
-	if !opts.IsDeletedBranch.IsNone() {
-		cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.IsTrue()})
+	if opts.IsDeletedBranch.Has() {
+		cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.Value()})
 	}
 	if opts.Keyword != "" {
 		cond = cond.And(builder.Like{"name", opts.Keyword})
@@ -92,7 +92,7 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
 
 func (opts FindBranchOptions) ToOrders() string {
 	orderBy := opts.OrderBy
-	if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end
+	if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end
 		if orderBy != "" {
 			orderBy += ", "
 		}
diff --git a/models/git/branch_test.go b/models/git/branch_test.go
index fd5d6519e9..b8ea663e81 100644
--- a/models/git/branch_test.go
+++ b/models/git/branch_test.go
@@ -13,7 +13,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -50,7 +50,7 @@ func TestGetDeletedBranches(t *testing.T) {
 	branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{
 		ListOptions:     db.ListOptionsAll,
 		RepoID:          repo.ID,
-		IsDeletedBranch: util.OptionalBoolTrue,
+		IsDeletedBranch: optional.Some(true),
 	})
 	assert.NoError(t, err)
 	assert.Len(t, branches, 2)
diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go
index eeb307e245..613333a5a2 100644
--- a/models/git/protected_branch_list.go
+++ b/models/git/protected_branch_list.go
@@ -8,7 +8,7 @@ import (
 	"sort"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/gobwas/glob"
 )
@@ -56,7 +56,7 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
 				Page:     page,
 			},
 			RepoID:          repoID,
-			IsDeletedBranch: util.OptionalBoolFalse,
+			IsDeletedBranch: optional.Some(false),
 		})
 		if err != nil {
 			return nil, err
diff --git a/models/user/user.go b/models/user/user.go
index f31dfb76bb..e92bbd4d0b 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -573,14 +574,14 @@ func IsUsableUsername(name string) error {
 
 // CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation
 type CreateUserOverwriteOptions struct {
-	KeepEmailPrivate             util.OptionalBool
+	KeepEmailPrivate             optional.Option[bool]
 	Visibility                   *structs.VisibleType
-	AllowCreateOrganization      util.OptionalBool
+	AllowCreateOrganization      optional.Option[bool]
 	EmailNotificationsPreference *string
 	MaxRepoCreation              *int
 	Theme                        *string
-	IsRestricted                 util.OptionalBool
-	IsActive                     util.OptionalBool
+	IsRestricted                 optional.Option[bool]
+	IsActive                     optional.Option[bool]
 }
 
 // CreateUser creates record of a new user.
@@ -607,14 +608,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 	// overwrite defaults if set
 	if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
 		overwrite := overwriteDefault[0]
-		if !overwrite.KeepEmailPrivate.IsNone() {
-			u.KeepEmailPrivate = overwrite.KeepEmailPrivate.IsTrue()
+		if overwrite.KeepEmailPrivate.Has() {
+			u.KeepEmailPrivate = overwrite.KeepEmailPrivate.Value()
 		}
 		if overwrite.Visibility != nil {
 			u.Visibility = *overwrite.Visibility
 		}
-		if !overwrite.AllowCreateOrganization.IsNone() {
-			u.AllowCreateOrganization = overwrite.AllowCreateOrganization.IsTrue()
+		if overwrite.AllowCreateOrganization.Has() {
+			u.AllowCreateOrganization = overwrite.AllowCreateOrganization.Value()
 		}
 		if overwrite.EmailNotificationsPreference != nil {
 			u.EmailNotificationsPreference = *overwrite.EmailNotificationsPreference
@@ -625,11 +626,11 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 		if overwrite.Theme != nil {
 			u.Theme = *overwrite.Theme
 		}
-		if !overwrite.IsRestricted.IsNone() {
-			u.IsRestricted = overwrite.IsRestricted.IsTrue()
+		if overwrite.IsRestricted.Has() {
+			u.IsRestricted = overwrite.IsRestricted.Value()
 		}
-		if !overwrite.IsActive.IsNone() {
-			u.IsActive = overwrite.IsActive.IsTrue()
+		if overwrite.IsActive.Has() {
+			u.IsActive = overwrite.IsActive.Value()
 		}
 	}
 
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 3ff7209c4c..8508d46cf4 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -27,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -671,7 +672,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 		ListOptions:     db.ListOptionsAll,
 	}
 	branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go
index 7ec345b6ba..bfc4577dbe 100644
--- a/modules/optional/option_test.go
+++ b/modules/optional/option_test.go
@@ -6,8 +6,6 @@ package optional
 import (
 	"testing"
 
-	"code.gitea.io/gitea/modules/util"
-
 	"github.com/stretchr/testify/assert"
 )
 
@@ -30,7 +28,8 @@ func TestOption(t *testing.T) {
 	var ptr *int
 	assert.False(t, FromPtr(ptr).Has())
 
-	opt1 := FromPtr(util.ToPointer(1))
+	int1 := 1
+	opt1 := FromPtr(&int1)
 	assert.True(t, opt1.Has())
 	assert.Equal(t, int(1), opt1.Value())
 
diff --git a/modules/util/util.go b/modules/util/util.go
index 0e5c6a4e64..28b549f405 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -11,6 +11,8 @@ import (
 	"strconv"
 	"strings"
 
+	"code.gitea.io/gitea/modules/optional"
+
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 )
@@ -42,6 +44,22 @@ func (o OptionalBool) IsNone() bool {
 	return o == OptionalBoolNone
 }
 
+// ToGeneric converts OptionalBool to optional.Option[bool]
+func (o OptionalBool) ToGeneric() optional.Option[bool] {
+	if o.IsNone() {
+		return optional.None[bool]()
+	}
+	return optional.Some[bool](o.IsTrue())
+}
+
+// OptionalBoolFromGeneric converts optional.Option[bool] to OptionalBool
+func OptionalBoolFromGeneric(o optional.Option[bool]) OptionalBool {
+	if o.Has() {
+		return OptionalBoolOf(o.Value())
+	}
+	return OptionalBoolNone
+}
+
 // OptionalBoolOf get the corresponding OptionalBool of a bool
 func OptionalBoolOf(b bool) OptionalBool {
 	if b {
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 272996f43d..2ce7651a09 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -21,7 +21,6 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
@@ -117,11 +116,8 @@ func CreateUser(ctx *context.APIContext) {
 	}
 
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
-	}
-
-	if form.Restricted != nil {
-		overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
+		IsActive:     optional.Some(true),
+		IsRestricted: optional.FromPtr(form.Restricted),
 	}
 
 	if form.Visibility != "" {
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index bd02a8afc4..2cdbcd25a2 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -17,9 +17,9 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/convert"
@@ -141,7 +141,7 @@ func DeleteBranch(ctx *context.APIContext) {
 	// check whether branches of this repository has been synced
 	totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "CountBranches", err)
@@ -340,7 +340,7 @@ func ListBranches(ctx *context.APIContext) {
 		branchOpts := git_model.FindBranchOptions{
 			ListOptions:     listOptions,
 			RepoID:          ctx.Repo.Repository.ID,
-			IsDeletedBranch: util.OptionalBoolFalse,
+			IsDeletedBranch: optional.Some(false),
 		}
 		var err error
 		totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
diff --git a/routers/install/install.go b/routers/install/install.go
index 064575d34c..78372669f4 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -25,12 +25,12 @@ import (
 	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/user"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/common"
@@ -533,8 +533,8 @@ func SubmitInstall(ctx *context.Context) {
 			IsAdmin: true,
 		}
 		overwriteDefault := &user_model.CreateUserOverwriteOptions{
-			IsRestricted: util.OptionalBoolFalse,
-			IsActive:     util.OptionalBoolTrue,
+			IsRestricted: optional.Some(false),
+			IsActive:     optional.Some(true),
 		}
 
 		if err = user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index af184fa9eb..adb9799c01 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -140,7 +140,7 @@ func NewUserPost(ctx *context.Context) {
 	}
 
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive:   util.OptionalBoolTrue,
+		IsActive:   optional.Some(true),
 		Visibility: &form.Visibility,
 	}
 
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index d00644dd5f..5e7368eb9a 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -979,7 +979,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			}
 
 			overwriteDefault := &user_model.CreateUserOverwriteOptions{
-				IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
+				IsActive: optional.Some(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
 			}
 
 			source := authSource.Cfg.(*oauth2.Source)
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 67d41cf807..df41c750de 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -31,6 +31,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/typesniffer"
@@ -700,7 +701,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
 		ListOptions: db.ListOptions{
 			ListAll: true,
 		},
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
 		return nil, nil, err
@@ -757,7 +758,7 @@ func CompareDiff(ctx *context.Context) {
 		ListOptions: db.ListOptions{
 			ListAll: true,
 		},
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("GetBranches", err)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index b9a4aff02e..14f1eb3102 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -32,6 +32,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	issue_template "code.gitea.io/gitea/modules/issue/template"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/upload"
@@ -186,7 +187,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
 		ListOptions: db.ListOptions{
 			ListAll: true,
 		},
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 		// Add it as the first option
 		ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
 	})
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index bede21be17..323413d976 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -24,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
@@ -685,7 +686,7 @@ type branchTagSearchResponse struct {
 func GetBranchesList(ctx *context.Context) {
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 		ListOptions: db.ListOptions{
 			ListAll: true,
 		},
@@ -720,7 +721,7 @@ func GetTagList(ctx *context.Context) {
 func PrepareBranchList(ctx *context.Context) {
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 		ListOptions: db.ListOptions{
 			ListAll: true,
 		},
diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go
index 359c1f2473..b6aeb0aed2 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -10,8 +10,8 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 
 	gouuid "github.com/google/uuid"
@@ -161,7 +161,7 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
 	}
 
 	overwriteDefault := user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	}
 
 	if err := user_model.CreateUser(req.Context(), user, &overwriteDefault); err != nil {
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 8f641ed541..68ecd16342 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -13,7 +13,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	auth_module "code.gitea.io/gitea/modules/auth"
 	"code.gitea.io/gitea/modules/optional"
-	"code.gitea.io/gitea/modules/util"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -85,8 +84,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 			IsAdmin:     sr.IsAdmin,
 		}
 		overwriteDefault := &user_model.CreateUserOverwriteOptions{
-			IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
-			IsActive:     util.OptionalBoolTrue,
+			IsRestricted: optional.Some(sr.IsRestricted),
+			IsActive:     optional.Some(true),
 		}
 
 		err := user_model.CreateUser(ctx, user, overwriteDefault)
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index eee7bb585a..62f052d68c 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -16,7 +16,6 @@ import (
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
-	"code.gitea.io/gitea/modules/util"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -125,8 +124,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 				IsAdmin:     su.IsAdmin,
 			}
 			overwriteDefault := &user_model.CreateUserOverwriteOptions{
-				IsRestricted: util.OptionalBoolOf(su.IsRestricted),
-				IsActive:     util.OptionalBoolTrue,
+				IsRestricted: optional.Some(su.IsRestricted),
+				IsActive:     optional.Some(true),
 			}
 
 			err = user_model.CreateUser(ctx, usr, overwriteDefault)
diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go
index 0891a86392..addd1bd2c9 100644
--- a/services/auth/source/pam/source_authenticate.go
+++ b/services/auth/source/pam/source_authenticate.go
@@ -11,8 +11,8 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/pam"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/google/uuid"
 )
@@ -60,7 +60,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		LoginName:   userName, // This is what the user typed in
 	}
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	}
 
 	if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go
index b244fc7d40..1f0a61c789 100644
--- a/services/auth/source/smtp/source_authenticate.go
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -12,6 +12,7 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -75,7 +76,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		LoginName:   userName,
 	}
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	}
 
 	if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/services/auth/sspi.go b/services/auth/sspi.go
index 0e974fde8f..8c0fc77a96 100644
--- a/services/auth/sspi.go
+++ b/services/auth/sspi.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
@@ -172,8 +173,8 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) (
 	}
 	emailNotificationPreference := user_model.EmailNotificationsDisabled
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive:                     util.OptionalBoolOf(cfg.AutoActivateUsers),
-		KeepEmailPrivate:             util.OptionalBoolTrue,
+		IsActive:                     optional.Some(cfg.AutoActivateUsers),
+		KeepEmailPrivate:             optional.Some(true),
 		EmailNotificationsPreference: &emailNotificationPreference,
 	}
 	if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index bfb965063f..7ca68776b5 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -154,7 +155,7 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
 		ListOptions: db.ListOptions{
 			ListAll: true,
 		},
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 	})
 
 	found := false
diff --git a/services/repository/branch.go b/services/repository/branch.go
index e2e50297af..38781acb58 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/queue"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -61,7 +62,7 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          repo.ID,
-		IsDeletedBranch: isDeletedBranch,
+		IsDeletedBranch: isDeletedBranch.ToGeneric(),
 		ListOptions: db.ListOptions{
 			Page:     page,
 			PageSize: pageSize,
@@ -239,7 +240,7 @@ func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName stri
 	// we cannot simply insert the branch but need to check we have branches or not
 	hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
 		RepoID:          repoID,
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 	}.ToConds())
 	if err != nil {
 		return err

From 3ef6252e06a1f3981f8b7d1717bfc581418b1dc5 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 23 Feb 2024 15:24:04 +0800
Subject: [PATCH 135/679] Allow options to disable user deletion from the
 interface on app.ini (#29275)

Extract from #20549

This PR added a new option on app.ini `[admin]USER_DISABLED_FEATURES` to
allow the site administrator to disable users visiting deletion user
interface or allow.
This options are also potentially allowed to define more features in
future PRs.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 custom/conf/app.example.ini                   |  3 +++
 .../config-cheat-sheet.en-us.md               |  2 ++
 .../config-cheat-sheet.zh-cn.md               |  2 ++
 modules/setting/admin.go                      | 10 +++++++-
 routers/web/user/setting/account.go           |  6 +++++
 templates/user/settings/account.tmpl          | 23 ++++++++++---------
 6 files changed, 34 insertions(+), 12 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index a360970593..5451537d02 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1474,6 +1474,9 @@ LEVEL = Info
 ;;
 ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 ;DEFAULT_EMAIL_NOTIFICATIONS = enabled
+;; Disabled features for users, could be "deletion", more features can be disabled in future
+;; - deletion: a user cannot delete their own account
+;USER_DISABLED_FEATURES =
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 838e26b0f6..643932de6c 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -518,6 +518,8 @@ And the following unique queues:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
+- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion` and more features can be added in future.
+  - `deletion`: User cannot delete their own account.
 
 ## Security (`security`)
 
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 01906930cb..5fe0a62215 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -497,6 +497,8 @@ Gitea 创建以下非唯一队列:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
+- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`, 未来可以增加更多设置。
+  - `deletion`: 用户不能通过界面或者API删除他自己。
 
 ## 安全性 (`security`)
 
diff --git a/modules/setting/admin.go b/modules/setting/admin.go
index 2d2dd26de9..48a2ea9744 100644
--- a/modules/setting/admin.go
+++ b/modules/setting/admin.go
@@ -3,14 +3,22 @@
 
 package setting
 
+import "code.gitea.io/gitea/modules/container"
+
 // Admin settings
 var Admin struct {
 	DisableRegularOrgCreation bool
 	DefaultEmailNotification  string
+	UserDisabledFeatures      container.Set[string]
 }
 
 func loadAdminFrom(rootCfg ConfigProvider) {
-	mustMapSetting(rootCfg, "admin", &Admin)
 	sec := rootCfg.Section("admin")
+	Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
 	Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
+	Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
 }
+
+const (
+	UserFeatureDeletion = "deletion"
+)
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index c7f194a3b5..659c3e29c1 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -233,6 +233,11 @@ func DeleteEmail(ctx *context.Context) {
 
 // DeleteAccount render user suicide page and response for delete user himself
 func DeleteAccount(ctx *context.Context) {
+	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureDeletion) {
+		ctx.Error(http.StatusNotFound)
+		return
+	}
+
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsSettingsAccount"] = true
 
@@ -299,6 +304,7 @@ func loadAccountData(ctx *context.Context) {
 	ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
 	ctx.Data["ActivationsPending"] = pendingActivation
 	ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
+	ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures
 
 	if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
 		ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index bfcf423d67..515e79d739 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -128,6 +128,7 @@
 			{{end}}
 		</div>
 
+		{{if not ($.UserDisabledFeatures.Contains "deletion")}}
 		<h4 class="ui top attached error header">
 			{{ctx.Locale.Tr "settings.delete_account"}}
 		</h4>
@@ -151,7 +152,18 @@
 					</button>
 				</div>
 			</form>
+			<div class="ui g-modal-confirm delete modal" id="delete-account">
+				<div class="header">
+					{{svg "octicon-trash"}}
+					{{ctx.Locale.Tr "settings.delete_account_title"}}
+				</div>
+				<div class="content">
+					<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
+				</div>
+				{{template "base/modal_actions_confirm" .}}
+			</div>
 		</div>
+		{{end}}
 	</div>
 
 <div class="ui g-modal-confirm delete modal" id="delete-email">
@@ -165,15 +177,4 @@
 	{{template "base/modal_actions_confirm" .}}
 </div>
 
-<div class="ui g-modal-confirm delete modal" id="delete-account">
-	<div class="header">
-		{{svg "octicon-trash"}}
-		{{ctx.Locale.Tr "settings.delete_account_title"}}
-	</div>
-	<div class="content">
-		<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
-	</div>
-	{{template "base/modal_actions_confirm" .}}
-</div>
-
 {{template "user/settings/layout_footer" .}}

From 7d0903bf90bce6d0ed2fa131ab028a55b8729b73 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 23 Feb 2024 19:09:18 +0800
Subject: [PATCH 136/679] Adjust changelog for v1.21.6 to move prs to correct
 labels (#29339) (#29343)

When releasing, the releaser should read all the pull requests carefully
and do some adjustments because some of pull requests' labels are not
right when it's merged.

And the changelog tool needs to be adjusted. If one pull request has
both `bug` and `API`, it should mark it as `bug` but not `API`.

Backport #29339
---
 CHANGELOG.md | 22 ++++++++++------------
 1 file changed, 10 insertions(+), 12 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f2c69888a..e119d0bec0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,9 +9,6 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
 * SECURITY
   * Fix XSS vulnerabilities (#29336)
   * Use general token signing secret (#29205) (#29325)
-* API
-  * Refactor issue template parsing and fix API endpoint (#29069) (#29140)
-  * Fix swift packages not resolving (#29095) (#29102)
 * ENHANCEMENTS
   * Refactor git version functions and check compatibility (#29155) (#29157)
   * Improve user experience for outdated comments (#29050) (#29086)
@@ -19,7 +16,11 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Wrap contained tags and branches again (#29021) (#29026)
   * Fix incorrect button CSS usages (#29015) (#29023)
   * Strip trailing newline in markdown code copy (#29019) (#29022)
+  * Implement some action notifier functions (#29173) (#29308)
+  * Load outdated comments when (un)resolving conversation on PR timeline (#29203) (#29221)
 * BUGFIXES
+  * Refactor issue template parsing and fix API endpoint (#29069) (#29140)
+  * Fix swift packages not resolving (#29095) (#29102)
   * Remove SSH workaround (#27893) (#29332)
   * Only log error when tag sync fails (#29295) (#29327)
   * Fix SSPI user creation (#28948) (#29323)
@@ -44,18 +45,15 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Avoid showing unnecessary JS errors when there are elements with different origin on the page (#29081) (#29089)
   * Fix gitea-origin-url with default ports (#29085) (#29088)
   * Fix orgmode link resolving (#29024) (#29076)
-  * Fix: Elasticsearch: Request Entity Too Large #28117 (#29062) (#29075)
+  * Fix Elasticsearh Request Entity Too Large #28117 (#29062) (#29075)
   * Do not render empty comments (#29039) (#29049)
   * Avoid sending update/delete release notice when it is draft (#29008) (#29025)
-* DOCS
-  * Rm outdated docs from some languages (#27530) (#29208)
-* MISC
-  * Implement some action notifier functions (#29173) (#29308)
   * Fix gitea-action user avatar broken on edited menu (#29190) (#29307)
   * Disallow merge when required checked are missing (#29143) (#29268)
-  * Convert visibility to number (#29226) (#29244)
-  * Load outdated comments when (un)resolving conversation on PR timeline (#29203) (#29221)
   * Fix incorrect link to swift doc and swift package-registry login command (#29096) (#29103)
+  * Convert visibility to number (#29226) (#29244)
+* DOCS
+  * Remove outdated docs from some languages (#27530) (#29208)
   * Fix typos in the documentation (#29048) (#29056)
   * Explained where create issue/PR template (#29035)
 
@@ -174,7 +172,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Fix Chinese translation of config cheat sheet[API] (#28472) (#28473)
   * Retry SSH key verification with additional CRLF if it failed (#28392) (#28464)
 
-## [1.21.2](https://github.com/go-gitea/gitea/releases/tag/1.21.2) - 2023-12-12
+## [1.21.2](https://github.com/go-gitea/gitea/releases/tag/v1.21.2) - 2023-12-12
 
 * SECURITY
   * Rebuild with recently released golang version
@@ -213,7 +211,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Use full width for project boards (#28225) (#28245)
   * Enable system users search via the API (#28013) (#28018)
 
-## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/1.21.1) - 2023-11-26
+## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/v1.21.1) - 2023-11-26
 
 * SECURITY
   * Fix comment permissions (#28213) (#28216)

From 2a278b996fd6608973c3ab2a2cfb584e67d5bd8b Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Fri, 23 Feb 2024 18:24:27 +0100
Subject: [PATCH 137/679] Add support for `linguist-detectable` and
 `linguist-documentation` (#29267)

Add support for `linguist-detectable` and `linguist-documentation`
Add tests for the attributes


https://github.com/github-linguist/linguist/blob/master/docs/overrides.md#detectable

https://github.com/github-linguist/linguist/blob/master/docs/overrides.md#documentation
---
 modules/git/repo_attribute.go              |  23 +-
 modules/git/repo_language_stats_gogit.go   |  77 +++---
 modules/git/repo_language_stats_nogogit.go |  77 +++---
 tests/integration/linguist_test.go         | 259 +++++++++++++++++++++
 4 files changed, 365 insertions(+), 71 deletions(-)
 create mode 100644 tests/integration/linguist_test.go

diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index 2b34f117f7..44f13ddc2d 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -11,6 +11,7 @@ import (
 	"os"
 
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 // CheckAttributeOpts represents the possible options to CheckAttribute
@@ -291,7 +292,7 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
 	}
 
 	checker := &CheckAttributeReader{
-		Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
+		Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"},
 		Repo:       repo,
 		IndexFile:  indexFilename,
 		WorkTree:   worktree,
@@ -316,3 +317,23 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
 
 	return checker, deferable
 }
+
+// true if "set"/"true", false if "unset"/"false", none otherwise
+func attributeToBool(attr map[string]string, name string) optional.Option[bool] {
+	if value, has := attr[name]; has && value != "unspecified" {
+		switch value {
+		case "set", "true":
+			return optional.Some(true)
+		case "unset", "false":
+			return optional.Some(false)
+		}
+	}
+	return optional.None[bool]()
+}
+
+func attributeToString(attr map[string]string, name string) optional.Option[string] {
+	if value, has := attr[name]; has && value != "unspecified" {
+		return optional.Some(value)
+	}
+	return optional.None[string]()
+}
diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go
index 4c6fbd6c7e..99c7a894d5 100644
--- a/modules/git/repo_language_stats_gogit.go
+++ b/modules/git/repo_language_stats_gogit.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/analyze"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/go-enry/go-enry/v2"
 	"github.com/go-git/go-git/v5"
@@ -57,25 +58,47 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			return nil
 		}
 
-		notVendored := false
-		notGenerated := false
+		isVendored := optional.None[bool]()
+		isGenerated := optional.None[bool]()
+		isDocumentation := optional.None[bool]()
+		isDetectable := optional.None[bool]()
 
 		if checker != nil {
 			attrs, err := checker.CheckPath(f.Name)
 			if err == nil {
-				if vendored, has := attrs["linguist-vendored"]; has {
-					if vendored == "set" || vendored == "true" {
-						return nil
-					}
-					notVendored = vendored == "false"
+				isVendored = attributeToBool(attrs, "linguist-vendored")
+				if isVendored.ValueOrDefault(false) {
+					return nil
 				}
-				if generated, has := attrs["linguist-generated"]; has {
-					if generated == "set" || generated == "true" {
-						return nil
-					}
-					notGenerated = generated == "false"
+
+				isGenerated = attributeToBool(attrs, "linguist-generated")
+				if isGenerated.ValueOrDefault(false) {
+					return nil
 				}
-				if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
+
+				isDocumentation = attributeToBool(attrs, "linguist-documentation")
+				if isDocumentation.ValueOrDefault(false) {
+					return nil
+				}
+
+				isDetectable = attributeToBool(attrs, "linguist-detectable")
+				if !isDetectable.ValueOrDefault(true) {
+					return nil
+				}
+
+				hasLanguage := attributeToString(attrs, "linguist-language")
+				if hasLanguage.Value() == "" {
+					hasLanguage = attributeToString(attrs, "gitlab-language")
+					if hasLanguage.Has() {
+						language := hasLanguage.Value()
+						if idx := strings.IndexByte(language, '?'); idx >= 0 {
+							hasLanguage = optional.Some(language[:idx])
+						}
+					}
+				}
+				if hasLanguage.Value() != "" {
+					language := hasLanguage.Value()
+
 					// group languages, such as Pug -> HTML; SCSS -> CSS
 					group := enry.GetLanguageGroup(language)
 					if len(group) != 0 {
@@ -85,28 +108,14 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 					// this language will always be added to the size
 					sizes[language] += f.Size
 					return nil
-				} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
-					// strip off a ? if present
-					if idx := strings.IndexByte(language, '?'); idx >= 0 {
-						language = language[:idx]
-					}
-					if len(language) != 0 {
-						// group languages, such as Pug -> HTML; SCSS -> CSS
-						group := enry.GetLanguageGroup(language)
-						if len(group) != 0 {
-							language = group
-						}
-
-						// this language will always be added to the size
-						sizes[language] += f.Size
-						return nil
-					}
 				}
 			}
 		}
 
-		if (!notVendored && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) ||
-			enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
+		if (!isVendored.Has() && analyze.IsVendor(f.Name)) ||
+			enry.IsDotFile(f.Name) ||
+			(!isDocumentation.Has() && enry.IsDocumentation(f.Name)) ||
+			enry.IsConfiguration(f.Name) {
 			return nil
 		}
 
@@ -115,12 +124,10 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 		if f.Size <= bigFileSize {
 			content, _ = readFile(f, fileSizeLimit)
 		}
-		if !notGenerated && enry.IsGenerated(f.Name, content) {
+		if !isGenerated.Has() && enry.IsGenerated(f.Name, content) {
 			return nil
 		}
 
-		// TODO: Use .gitattributes file for linguist overrides
-
 		language := analyze.GetCodeLanguage(f.Name, content)
 		if language == enry.OtherLanguage || language == "" {
 			return nil
@@ -138,7 +145,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			included = langtype == enry.Programming || langtype == enry.Markup
 			includedLanguage[language] = included
 		}
-		if included {
+		if included || isDetectable.ValueOrDefault(false) {
 			sizes[language] += f.Size
 		} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
 			firstExcludedLanguage = language
diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go
index d68d7d210a..16669924d6 100644
--- a/modules/git/repo_language_stats_nogogit.go
+++ b/modules/git/repo_language_stats_nogogit.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/modules/analyze"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/go-enry/go-enry/v2"
 )
@@ -88,25 +89,47 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			continue
 		}
 
-		notVendored := false
-		notGenerated := false
+		isVendored := optional.None[bool]()
+		isGenerated := optional.None[bool]()
+		isDocumentation := optional.None[bool]()
+		isDetectable := optional.None[bool]()
 
 		if checker != nil {
 			attrs, err := checker.CheckPath(f.Name())
 			if err == nil {
-				if vendored, has := attrs["linguist-vendored"]; has {
-					if vendored == "set" || vendored == "true" {
-						continue
-					}
-					notVendored = vendored == "false"
+				isVendored = attributeToBool(attrs, "linguist-vendored")
+				if isVendored.ValueOrDefault(false) {
+					continue
 				}
-				if generated, has := attrs["linguist-generated"]; has {
-					if generated == "set" || generated == "true" {
-						continue
-					}
-					notGenerated = generated == "false"
+
+				isGenerated = attributeToBool(attrs, "linguist-generated")
+				if isGenerated.ValueOrDefault(false) {
+					continue
 				}
-				if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
+
+				isDocumentation = attributeToBool(attrs, "linguist-documentation")
+				if isDocumentation.ValueOrDefault(false) {
+					continue
+				}
+
+				isDetectable = attributeToBool(attrs, "linguist-detectable")
+				if !isDetectable.ValueOrDefault(true) {
+					continue
+				}
+
+				hasLanguage := attributeToString(attrs, "linguist-language")
+				if hasLanguage.Value() == "" {
+					hasLanguage = attributeToString(attrs, "gitlab-language")
+					if hasLanguage.Has() {
+						language := hasLanguage.Value()
+						if idx := strings.IndexByte(language, '?'); idx >= 0 {
+							hasLanguage = optional.Some(language[:idx])
+						}
+					}
+				}
+				if hasLanguage.Value() != "" {
+					language := hasLanguage.Value()
+
 					// group languages, such as Pug -> HTML; SCSS -> CSS
 					group := enry.GetLanguageGroup(language)
 					if len(group) != 0 {
@@ -116,29 +139,14 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 					// this language will always be added to the size
 					sizes[language] += f.Size()
 					continue
-				} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
-					// strip off a ? if present
-					if idx := strings.IndexByte(language, '?'); idx >= 0 {
-						language = language[:idx]
-					}
-					if len(language) != 0 {
-						// group languages, such as Pug -> HTML; SCSS -> CSS
-						group := enry.GetLanguageGroup(language)
-						if len(group) != 0 {
-							language = group
-						}
-
-						// this language will always be added to the size
-						sizes[language] += f.Size()
-						continue
-					}
 				}
-
 			}
 		}
 
-		if (!notVendored && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) ||
-			enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) {
+		if (!isVendored.Has() && analyze.IsVendor(f.Name())) ||
+			enry.IsDotFile(f.Name()) ||
+			(!isDocumentation.Has() && enry.IsDocumentation(f.Name())) ||
+			enry.IsConfiguration(f.Name()) {
 			continue
 		}
 
@@ -170,7 +178,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 				return nil, err
 			}
 		}
-		if !notGenerated && enry.IsGenerated(f.Name(), content) {
+		if !isGenerated.Has() && enry.IsGenerated(f.Name(), content) {
 			continue
 		}
 
@@ -193,13 +201,12 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			included = langType == enry.Programming || langType == enry.Markup
 			includedLanguage[language] = included
 		}
-		if included {
+		if included || isDetectable.ValueOrDefault(false) {
 			sizes[language] += f.Size()
 		} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
 			firstExcludedLanguage = language
 			firstExcludedLanguageSize += f.Size()
 		}
-		continue
 	}
 
 	// If there are no included languages add the first excluded language
diff --git a/tests/integration/linguist_test.go b/tests/integration/linguist_test.go
new file mode 100644
index 0000000000..e569de93a8
--- /dev/null
+++ b/tests/integration/linguist_test.go
@@ -0,0 +1,259 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"context"
+	"net/url"
+	"strings"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/indexer/stats"
+	"code.gitea.io/gitea/modules/queue"
+	repo_service "code.gitea.io/gitea/services/repository"
+	files_service "code.gitea.io/gitea/services/repository/files"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLinguist(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, _ *url.URL) {
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+		cppContent := "#include <iostream>\nint main() {\nstd::cout << \"Hello Gitea!\";\nreturn 0;\n}"
+		pyContent := "print(\"Hello Gitea!\")"
+		phpContent := "<?php\necho 'Hallo Welt';\n?>"
+		lockContent := "# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand."
+		mdContent := "markdown"
+
+		cases := []struct {
+			GitAttributesContent  string
+			FilesToAdd            []*files_service.ChangeRepoFile
+			ExpectedLanguageOrder []string
+		}{
+			// case 0
+			{
+				ExpectedLanguageOrder: []string{},
+			},
+			// case 1
+			{
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "python.py",
+						ContentReader: strings.NewReader(pyContent),
+					},
+					{
+						TreePath:      "php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"C++", "PHP", "Python"},
+			},
+			// case 2
+			{
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      ".cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "python.py",
+						ContentReader: strings.NewReader(pyContent),
+					},
+					{
+						TreePath:      "vendor/php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Python"},
+			},
+			// case 3
+			{
+				GitAttributesContent: "*.cpp linguist-language=Go",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Go"},
+			},
+			// case 4
+			{
+				GitAttributesContent: "*.cpp gitlab-language=Go?parent=json",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Go"},
+			},
+			// case 5
+			{
+				GitAttributesContent: "*.cpp linguist-language=HTML gitlab-language=Go?parent=json",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"HTML"},
+			},
+			// case 6
+			{
+				GitAttributesContent: "vendor/** linguist-vendored=false",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "vendor/php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"PHP"},
+			},
+			// case 7
+			{
+				GitAttributesContent: "*.cpp linguist-vendored=true\n*.py linguist-vendored\nvendor/** -linguist-vendored",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "python.py",
+						ContentReader: strings.NewReader(pyContent),
+					},
+					{
+						TreePath:      "vendor/php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"PHP"},
+			},
+			// case 8
+			{
+				GitAttributesContent: "poetry.lock linguist-language=Go",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "poetry.lock",
+						ContentReader: strings.NewReader(lockContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Go"},
+			},
+			// case 9
+			{
+				GitAttributesContent: "poetry.lock linguist-generated=false",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "poetry.lock",
+						ContentReader: strings.NewReader(lockContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"TOML"},
+			},
+			// case 10
+			{
+				GitAttributesContent: "*.cpp -linguist-detectable",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{},
+			},
+			// case 11
+			{
+				GitAttributesContent: "*.md linguist-detectable",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "test.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Markdown"},
+			},
+			// case 12
+			{
+				GitAttributesContent: "test2.md linguist-detectable",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "test.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+					{
+						TreePath:      "test2.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"C++", "Markdown"},
+			},
+			// case 13
+			{
+				GitAttributesContent: "README.md linguist-documentation=false",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Markdown"},
+			},
+		}
+
+		for i, c := range cases {
+			repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+				Name: "linguist-test",
+			})
+			assert.NoError(t, err)
+
+			files := []*files_service.ChangeRepoFile{
+				{
+					TreePath:      ".gitattributes",
+					ContentReader: strings.NewReader(c.GitAttributesContent),
+				},
+			}
+			files = append(files, c.FilesToAdd...)
+			for _, f := range files {
+				f.Operation = "create"
+			}
+
+			_, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
+				Files:     files,
+				OldBranch: repo.DefaultBranch,
+				NewBranch: repo.DefaultBranch,
+			})
+			assert.NoError(t, err)
+
+			assert.NoError(t, stats.UpdateRepoIndexer(repo))
+			assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 10*time.Second))
+
+			stats, err := repo_model.GetTopLanguageStats(db.DefaultContext, repo, len(c.FilesToAdd))
+			assert.NoError(t, err)
+
+			languages := make([]string, 0, len(stats))
+			for _, s := range stats {
+				languages = append(languages, s.Language)
+			}
+			assert.Equal(t, c.ExpectedLanguageOrder, languages, "case %d: unexpected language stats", i)
+
+			assert.NoError(t, repo_service.DeleteRepository(db.DefaultContext, user, repo, false))
+		}
+	})
+}

From b762a1f1b1f7941a7db2207552d7b441d868cbe9 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Sat, 24 Feb 2024 01:49:46 +0800
Subject: [PATCH 138/679] Fix tarball/zipball download bug (#29342)

Fix #29249

~~Use the `/repos/{owner}/{repo}/archive/{archive}` API to download.~~

Apply #26430 to archive download URLs.
---
 services/auth/auth.go   | 5 +++++
 services/auth/oauth2.go | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/services/auth/auth.go b/services/auth/auth.go
index 6746dc2a54..7c07dc438e 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -40,6 +40,7 @@ func isContainerPath(req *http.Request) bool {
 var (
 	gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`)
 	lfsPathRe            = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
+	archivePathRe        = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/archive/`)
 )
 
 func isGitRawOrAttachPath(req *http.Request) bool {
@@ -56,6 +57,10 @@ func isGitRawOrAttachOrLFSPath(req *http.Request) bool {
 	return false
 }
 
+func isArchivePath(req *http.Request) bool {
+	return archivePathRe.MatchString(req.URL.Path)
+}
+
 // handleSignIn clears existing session variables and stores new ones for the specified user object
 func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
 	// We need to regenerate the session...
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index f2f7858a85..46d8510143 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -133,7 +133,7 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
 func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
 	// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
 	if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
-		!isGitRawOrAttachPath(req) {
+		!isGitRawOrAttachPath(req) && !isArchivePath(req) {
 		return nil, nil
 	}
 

From 12d233faf786a54579a33b99b3cd56586c279f56 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 23 Feb 2024 23:19:54 +0200
Subject: [PATCH 139/679] Remove jQuery from the stopwatch (#29351)

- Switched to plain JavaScript
- Tested the stopwatch functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/c8e9a401-45e5-4a1d-a683-0d655f1d570e)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/stopwatch.js | 38 ++++++++++++++++++--------------
 1 file changed, 21 insertions(+), 17 deletions(-)

diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
index f43014fec5..e7e20e5212 100644
--- a/web_src/js/features/stopwatch.js
+++ b/web_src/js/features/stopwatch.js
@@ -1,8 +1,9 @@
-import $ from 'jquery';
 import prettyMilliseconds from 'pretty-ms';
 import {createTippy} from '../modules/tippy.js';
+import {GET} from '../modules/fetch.js';
+import {hideElem, showElem} from '../utils/dom.js';
 
-const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
+const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
 
 export function initStopwatch() {
   if (!enableTimeTracking) {
@@ -28,7 +29,7 @@ export function initStopwatch() {
   });
 
   // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
-  const currSeconds = $('.stopwatch-time').attr('data-seconds');
+  const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds');
   if (currSeconds) {
     updateStopwatchTime(currSeconds);
   }
@@ -112,29 +113,31 @@ async function updateStopwatchWithCallback(callback, timeout) {
 }
 
 async function updateStopwatch() {
-  const data = await $.ajax({
-    type: 'GET',
-    url: `${appSubUrl}/user/stopwatches`,
-    headers: {'X-Csrf-Token': csrfToken},
-  });
+  const response = await GET(`${appSubUrl}/user/stopwatches`);
+  if (!response.ok) {
+    console.error('Failed to fetch stopwatch data');
+    return false;
+  }
+  const data = await response.json();
   return updateStopwatchData(data);
 }
 
 function updateStopwatchData(data) {
   const watch = data[0];
-  const btnEl = $('.active-stopwatch-trigger');
+  const btnEl = document.querySelector('.active-stopwatch-trigger');
   if (!watch) {
     clearStopwatchTimer();
-    btnEl.addClass('gt-hidden');
+    hideElem(btnEl);
   } else {
     const {repo_owner_name, repo_name, issue_index, seconds} = watch;
     const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
-    $('.stopwatch-link').attr('href', issueUrl);
-    $('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`);
-    $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`);
-    $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`);
+    document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl);
+    document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/toggle`);
+    document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
+    const stopwatchIssue = document.querySelector('.stopwatch-issue');
+    if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
     updateStopwatchTime(seconds);
-    btnEl.removeClass('gt-hidden');
+    showElem(btnEl);
   }
   return Boolean(data.length);
 }
@@ -151,12 +154,13 @@ function updateStopwatchTime(seconds) {
   if (!Number.isFinite(secs)) return;
 
   clearStopwatchTimer();
-  const $stopwatch = $('.stopwatch-time');
+  const stopwatch = document.querySelector('.stopwatch-time');
+  // TODO: replace with <relative-time> similar to how system status up time is shown
   const start = Date.now();
   const updateUi = () => {
     const delta = Date.now() - start;
     const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
-    $stopwatch.text(dur);
+    if (stopwatch) stopwatch.textContent = dur;
   };
   updateUi();
   updateTimeIntervalId = setInterval(updateUi, 1000);

From 53c7d8908e5ef35818b72b8c3d873b509269bc1a Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Fri, 23 Feb 2024 22:51:46 +0100
Subject: [PATCH 140/679] Make optional.Option[T] type serializable (#29282)

make the generic `Option` type de-/serializable for json and yaml

---------

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 modules/optional/option_test.go        |  22 +--
 modules/optional/serialization.go      |  46 ++++++
 modules/optional/serialization_test.go | 190 +++++++++++++++++++++++++
 3 files changed, 248 insertions(+), 10 deletions(-)
 create mode 100644 modules/optional/serialization.go
 create mode 100644 modules/optional/serialization_test.go

diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go
index bfc4577dbe..410fd73577 100644
--- a/modules/optional/option_test.go
+++ b/modules/optional/option_test.go
@@ -1,47 +1,49 @@
 // Copyright 2024 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package optional
+package optional_test
 
 import (
 	"testing"
 
+	"code.gitea.io/gitea/modules/optional"
+
 	"github.com/stretchr/testify/assert"
 )
 
 func TestOption(t *testing.T) {
-	var uninitialized Option[int]
+	var uninitialized optional.Option[int]
 	assert.False(t, uninitialized.Has())
 	assert.Equal(t, int(0), uninitialized.Value())
 	assert.Equal(t, int(1), uninitialized.ValueOrDefault(1))
 
-	none := None[int]()
+	none := optional.None[int]()
 	assert.False(t, none.Has())
 	assert.Equal(t, int(0), none.Value())
 	assert.Equal(t, int(1), none.ValueOrDefault(1))
 
-	some := Some[int](1)
+	some := optional.Some[int](1)
 	assert.True(t, some.Has())
 	assert.Equal(t, int(1), some.Value())
 	assert.Equal(t, int(1), some.ValueOrDefault(2))
 
 	var ptr *int
-	assert.False(t, FromPtr(ptr).Has())
+	assert.False(t, optional.FromPtr(ptr).Has())
 
 	int1 := 1
-	opt1 := FromPtr(&int1)
+	opt1 := optional.FromPtr(&int1)
 	assert.True(t, opt1.Has())
 	assert.Equal(t, int(1), opt1.Value())
 
-	assert.False(t, FromNonDefault("").Has())
+	assert.False(t, optional.FromNonDefault("").Has())
 
-	opt2 := FromNonDefault("test")
+	opt2 := optional.FromNonDefault("test")
 	assert.True(t, opt2.Has())
 	assert.Equal(t, "test", opt2.Value())
 
-	assert.False(t, FromNonDefault(0).Has())
+	assert.False(t, optional.FromNonDefault(0).Has())
 
-	opt3 := FromNonDefault(1)
+	opt3 := optional.FromNonDefault(1)
 	assert.True(t, opt3.Has())
 	assert.Equal(t, int(1), opt3.Value())
 }
diff --git a/modules/optional/serialization.go b/modules/optional/serialization.go
new file mode 100644
index 0000000000..6688e78cd1
--- /dev/null
+++ b/modules/optional/serialization.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional
+
+import (
+	"code.gitea.io/gitea/modules/json"
+
+	"gopkg.in/yaml.v3"
+)
+
+func (o *Option[T]) UnmarshalJSON(data []byte) error {
+	var v *T
+	if err := json.Unmarshal(data, &v); err != nil {
+		return err
+	}
+	*o = FromPtr(v)
+	return nil
+}
+
+func (o Option[T]) MarshalJSON() ([]byte, error) {
+	if !o.Has() {
+		return []byte("null"), nil
+	}
+
+	return json.Marshal(o.Value())
+}
+
+func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error {
+	var v *T
+	if err := value.Decode(&v); err != nil {
+		return err
+	}
+	*o = FromPtr(v)
+	return nil
+}
+
+func (o Option[T]) MarshalYAML() (interface{}, error) {
+	if !o.Has() {
+		return nil, nil
+	}
+
+	value := new(yaml.Node)
+	err := value.Encode(o.Value())
+	return value, err
+}
diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go
new file mode 100644
index 0000000000..09a4bddea0
--- /dev/null
+++ b/modules/optional/serialization_test.go
@@ -0,0 +1,190 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional_test
+
+import (
+	std_json "encoding/json" //nolint:depguard
+	"testing"
+
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/optional"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v3"
+)
+
+type testSerializationStruct struct {
+	NormalString string                  `json:"normal_string" yaml:"normal_string"`
+	NormalBool   bool                    `json:"normal_bool" yaml:"normal_bool"`
+	OptBool      optional.Option[bool]   `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
+	OptString    optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
+	OptTwoBool   optional.Option[bool]   `json:"optional_two_bool" yaml:"optional_two_bool"`
+	OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"`
+}
+
+func TestOptionalToJson(t *testing.T) {
+	tests := []struct {
+		name string
+		obj  *testSerializationStruct
+		want string
+	}{
+		{
+			name: "empty",
+			obj:  new(testSerializationStruct),
+			want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`,
+		},
+		{
+			name: "some",
+			obj: &testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+				OptTwoBool:   optional.None[bool](),
+				OptTwoString: optional.None[string](),
+			},
+			want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			b, err := json.Marshal(tc.obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, string(b), "gitea json module returned unexpected")
+
+			b, err = std_json.Marshal(tc.obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, string(b), "std json module returned unexpected")
+		})
+	}
+}
+
+func TestOptionalFromJson(t *testing.T) {
+	tests := []struct {
+		name string
+		data string
+		want testSerializationStruct
+	}{
+		{
+			name: "empty",
+			data: `{}`,
+			want: testSerializationStruct{
+				NormalString: "",
+			},
+		},
+		{
+			name: "some",
+			data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
+			want: testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+			},
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			var obj1 testSerializationStruct
+			err := json.Unmarshal([]byte(tc.data), &obj1)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, obj1, "gitea json module returned unexpected")
+
+			var obj2 testSerializationStruct
+			err = std_json.Unmarshal([]byte(tc.data), &obj2)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, obj2, "std json module returned unexpected")
+		})
+	}
+}
+
+func TestOptionalToYaml(t *testing.T) {
+	tests := []struct {
+		name string
+		obj  *testSerializationStruct
+		want string
+	}{
+		{
+			name: "empty",
+			obj:  new(testSerializationStruct),
+			want: `normal_string: ""
+normal_bool: false
+optional_two_bool: null
+optional_two_string: null
+`,
+		},
+		{
+			name: "some",
+			obj: &testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+			},
+			want: `normal_string: a string
+normal_bool: true
+optional_bool: false
+optional_string: ""
+optional_two_bool: null
+optional_two_string: null
+`,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			b, err := yaml.Marshal(tc.obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, string(b), "yaml module returned unexpected")
+		})
+	}
+}
+
+func TestOptionalFromYaml(t *testing.T) {
+	tests := []struct {
+		name string
+		data string
+		want testSerializationStruct
+	}{
+		{
+			name: "empty",
+			data: ``,
+			want: testSerializationStruct{},
+		},
+		{
+			name: "empty but init",
+			data: `normal_string: ""
+normal_bool: false
+optional_bool:
+optional_two_bool:
+optional_two_string:
+`,
+			want: testSerializationStruct{},
+		},
+		{
+			name: "some",
+			data: `
+normal_string: a string
+normal_bool: true
+optional_bool: false
+optional_string: ""
+optional_two_bool: null
+optional_twostring: null
+`,
+			want: testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+			},
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			var obj testSerializationStruct
+			err := yaml.Unmarshal([]byte(tc.data), &obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, obj, "yaml module returned unexpected")
+		})
+	}
+}

From 08c1926e1c3e2487f207b5f225d8b0f2831d0708 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 23 Feb 2024 23:07:27 +0100
Subject: [PATCH 141/679] Refactor generate-svg.js (#29348)

Small refactor to avoid `process` global and to sync it with
`generate-images`.
---
 build/generate-svg.js | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/build/generate-svg.js b/build/generate-svg.js
index 2c0a5e37ba..f26b60d960 100755
--- a/build/generate-svg.js
+++ b/build/generate-svg.js
@@ -4,15 +4,16 @@ import {optimize} from 'svgo';
 import {parse} from 'node:path';
 import {readFile, writeFile, mkdir} from 'node:fs/promises';
 import {fileURLToPath} from 'node:url';
+import {exit} from 'node:process';
 
 const glob = (pattern) => fastGlob.sync(pattern, {
   cwd: fileURLToPath(new URL('..', import.meta.url)),
   absolute: true,
 });
 
-function exit(err) {
+function doExit(err) {
   if (err) console.error(err);
-  process.exit(err ? 1 : 0);
+  exit(err ? 1 : 0);
 }
 
 async function processFile(file, {prefix, fullName} = {}) {
@@ -64,7 +65,7 @@ async function main() {
 }
 
 try {
-  exit(await main());
+  doExit(await main());
 } catch (err) {
-  exit(err);
+  doExit(err);
 }

From 6f6120dfa8d549d0b866eeb9317054fea831c844 Mon Sep 17 00:00:00 2001
From: Carlos Felgueiras <carlosfelgueiras@tecnico.ulisboa.pt>
Date: Sat, 24 Feb 2024 00:02:14 +0100
Subject: [PATCH 142/679] Fix validity of the FROM email address not being
 checked (#29347)

Fixes #27188.
Introduces a check on the installation that tries to parse the FROM
address. If it fails, shows a new error message to the user.

---------

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 options/locale/locale_en-US.ini | 1 +
 routers/install/install.go      | 6 ++++++
 2 files changed, 7 insertions(+)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index ae34d72e41..31dbabe874 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -247,6 +247,7 @@ email_title = Email Settings
 smtp_addr = SMTP Host
 smtp_port = SMTP Port
 smtp_from = Send Email As
+smtp_from_invalid = The "Send Email As" address is invalid
 smtp_from_helper = Email address Gitea will use. Enter a plain email address or use the "Name" <email@example.com> format.
 mailer_user = SMTP Username
 mailer_password = SMTP Password
diff --git a/routers/install/install.go b/routers/install/install.go
index 78372669f4..decf74cecb 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -7,6 +7,7 @@ package install
 import (
 	"fmt"
 	"net/http"
+	"net/mail"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -419,6 +420,11 @@ func SubmitInstall(ctx *context.Context) {
 	}
 
 	if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
+		if _, err := mail.ParseAddress(form.SMTPFrom); err != nil {
+			ctx.RenderWithErr(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form)
+			return
+		}
+
 		cfg.Section("mailer").Key("ENABLED").SetValue("true")
 		cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr)
 		cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort)

From 875f5ea6d83c8371f309df99654ca3556623004c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= <sahin@sahinakkaya.dev>
Date: Sat, 24 Feb 2024 02:41:24 +0300
Subject: [PATCH 143/679] Implement code frequency graph (#29191)

### Overview
This is the implementation of Code Frequency page. This feature was
mentioned on these issues: #18262, #7392.


It adds another tab to Activity page called Code Frequency. Code
Frequency tab shows additions and deletions over time since the
repository existed.


Before:
<img width="1296" alt="image"
src="https://github.com/go-gitea/gitea/assets/32161460/2603504f-aee7-4929-a8c4-fb3412a7a0f6">

After:
<img width="1296" alt="image"
src="https://github.com/go-gitea/gitea/assets/32161460/58c03721-729f-4536-a663-9f337f240963">

---


#### Features
- See additions deletions over time since repository existed
- Click on "Additions" or "Deletions" legend to show only one type of
contribution
- Use the same cache from Contributors page so that the loading of data
will be fast once it is cached by visiting either one of the pages

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 options/locale/locale_en-US.ini             |   2 +
 routers/web/repo/code_frequency.go          |  41 +++++
 routers/web/web.go                          |   4 +
 services/repository/contributors_graph.go   |   2 -
 templates/repo/activity.tmpl                |   1 +
 templates/repo/code_frequency.tmpl          |   9 +
 templates/repo/navbar.tmpl                  |   3 +
 web_src/js/components/RepoCodeFrequency.vue | 172 ++++++++++++++++++++
 web_src/js/components/RepoContributors.vue  |  36 +---
 web_src/js/features/code-frequency.js       |  21 +++
 web_src/js/index.js                         |   2 +
 web_src/js/utils.js                         |   2 +
 web_src/js/utils/color.js                   |  14 ++
 13 files changed, 277 insertions(+), 32 deletions(-)
 create mode 100644 routers/web/repo/code_frequency.go
 create mode 100644 templates/repo/code_frequency.tmpl
 create mode 100644 web_src/js/components/RepoCodeFrequency.vue
 create mode 100644 web_src/js/features/code-frequency.js

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 31dbabe874..b35672eac2 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1919,6 +1919,7 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend
 activity = Activity
 activity.navbar.pulse = Pulse
 activity.navbar.contributors = Contributors
+activity.navbar.code_frequency = Code Frequency
 activity.period.filter_label = Period:
 activity.period.daily = 1 day
 activity.period.halfweekly = 3 days
@@ -2597,6 +2598,7 @@ component_loading = Loading %s...
 component_loading_failed = Could not load %s
 component_loading_info = This might take a bit…
 component_failed_to_load = An unexpected error happened.
+code_frequency.what = code frequency
 contributors.what = contributions
 
 [org]
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
new file mode 100644
index 0000000000..48ade655b7
--- /dev/null
+++ b/routers/web/repo/code_frequency.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplCodeFrequency base.TplName = "repo/activity"
+)
+
+// CodeFrequency renders the page to show repository code frequency
+func CodeFrequency(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency")
+
+	ctx.Data["PageIsActivity"] = true
+	ctx.Data["PageIsCodeFrequency"] = true
+	ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+	ctx.HTML(http.StatusOK, tplCodeFrequency)
+}
+
+// CodeFrequencyData returns JSON of code frequency data
+func CodeFrequencyData(ctx *context.Context) {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+			ctx.Status(http.StatusAccepted)
+			return
+		}
+		ctx.ServerError("GetCodeFrequencyData", err)
+	} else {
+		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+	}
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 77c8319f06..5e18aac67d 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1403,6 +1403,10 @@ func registerRoutes(m *web.Route) {
 				m.Get("", repo.Contributors)
 				m.Get("/data", repo.ContributorsData)
 			})
+			m.Group("/code-frequency", func() {
+				m.Get("", repo.CodeFrequency)
+				m.Get("/data", repo.CodeFrequencyData)
+			})
 		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
 
 		m.Group("/activity_author_data", func() {
diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
index 8421df8e3a..7c9f535ae0 100644
--- a/services/repository/contributors_graph.go
+++ b/services/repository/contributors_graph.go
@@ -143,7 +143,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
 		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
 			_ = stdoutWriter.Close()
 			scanner := bufio.NewScanner(stdoutReader)
-			scanner.Split(bufio.ScanLines)
 
 			for scanner.Scan() {
 				line := strings.TrimSpace(scanner.Text())
@@ -180,7 +179,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
 					}
 				}
 				commitStats.Total = commitStats.Additions + commitStats.Deletions
-				scanner.Scan()
 				scanner.Text() // empty line at the end
 
 				res := &ExtendedCommitStats{
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index 960083d2fb..94f52b0e26 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -8,6 +8,7 @@
 		<div class="flex-container-main">
 			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
 			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
+			{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/code_frequency.tmpl b/templates/repo/code_frequency.tmpl
new file mode 100644
index 0000000000..50ec1beb6b
--- /dev/null
+++ b/templates/repo/code_frequency.tmpl
@@ -0,0 +1,9 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+	<div id="repo-code-frequency-chart"
+		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.code_frequency.what")}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.code_frequency.what")}}"
+		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
+		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
+	>
+	</div>
+{{end}}
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
index a9042ee30d..aa5021e73a 100644
--- a/templates/repo/navbar.tmpl
+++ b/templates/repo/navbar.tmpl
@@ -5,4 +5,7 @@
 	<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
 		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
 	</a>
+	<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
+		{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
+	</a>
 </div>
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
new file mode 100644
index 0000000000..ad607a041a
--- /dev/null
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -0,0 +1,172 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+  Chart,
+  Legend,
+  LinearScale,
+  TimeScale,
+  PointElement,
+  LineElement,
+  Filler,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import {Line as ChartLine} from 'vue-chartjs';
+import {
+  startDaysBetween,
+  firstStartDateAfterDate,
+  fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+
+const {pageData} = window.config;
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+  TimeScale,
+  LinearScale,
+  Legend,
+  PointElement,
+  LineElement,
+  Filler,
+);
+
+export default {
+  components: {ChartLine, SvgIcon},
+  props: {
+    locale: {
+      type: Object,
+      required: true
+    },
+  },
+  data: () => ({
+    isLoading: false,
+    errorText: '',
+    repoLink: pageData.repoLink || [],
+    data: [],
+  }),
+  mounted() {
+    this.fetchGraphData();
+  },
+  methods: {
+    async fetchGraphData() {
+      this.isLoading = true;
+      try {
+        let response;
+        do {
+          response = await GET(`${this.repoLink}/activity/code-frequency/data`);
+          if (response.status === 202) {
+            await sleep(1000); // wait for 1 second before retrying
+          }
+        } while (response.status === 202);
+        if (response.ok) {
+          this.data = await response.json();
+          const weekValues = Object.values(this.data);
+          const start = weekValues[0].week;
+          const end = firstStartDateAfterDate(new Date());
+          const startDays = startDaysBetween(new Date(start), new Date(end));
+          this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
+          this.errorText = '';
+        } else {
+          this.errorText = response.statusText;
+        }
+      } catch (err) {
+        this.errorText = err.message;
+      } finally {
+        this.isLoading = false;
+      }
+    },
+
+    toGraphData(data) {
+      return {
+        datasets: [
+          {
+            data: data.map((i) => ({x: i.week, y: i.additions})),
+            pointRadius: 0,
+            pointHitRadius: 0,
+            fill: true,
+            label: 'Additions',
+            backgroundColor: chartJsColors['additions'],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+          {
+            data: data.map((i) => ({x: i.week, y: -i.deletions})),
+            pointRadius: 0,
+            pointHitRadius: 0,
+            fill: true,
+            label: 'Deletions',
+            backgroundColor: chartJsColors['deletions'],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+        ],
+      };
+    },
+
+    getOptions() {
+      return {
+        responsive: true,
+        maintainAspectRatio: false,
+        animation: true,
+        plugins: {
+          legend: {
+            display: true,
+          },
+        },
+        scales: {
+          x: {
+            type: 'time',
+            grid: {
+              display: false,
+            },
+            time: {
+              minUnit: 'month',
+            },
+            ticks: {
+              maxRotation: 0,
+              maxTicksLimit: 12
+            },
+          },
+          y: {
+            ticks: {
+              maxTicksLimit: 6
+            },
+          },
+        },
+      };
+    },
+  },
+};
+</script>
+<template>
+  <div>
+    <div class="ui header gt-df gt-ac gt-sb">
+      {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
+    </div>
+    <div class="gt-df ui segment main-graph">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
+        <div v-if="isLoading">
+          <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
+          {{ locale.loadingInfo }}
+        </div>
+        <div v-else class="text red">
+          <SvgIcon name="octicon-x-circle-fill"/>
+          {{ errorText }}
+        </div>
+      </div>
+      <ChartLine
+        v-memo="data" v-if="data.length !== 0"
+        :data="toGraphData(data)" :options="getOptions()"
+      />
+    </div>
+  </div>
+</template>
+<style scoped>
+.main-graph {
+  height: 440px;
+}
+</style>
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index fa1545b3df..84fdcae1f6 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -3,10 +3,7 @@ import {SvgIcon} from '../svg.js';
 import {
   Chart,
   Title,
-  Tooltip,
-  Legend,
   BarElement,
-  CategoryScale,
   LinearScale,
   TimeScale,
   PointElement,
@@ -21,27 +18,13 @@ import {
   firstStartDateAfterDate,
   fillEmptyStartDaysWithZeroes,
 } from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
 import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
 import $ from 'jquery';
 
 const {pageData} = window.config;
 
-const colors = {
-  text: '--color-text',
-  border: '--color-secondary-alpha-60',
-  commits: '--color-primary-alpha-60',
-  additions: '--color-green',
-  deletions: '--color-red',
-  title: '--color-secondary-dark-4',
-};
-
-const styles = window.getComputedStyle(document.documentElement);
-const getColor = (name) => styles.getPropertyValue(name).trim();
-
-for (const [key, value] of Object.entries(colors)) {
-  colors[key] = getColor(value);
-}
-
 const customEventListener = {
   id: 'customEventListener',
   afterEvent: (chart, args, opts) => {
@@ -54,17 +37,14 @@ const customEventListener = {
   }
 };
 
-Chart.defaults.color = colors.text;
-Chart.defaults.borderColor = colors.border;
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
 
 Chart.register(
   TimeScale,
-  CategoryScale,
   LinearScale,
   BarElement,
   Title,
-  Tooltip,
-  Legend,
   PointElement,
   LineElement,
   Filler,
@@ -122,7 +102,7 @@ export default {
         do {
           response = await GET(`${this.repoLink}/activity/contributors/data`);
           if (response.status === 202) {
-            await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second before retrying
+            await sleep(1000); // wait for 1 second before retrying
           }
         } while (response.status === 202);
         if (response.ok) {
@@ -222,7 +202,7 @@ export default {
             pointRadius: 0,
             pointHitRadius: 0,
             fill: 'start',
-            backgroundColor: colors[this.type],
+            backgroundColor: chartJsColors[this.type],
             borderWidth: 0,
             tension: 0.3,
           },
@@ -254,7 +234,6 @@ export default {
           title: {
             display: type === 'main',
             text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
-            color: colors.title,
             position: 'top',
             align: 'center',
           },
@@ -262,9 +241,6 @@ export default {
             chartType: type,
             instance: this,
           },
-          legend: {
-            display: false,
-          },
           zoom: {
             pan: {
               enabled: true,
diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js
new file mode 100644
index 0000000000..103d82f6e3
--- /dev/null
+++ b/web_src/js/features/code-frequency.js
@@ -0,0 +1,21 @@
+import {createApp} from 'vue';
+
+export async function initRepoCodeFrequency() {
+  const el = document.getElementById('repo-code-frequency-chart');
+  if (!el) return;
+
+  const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue');
+  try {
+    const View = createApp(RepoCodeFrequency, {
+      locale: {
+        loadingTitle: el.getAttribute('data-locale-loading-title'),
+        loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+        loadingInfo: el.getAttribute('data-locale-loading-info'),
+      }
+    });
+    View.mount(el);
+  } catch (err) {
+    console.error('RepoCodeFrequency failed to load', err);
+    el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+  }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index ddd435f05e..876e4291ee 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -87,6 +87,7 @@ import {onDomReady} from './utils/dom.js';
 import {initRepoIssueList} from './features/repo-issue-list.js';
 import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
 import {initRepoContributors} from './features/contributors.js';
+import {initRepoCodeFrequency} from './features/code-frequency.js';
 import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 import {initDirAuto} from './modules/dirauto.js';
 
@@ -177,6 +178,7 @@ onDomReady(() => {
   initRepository();
   initRepositoryActionView();
   initRepoContributors();
+  initRepoCodeFrequency();
 
   initCommitStatuses();
   initCaptcha();
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index c82e42d349..3a2694335f 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -139,3 +139,5 @@ export function parseDom(text, contentType) {
 export function serializeXml(node) {
   return xmlSerializer.serializeToString(node);
 }
+
+export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js
index 5d9c4ca45d..0ba6af49ee 100644
--- a/web_src/js/utils/color.js
+++ b/web_src/js/utils/color.js
@@ -19,3 +19,17 @@ function getLuminance(r, g, b) {
 export function useLightTextOnBackground(r, g, b) {
   return getLuminance(r, g, b) < 0.453;
 }
+
+function resolveColors(obj) {
+  const styles = window.getComputedStyle(document.documentElement);
+  const getColor = (name) => styles.getPropertyValue(name).trim();
+  return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
+}
+
+export const chartJsColors = resolveColors({
+  text: '--color-text',
+  border: '--color-secondary-alpha-60',
+  commits: '--color-primary-alpha-60',
+  additions: '--color-green',
+  deletions: '--color-red',
+});

From 4ba642d07d50d7eb42ae33cd6f1f7f2c82c02a40 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Sat, 24 Feb 2024 05:18:49 +0100
Subject: [PATCH 144/679] Revert "Support SAML authentication (#25165)"
 (#29358)

This reverts #25165 (5bb8d1924d77c675467694de26697b876d709a17), as there
was a chance some important reviews got missed.

so after reverting this patch it will be resubmitted for reviewing again

https://github.com/go-gitea/gitea/pull/25165#issuecomment-1960670242

temporary Open #5512 again
---
 .github/workflows/pull-db-tests.yml           |   8 -
 assets/go-licenses.json                       |  25 ---
 docs/content/usage/authentication.en-us.md    |  69 ------
 go.mod                                        |   5 -
 go.sum                                        |  12 --
 models/auth/oauth2.go                         |  20 +-
 models/auth/source.go                         |  38 ----
 options/locale/locale_en-US.ini               |  14 --
 routers/init.go                               |   2 -
 routers/web/admin/auths.go                    |  84 --------
 routers/web/auth/auth.go                      |  35 +--
 routers/web/auth/linkaccount.go               |  45 ++--
 routers/web/auth/oauth.go                     |  19 +-
 routers/web/auth/openid.go                    |   5 +-
 routers/web/auth/saml.go                      | 172 ---------------
 routers/web/web.go                            |   5 -
 .../auth/source/saml/assert_interface_test.go |  22 --
 services/auth/source/saml/init.go             |  29 ---
 services/auth/source/saml/name_id_format.go   |  38 ----
 services/auth/source/saml/providers.go        | 109 ----------
 services/auth/source/saml/source.go           | 202 ------------------
 .../auth/source/saml/source_authenticate.go   |  16 --
 services/auth/source/saml/source_callout.go   |  89 --------
 services/auth/source/saml/source_metadata.go  |  32 ---
 services/auth/source/saml/source_register.go  |  23 --
 services/externalaccount/link.go              |  11 +-
 services/externalaccount/user.go              |  12 +-
 services/forms/auth_form.go                   |  15 +-
 templates/admin/auth/edit.tmpl                |  66 ------
 templates/admin/auth/new.tmpl                 |   6 -
 templates/admin/auth/source/saml.tmpl         |  62 ------
 templates/user/auth/signin_inner.tmpl         |  17 --
 tests/integration/README.md                   |  17 --
 tests/integration/saml_test.go                | 150 -------------
 web_src/js/features/admin/common.js           |   8 +-
 web_src/js/features/user-auth.js              |  21 --
 web_src/js/index.js                           |   6 +-
 37 files changed, 69 insertions(+), 1440 deletions(-)
 delete mode 100644 routers/web/auth/saml.go
 delete mode 100644 services/auth/source/saml/assert_interface_test.go
 delete mode 100644 services/auth/source/saml/init.go
 delete mode 100644 services/auth/source/saml/name_id_format.go
 delete mode 100644 services/auth/source/saml/providers.go
 delete mode 100644 services/auth/source/saml/source.go
 delete mode 100644 services/auth/source/saml/source_authenticate.go
 delete mode 100644 services/auth/source/saml/source_callout.go
 delete mode 100644 services/auth/source/saml/source_metadata.go
 delete mode 100644 services/auth/source/saml/source_register.go
 delete mode 100644 templates/admin/auth/source/saml.tmpl
 delete mode 100644 tests/integration/saml_test.go

diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index 8843c6d65e..a3886bf618 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -37,14 +37,6 @@ jobs:
           MINIO_ROOT_PASSWORD: 12345678
         ports:
           - "9000:9000"
-      simplesaml:
-        image: allspice/simple-saml
-        ports:
-          - "8080:8080"
-        env:
-          SIMPLESAMLPHP_SP_ENTITY_ID: http://localhost:3002/user/saml/test-sp/metadata
-          SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: http://localhost:3002/user/saml/test-sp/acs
-          SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: http://localhost:3002/user/saml/test-sp/acs
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-go@v5
diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index ed722b0192..2aa60780c4 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -124,11 +124,6 @@
     "path": "github.com/aymerick/douceur/LICENSE",
     "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Aymerick JEHANNE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
   },
-  {
-    "name": "github.com/beevik/etree",
-    "path": "github.com/beevik/etree/LICENSE",
-    "licenseText": "Copyright 2015-2019 Brett Vickers. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n   1. Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n\n   2. Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\nOF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
-  },
   {
     "name": "github.com/beorn7/perks/quantile",
     "path": "github.com/beorn7/perks/quantile/LICENSE",
@@ -644,11 +639,6 @@
     "path": "github.com/jhillyerd/enmime/LICENSE",
     "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2012-2016 James Hillyerd, All Rights Reserved\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
   },
-  {
-    "name": "github.com/jonboulle/clockwork",
-    "path": "github.com/jonboulle/clockwork/LICENSE",
-    "licenseText": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
   {
     "name": "github.com/josharian/intern",
     "path": "github.com/josharian/intern/license.md",
@@ -729,11 +719,6 @@
     "path": "github.com/markbates/goth/LICENSE.txt",
     "licenseText": "Copyright (c) 2014 Mark Bates\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
   },
-  {
-    "name": "github.com/mattermost/xml-roundtrip-validator",
-    "path": "github.com/mattermost/xml-roundtrip-validator/LICENSE.txt",
-    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
   {
     "name": "github.com/mattn/go-colorable",
     "path": "github.com/mattn/go-colorable/LICENSE",
@@ -919,16 +904,6 @@
     "path": "github.com/rs/xid/LICENSE",
     "licenseText": "Copyright (c) 2015 Olivier Poitrey \u003crs@dailymotion.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
   },
-  {
-    "name": "github.com/russellhaering/gosaml2",
-    "path": "github.com/russellhaering/gosaml2/LICENSE",
-    "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n"
-  },
-  {
-    "name": "github.com/russellhaering/goxmldsig",
-    "path": "github.com/russellhaering/goxmldsig/LICENSE",
-    "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n"
-  },
   {
     "name": "github.com/russross/blackfriday/v2",
     "path": "github.com/russross/blackfriday/v2/LICENSE.txt",
diff --git a/docs/content/usage/authentication.en-us.md b/docs/content/usage/authentication.en-us.md
index 1838cfcc77..adc936dfbe 100644
--- a/docs/content/usage/authentication.en-us.md
+++ b/docs/content/usage/authentication.en-us.md
@@ -349,72 +349,3 @@ If set `ENABLE_REVERSE_PROXY_FULL_NAME=true`, a user full name expected in `X-WE
 You can also limit the reverse proxy's IP address range with `REVERSE_PROXY_TRUSTED_PROXIES` which default value is `127.0.0.0/8,::1/128`. By `REVERSE_PROXY_LIMIT`, you can limit trusted proxies level.
 
 Notice: Reverse Proxy Auth doesn't support the API. You still need an access token or basic auth to make API requests.
-
-## SAML
-
-### Configuring Gitea as a SAML 2.0 Service Provider
-
-- Navigate to `Site Administration > Identity & Access > Authentication Sources`.
-- Click the `Add Authentication Source` button.
-- Select `SAML` as the authentication type.
-
-#### Features Not Yet Supported
-
-Currently, auto-registration is not supported for SAML. During the external account linking process the user will be prompted to set a username and email address or link to an existing account.
-
-SAML group mapping is not supported.
-
-#### Settings
-
-- `Authentication Name` **(required)**
-
-  - The name of this authentication source (appears in the Gitea ACS and metadata URLs)
-
-- `SAML NameID Format` **(required)**
-
-  - This specifies how Identity Provider (IdP) users are mapped to Gitea users. This option will be provider specific.
-
-- `Icon URL` (optional)
-
-  - URL of an icon to display on the Sign-In page for this authentication source.
-
-- `[Insecure] Skip Assertion Signature Validation` (optional)
-
-  - This option is not recommended and disables integrity verification of IdP SAML assertions.
-
-- `Identity Provider Metadata URL` (optional if XML set)
-
-  - The URL of the IdP metadata endpoint.
-  - This field must be set if `Identity Provider Metadata XML` is left blank.
-
-- `Identity Provider Metadata XML` (optional if URL set)
-
-  - The XML returned by the IdP metadata endpoint.
-  - This field must be set if `Identity Provider Metadata URL` is left blank.
-
-- `Service Provider Certificate` (optional)
-
-  - X.509-formatted certificate (with `Service Provider Private Key`) used for signing SAML requests.
-  - A certificate will be generated if this field is left blank.
-
-- `Service Provider Private Key` (optional)
-
-  - DSA/RSA private key (with `Service Provider Certificate`) used for signing SAML requests.
-  - A private key will be generated if this field is left blank.
-
-- `Email Assertion Key` (optional)
-
-  - The SAML assertion key used for the IdP user's email (depends on provider configuration).
-
-- `Name Assertion Key` (optional)
-
-  - The SAML assertion key used for the IdP user's nickname (depends on provider configuration).
-
-- `Username Assertion Key` (optional)
-
-  - The SAML assertion key used for the IdP user's username (depends on provider configuration).
-
-### Configuring a SAML 2.0 Identity Provider to use Gitea
-
-- The service provider assertion consumer service url will look like: `http(s)://[mydomain]/user/saml/[Authentication Name]/acs`.
-- The service provider metadata url will look like: `http(s)://[mydomain]/user/saml/[Authentication Name]/metadata`.
diff --git a/go.mod b/go.mod
index 012a34612f..7a752ec874 100644
--- a/go.mod
+++ b/go.mod
@@ -91,8 +91,6 @@ require (
 	github.com/quasoft/websspi v1.1.2
 	github.com/redis/go-redis/v9 v9.4.0
 	github.com/robfig/cron/v3 v3.0.1
-	github.com/russellhaering/gosaml2 v0.9.1
-	github.com/russellhaering/goxmldsig v1.3.0
 	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
 	github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd
 	github.com/sergi/go-diff v1.3.1
@@ -145,7 +143,6 @@ require (
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
-	github.com/beevik/etree v1.1.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bits-and-blooms/bitset v1.13.0 // indirect
 	github.com/blevesearch/bleve_index_api v1.1.5 // indirect
@@ -219,7 +216,6 @@ require (
 	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/jessevdk/go-flags v1.5.0 // indirect
-	github.com/jonboulle/clockwork v0.3.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/klauspost/pgzip v1.2.6 // indirect
@@ -229,7 +225,6 @@ require (
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/markbates/going v1.0.3 // indirect
-	github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/mholt/acmez v1.2.0 // indirect
diff --git a/go.sum b/go.sum
index 393e10cfa0..b3b8ad8ce4 100644
--- a/go.sum
+++ b/go.sum
@@ -130,8 +130,6 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
-github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
-github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bits-and-blooms/bitset v1.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE=
@@ -568,9 +566,6 @@ github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZO
 github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
-github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
-github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -639,8 +634,6 @@ github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE
 github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
 github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
 github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
-github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
-github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -773,17 +766,12 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/russellhaering/gosaml2 v0.9.1 h1:H/whrl8NuSoxyW46Ww5lKPskm+5K+qYLw9afqJ/Zef0=
-github.com/russellhaering/gosaml2 v0.9.1/go.mod h1:ja+qgbayxm+0mxBRLMSUuX3COqy+sb0RRhIGun/W2kc=
-github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM=
-github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go
index a252458d4e..9d53fffc78 100644
--- a/models/auth/oauth2.go
+++ b/models/auth/oauth2.go
@@ -8,7 +8,6 @@ import (
 	"crypto/sha256"
 	"encoding/base32"
 	"encoding/base64"
-	"encoding/gob"
 	"fmt"
 	"net"
 	"net/url"
@@ -82,10 +81,6 @@ func Init(ctx context.Context) error {
 		builtinAllClientIDs = append(builtinAllClientIDs, clientID)
 	}
 
-	// This is needed in order to encode and store the struct in the goth/gothic session
-	// during the process of linking the external user.
-	gob.Register(LinkAccountUser{})
-
 	var registeredApps []*OAuth2Application
 	if err := db.GetEngine(ctx).In("client_id", builtinAllClientIDs).Find(&registeredApps); err != nil {
 		return err
@@ -610,6 +605,21 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error {
 	return util.ErrNotExist
 }
 
+// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
+func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
+	authSource := new(Source)
+	has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
+	if err != nil {
+		return nil, err
+	}
+
+	if !has {
+		return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
+	}
+
+	return authSource, nil
+}
+
 func DeleteOAuth2RelictsByUserID(ctx context.Context, userID int64) error {
 	deleteCond := builder.Select("id").From("oauth2_grant").Where(builder.Eq{"oauth2_grant.user_id": userID})
 
diff --git a/models/auth/source.go b/models/auth/source.go
index bc564d35ba..1bdde8235c 100644
--- a/models/auth/source.go
+++ b/models/auth/source.go
@@ -14,7 +14,6 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/markbates/goth"
 	"xorm.io/builder"
 	"xorm.io/xorm"
 	"xorm.io/xorm/convert"
@@ -33,7 +32,6 @@ const (
 	DLDAP       // 5
 	OAuth2      // 6
 	SSPI        // 7
-	SAML        // 8
 )
 
 // String returns the string name of the LoginType
@@ -54,7 +52,6 @@ var Names = map[Type]string{
 	PAM:    "PAM",
 	OAuth2: "OAuth2",
 	SSPI:   "SPNEGO with SSPI",
-	SAML:   "SAML",
 }
 
 // Config represents login config as far as the db is concerned
@@ -124,12 +121,6 @@ type Source struct {
 	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 }
 
-// LinkAccountUser is used to link an external user with a local user
-type LinkAccountUser struct {
-	Type     Type
-	GothUser goth.User
-}
-
 // TableName xorm will read the table name from this method
 func (Source) TableName() string {
 	return "login_source"
@@ -189,11 +180,6 @@ func (source *Source) IsSSPI() bool {
 	return source.Type == SSPI
 }
 
-// IsSAML returns true of this source is of the SAML type.
-func (source *Source) IsSAML() bool {
-	return source.Type == SAML
-}
-
 // HasTLS returns true of this source supports TLS.
 func (source *Source) HasTLS() bool {
 	hasTLSer, ok := source.Cfg.(HasTLSer)
@@ -406,27 +392,3 @@ func IsErrSourceInUse(err error) bool {
 func (err ErrSourceInUse) Error() string {
 	return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID)
 }
-
-// GetActiveAuthProviderSources returns all activated sources
-func GetActiveAuthProviderSources(ctx context.Context, authType Type) ([]*Source, error) {
-	sources := make([]*Source, 0, 1)
-	if err := db.GetEngine(ctx).Where("is_active = ? and type = ?", true, authType).Find(&sources); err != nil {
-		return nil, err
-	}
-	return sources, nil
-}
-
-// GetActiveAuthSourceByName returns an AuthSource based on the given name and type
-func GetActiveAuthSourceByName(ctx context.Context, name string, authType Type) (*Source, error) {
-	authSource := new(Source)
-	has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, authType, true).Get(authSource)
-	if err != nil {
-		return nil, err
-	}
-
-	if !has {
-		return nil, fmt.Errorf("auth source not found, name: %q", name)
-	}
-
-	return authSource, nil
-}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index b35672eac2..2c92f40a17 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -523,9 +523,6 @@ Content = Content
 SSPISeparatorReplacement = Separator
 SSPIDefaultLanguage = Default Language
 
-SAMLMetadata = Either SAML Identity Provider metadata URL or XML
-SAMLMetadataURL = SAML Identity Provider metadata URL is invalid
-
 require_error = ` cannot be empty.`
 alpha_dash_error = ` should contain only alphanumeric, dash ('-') and underscore ('_') characters.`
 alpha_dash_dot_error = ` should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.`
@@ -3032,18 +3029,7 @@ auths.sspi_separator_replacement = Separator to use instead of \, / and @
 auths.sspi_separator_replacement_helper = The character to use to replace the separators of down-level logon names (eg. the \ in "DOMAIN\user") and user principal names (eg. the @ in "user@example.org").
 auths.sspi_default_language = Default user language
 auths.sspi_default_language_helper = Default language for users automatically created by SSPI auth method. Leave empty if you prefer language to be automatically detected.
-auths.saml_nameidformat = SAML NameID Format
-auths.saml_identity_provider_metadata_url = Identity Provider Metadata URL
-auths.saml_identity_provider_metadata = Identity Provider Metadata XML
-auths.saml_insecure_skip_assertion_signature_validation = [Insecure] Skip Assertion Signature Validation
-auths.saml_service_provider_certificate = Service Provider Certificate
-auths.saml_service_provider_private_key = Service Provider Private Key
-auths.saml_identity_provider_email_assertion_key = Email Assertion Key
-auths.saml_identity_provider_name_assertion_key = Name Assertion Key
-auths.saml_identity_provider_username_assertion_key = Username Assertion Key
-auths.saml_icon_url = Icon URL
 auths.tips = Tips
-auths.tips.saml = Documentation can be found at https://docs.gitea.com/usage/authentication#saml
 auths.tips.oauth2.general = OAuth2 Authentication
 auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be:
 auths.tip.oauth2_provider = OAuth2 Provider
diff --git a/routers/init.go b/routers/init.go
index 9ae8c368a2..e0a7150ba3 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -35,7 +35,6 @@ import (
 	actions_service "code.gitea.io/gitea/services/actions"
 	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
-	"code.gitea.io/gitea/services/auth/source/saml"
 	"code.gitea.io/gitea/services/automerge"
 	"code.gitea.io/gitea/services/cron"
 	feed_service "code.gitea.io/gitea/services/feed"
@@ -139,7 +138,6 @@ func InitWebInstalled(ctx context.Context) {
 	log.Info("ORM engine initialization successful!")
 	mustInit(system.Init)
 	mustInitCtx(ctx, oauth2.Init)
-	mustInitCtx(ctx, saml.Init)
 
 	mustInit(release_service.Init)
 
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 187b569d39..7fdd18dfae 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -1,12 +1,9 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2024 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package admin
 
 import (
-	"crypto/tls"
-	"crypto/x509"
 	"errors"
 	"fmt"
 	"net/http"
@@ -28,7 +25,6 @@ import (
 	"code.gitea.io/gitea/services/auth/source/ldap"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	pam_service "code.gitea.io/gitea/services/auth/source/pam"
-	"code.gitea.io/gitea/services/auth/source/saml"
 	"code.gitea.io/gitea/services/auth/source/smtp"
 	"code.gitea.io/gitea/services/auth/source/sspi"
 	"code.gitea.io/gitea/services/forms"
@@ -75,7 +71,6 @@ var (
 			{auth.SMTP.String(), auth.SMTP},
 			{auth.OAuth2.String(), auth.OAuth2},
 			{auth.SSPI.String(), auth.SSPI},
-			{auth.SAML.String(), auth.SAML},
 		}
 		if pam.Supported {
 			items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
@@ -88,16 +83,6 @@ var (
 		{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
 		{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
 	}
-
-	nameIDFormats = []dropdownItem{
-		{saml.NameIDFormatNames[saml.SAML20Persistent], saml.SAML20Persistent}, // use this as default value
-		{saml.NameIDFormatNames[saml.SAML11Email], saml.SAML11Email},
-		{saml.NameIDFormatNames[saml.SAML11Persistent], saml.SAML11Persistent},
-		{saml.NameIDFormatNames[saml.SAML11Unspecified], saml.SAML11Unspecified},
-		{saml.NameIDFormatNames[saml.SAML20Email], saml.SAML20Email},
-		{saml.NameIDFormatNames[saml.SAML20Transient], saml.SAML20Transient},
-		{saml.NameIDFormatNames[saml.SAML20Unspecified], saml.SAML20Unspecified},
-	}
 )
 
 // NewAuthSource render adding a new auth source page
@@ -113,8 +98,6 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.Data["is_sync_enabled"] = true
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
-	ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[saml.SAML20Persistent]
-	ctx.Data["NameIDFormats"] = nameIDFormats
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	oauth2providers := oauth2.GetSupportedOAuth2Providers()
 	ctx.Data["OAuth2Providers"] = oauth2providers
@@ -248,52 +231,6 @@ func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi
 	}, nil
 }
 
-func parseSAMLConfig(ctx *context.Context, form forms.AuthenticationForm) (*saml.Source, error) {
-	if util.IsEmptyString(form.IdentityProviderMetadata) && util.IsEmptyString(form.IdentityProviderMetadataURL) {
-		return nil, fmt.Errorf("%s %s", ctx.Tr("form.SAMLMetadata"), ctx.Tr("form.require_error"))
-	}
-
-	if !util.IsEmptyString(form.IdentityProviderMetadataURL) {
-		_, err := url.Parse(form.IdentityProviderMetadataURL)
-		if err != nil {
-			return nil, fmt.Errorf("%s", ctx.Tr("form.SAMLMetadataURL"))
-		}
-	}
-
-	// check the integrity of the certificate and private key (autogenerated if these form fields are blank)
-	if !util.IsEmptyString(form.ServiceProviderCertificate) && !util.IsEmptyString(form.ServiceProviderPrivateKey) {
-		keyPair, err := tls.X509KeyPair([]byte(form.ServiceProviderCertificate), []byte(form.ServiceProviderPrivateKey))
-		if err != nil {
-			return nil, err
-		}
-		keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
-		if err != nil {
-			return nil, err
-		}
-	} else {
-		privateKey, cert, err := saml.GenerateSAMLSPKeypair()
-		if err != nil {
-			return nil, err
-		}
-
-		form.ServiceProviderPrivateKey = privateKey
-		form.ServiceProviderCertificate = cert
-	}
-
-	return &saml.Source{
-		IdentityProviderMetadata:                 form.IdentityProviderMetadata,
-		IdentityProviderMetadataURL:              form.IdentityProviderMetadataURL,
-		InsecureSkipAssertionSignatureValidation: form.InsecureSkipAssertionSignatureValidation,
-		NameIDFormat:                             saml.NameIDFormat(form.NameIDFormat),
-		ServiceProviderCertificate:               form.ServiceProviderCertificate,
-		ServiceProviderPrivateKey:                form.ServiceProviderPrivateKey,
-		EmailAssertionKey:                        form.EmailAssertionKey,
-		NameAssertionKey:                         form.NameAssertionKey,
-		UsernameAssertionKey:                     form.UsernameAssertionKey,
-		IconURL:                                  form.SAMLIconURL,
-	}, nil
-}
-
 // NewAuthSourcePost response for adding an auth source
 func NewAuthSourcePost(ctx *context.Context) {
 	form := *web.GetForm(ctx).(*forms.AuthenticationForm)
@@ -307,8 +244,6 @@ func NewAuthSourcePost(ctx *context.Context) {
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	oauth2providers := oauth2.GetSupportedOAuth2Providers()
 	ctx.Data["OAuth2Providers"] = oauth2providers
-	ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[saml.NameIDFormat(form.NameIDFormat)]
-	ctx.Data["NameIDFormats"] = nameIDFormats
 
 	ctx.Data["SSPIAutoCreateUsers"] = true
 	ctx.Data["SSPIAutoActivateUsers"] = true
@@ -355,13 +290,6 @@ func NewAuthSourcePost(ctx *context.Context) {
 			ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
 			return
 		}
-	case auth.SAML:
-		var err error
-		config, err = parseSAMLConfig(ctx, form)
-		if err != nil {
-			ctx.RenderWithErr(err.Error(), tplAuthNew, form)
-			return
-		}
 	default:
 		ctx.Error(http.StatusBadRequest)
 		return
@@ -408,7 +336,6 @@ func EditAuthSource(ctx *context.Context) {
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	oauth2providers := oauth2.GetSupportedOAuth2Providers()
 	ctx.Data["OAuth2Providers"] = oauth2providers
-	ctx.Data["NameIDFormats"] = nameIDFormats
 
 	source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
 	if err != nil {
@@ -417,9 +344,6 @@ func EditAuthSource(ctx *context.Context) {
 	}
 	ctx.Data["Source"] = source
 	ctx.Data["HasTLS"] = source.HasTLS()
-	if source.IsSAML() {
-		ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[source.Cfg.(*saml.Source).NameIDFormat]
-	}
 
 	if source.IsOAuth2() {
 		type Named interface {
@@ -454,8 +378,6 @@ func EditAuthSourcePost(ctx *context.Context) {
 	}
 	ctx.Data["Source"] = source
 	ctx.Data["HasTLS"] = source.HasTLS()
-	ctx.Data["CurrentNameIDFormat"] = saml.NameIDFormatNames[saml.SAML20Persistent]
-	ctx.Data["NameIDFormats"] = nameIDFormats
 
 	if ctx.HasError() {
 		ctx.HTML(http.StatusOK, tplAuthEdit)
@@ -490,12 +412,6 @@ func EditAuthSourcePost(ctx *context.Context) {
 			ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
 			return
 		}
-	case auth.SAML:
-		config, err = parseSAMLConfig(ctx, form)
-		if err != nil {
-			ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
-			return
-		}
 	default:
 		ctx.Error(http.StatusBadRequest)
 		return
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index f5955ec5ff..3de1f3373d 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -28,7 +28,6 @@ import (
 	"code.gitea.io/gitea/routers/utils"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
-	"code.gitea.io/gitea/services/auth/source/saml"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -171,14 +170,6 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 	ctx.Data["OAuth2Providers"] = oauth2Providers
-
-	samlProviders, err := saml.GetSAMLProviders(ctx, util.OptionalBoolTrue)
-	if err != nil {
-		ctx.ServerError("UserSignIn", err)
-		return
-	}
-	ctx.Data["SAMLProviders"] = samlProviders
-
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
 	ctx.Data["PageIsSignIn"] = true
@@ -202,14 +193,6 @@ func SignInPost(ctx *context.Context) {
 		return
 	}
 	ctx.Data["OAuth2Providers"] = oauth2Providers
-
-	samlProviders, err := saml.GetSAMLProviders(ctx, util.OptionalBoolTrue)
-	if err != nil {
-		ctx.ServerError("UserSignIn", err)
-		return
-	}
-	ctx.Data["SAMLProviders"] = samlProviders
-
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
 	ctx.Data["PageIsSignIn"] = true
@@ -521,7 +504,7 @@ func SignUpPost(ctx *context.Context) {
 		Passwd: form.Password,
 	}
 
-	if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false, auth.NoType) {
+	if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false) {
 		// error already handled
 		return
 	}
@@ -532,16 +515,16 @@ func SignUpPost(ctx *context.Context) {
 
 // createAndHandleCreatedUser calls createUserInContext and
 // then handleUserCreated.
-func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool, authType auth.Type) bool {
-	if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink, authType) {
+func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool {
+	if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) {
 		return false
 	}
-	return handleUserCreated(ctx, u, gothUser, authType)
+	return handleUserCreated(ctx, u, gothUser)
 }
 
 // createUserInContext creates a user and handles errors within a given context.
 // Optionally a template can be specified.
-func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool, authType auth.Type) (ok bool) {
+func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
 	if err := user_model.CreateUser(ctx, u, overwrites); err != nil {
 		if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
 			if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
@@ -558,10 +541,10 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
 				}
 
 				// TODO: probably we should respect 'remember' user's choice...
-				linkAccount(ctx, user, *gothUser, true, authType)
+				linkAccount(ctx, user, *gothUser, true)
 				return false // user is already created here, all redirects are handled
 			} else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
-				showLinkingLogin(ctx, *gothUser, authType)
+				showLinkingLogin(ctx, *gothUser)
 				return false // user will be created only after linking login
 			}
 		}
@@ -607,7 +590,7 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
 // handleUserCreated does additional steps after a new user is created.
 // It auto-sets admin for the only user, updates the optional external user and
 // sends a confirmation email if required.
-func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User, authType auth.Type) (ok bool) {
+func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
 	// Auto-set admin for the only user.
 	if user_model.CountUsers(ctx, nil) == 1 {
 		opts := &user_service.UpdateOptions{
@@ -623,7 +606,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
 
 	// update external user information
 	if gothUser != nil {
-		if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser, authType); err != nil {
+		if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
 			if !errors.Is(err, util.ErrNotExist) {
 				log.Error("UpdateExternalUser failed: %v", err)
 			}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index c62ae84083..1d94e52fe3 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -48,13 +48,13 @@ func LinkAccount(ctx *context.Context) {
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
-	externalLinkUser := ctx.Session.Get("linkAccountUser")
-	if externalLinkUser == nil {
+	gothUser := ctx.Session.Get("linkAccountGothUser")
+	if gothUser == nil {
 		ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
 		return
 	}
 
-	gu := externalLinkUser.(auth.LinkAccountUser).GothUser
+	gu, _ := gothUser.(goth.User)
 	uname, err := getUserName(&gu)
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
@@ -135,14 +135,12 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
-	externalLinkUserInterface := ctx.Session.Get("linkAccountUser")
-	if externalLinkUserInterface == nil {
+	gothUser := ctx.Session.Get("linkAccountGothUser")
+	if gothUser == nil {
 		ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
 		return
 	}
 
-	externalLinkUser := externalLinkUserInterface.(auth.LinkAccountUser)
-
 	if ctx.HasError() {
 		ctx.HTML(http.StatusOK, tplLinkAccount)
 		return
@@ -154,10 +152,10 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 		return
 	}
 
-	linkAccount(ctx, u, externalLinkUser.GothUser, signInForm.Remember, externalLinkUser.Type)
+	linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
 }
 
-func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool, authType auth.Type) {
+func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) {
 	updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
 
 	// If this user is enrolled in 2FA, we can't sign the user in just yet.
@@ -170,7 +168,7 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r
 			return
 		}
 
-		err = externalaccount.LinkAccountToUser(ctx, u, gothUser, authType)
+		err = externalaccount.LinkAccountToUser(ctx, u, gothUser)
 		if err != nil {
 			ctx.ServerError("UserLinkAccount", err)
 			return
@@ -224,14 +222,14 @@ func LinkAccountPostRegister(ctx *context.Context) {
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
-	externalLinkUser := ctx.Session.Get("linkAccountUser")
-	if externalLinkUser == nil {
+	gothUserInterface := ctx.Session.Get("linkAccountGothUser")
+	if gothUserInterface == nil {
 		ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
 		return
 	}
-	linkUser, ok := externalLinkUser.(auth.LinkAccountUser)
+	gothUser, ok := gothUserInterface.(goth.User)
 	if !ok {
-		ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountUser type is %t but not goth.User", externalLinkUser))
+		ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
 		return
 	}
 
@@ -277,7 +275,7 @@ func LinkAccountPostRegister(ctx *context.Context) {
 		}
 	}
 
-	authSource, err := auth.GetActiveAuthSourceByName(ctx, linkUser.GothUser.Provider, linkUser.Type)
+	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
 	if err != nil {
 		ctx.ServerError("CreateUser", err)
 		return
@@ -287,24 +285,21 @@ func LinkAccountPostRegister(ctx *context.Context) {
 		Name:        form.UserName,
 		Email:       form.Email,
 		Passwd:      form.Password,
-		LoginType:   authSource.Type,
+		LoginType:   auth.OAuth2,
 		LoginSource: authSource.ID,
-		LoginName:   linkUser.GothUser.UserID,
+		LoginName:   gothUser.UserID,
 	}
 
-	if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &linkUser.GothUser, false, linkUser.Type) {
+	if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) {
 		// error already handled
 		return
 	}
 
-	if linkUser.Type == auth.OAuth2 {
-		source := authSource.Cfg.(*oauth2.Source)
-		if err := syncGroupsToTeams(ctx, source, &linkUser.GothUser, u); err != nil {
-			ctx.ServerError("SyncGroupsToTeams", err)
-			return
-		}
+	source := authSource.Cfg.(*oauth2.Source)
+	if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
+		ctx.ServerError("SyncGroupsToTeams", err)
+		return
 	}
-	// TODO we will support some form of group mapping for SAML
 
 	handleSignIn(ctx, u, false)
 }
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 5e7368eb9a..33a4ae9192 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -841,7 +841,7 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect
 func SignInOAuth(ctx *context.Context) {
 	provider := ctx.Params(":provider")
 
-	authSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.OAuth2)
+	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
 	if err != nil {
 		ctx.ServerError("SignIn", err)
 		return
@@ -892,7 +892,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 	}
 
 	// first look if the provider is still active
-	authSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.OAuth2)
+	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
 	if err != nil {
 		ctx.ServerError("SignIn", err)
 		return
@@ -935,7 +935,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 	if u == nil {
 		if ctx.Doer != nil {
 			// attach user to already logged in user
-			err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser, auth.OAuth2)
+			err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
 			if err != nil {
 				ctx.ServerError("UserLinkAccount", err)
 				return
@@ -988,7 +988,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			u.IsAdmin = isAdmin.ValueOrDefault(false)
 			u.IsRestricted = isRestricted.ValueOrDefault(false)
 
-			if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled, auth.OAuth2) {
+			if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
 				// error already handled
 				return
 			}
@@ -999,7 +999,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			}
 		} else {
 			// no existing user is found, request attach or new account
-			showLinkingLogin(ctx, gothUser, auth.OAuth2)
+			showLinkingLogin(ctx, gothUser)
 			return
 		}
 	}
@@ -1063,12 +1063,9 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g
 	return isAdmin, isRestricted
 }
 
-func showLinkingLogin(ctx *context.Context, gothUser goth.User, authType auth.Type) {
+func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
 	if err := updateSession(ctx, nil, map[string]any{
-		"linkAccountUser": auth.LinkAccountUser{
-			Type:     authType,
-			GothUser: gothUser,
-		},
+		"linkAccountGothUser": gothUser,
 	}); err != nil {
 		ctx.ServerError("updateSession", err)
 		return
@@ -1147,7 +1144,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
 		}
 
 		// update external user information
-		if err := externalaccount.UpdateExternalUser(ctx, u, gothUser, auth.OAuth2); err != nil {
+		if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
 			if !errors.Is(err, util.ErrNotExist) {
 				log.Error("UpdateExternalUser failed: %v", err)
 			}
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index bf377b4496..29ef772b1c 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -8,7 +8,6 @@ import (
 	"net/http"
 	"net/url"
 
-	auth_model "code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/openid"
 	"code.gitea.io/gitea/modules/base"
@@ -364,7 +363,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
 		Email:  form.Email,
 		Passwd: password,
 	}
-	if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false, auth_model.NoType) {
+	if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false) {
 		// error already handled
 		return
 	}
@@ -380,7 +379,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
 		return
 	}
 
-	if !handleUserCreated(ctx, u, nil, auth_model.NoType) {
+	if !handleUserCreated(ctx, u, nil) {
 		// error already handled
 		return
 	}
diff --git a/routers/web/auth/saml.go b/routers/web/auth/saml.go
deleted file mode 100644
index 29d689d2e9..0000000000
--- a/routers/web/auth/saml.go
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright 2024 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package auth
-
-import (
-	"errors"
-	"fmt"
-	"net/http"
-	"strings"
-
-	"code.gitea.io/gitea/models/auth"
-	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
-	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/services/auth/source/saml"
-	"code.gitea.io/gitea/services/externalaccount"
-
-	"github.com/markbates/goth"
-)
-
-func SignInSAML(ctx *context.Context) {
-	provider := ctx.Params(":provider")
-
-	loginSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.SAML)
-	if err != nil || loginSource == nil {
-		ctx.NotFound("SAMLMetadata", err)
-		return
-	}
-
-	if err = loginSource.Cfg.(*saml.Source).Callout(ctx.Req, ctx.Resp); err != nil {
-		if strings.Contains(err.Error(), "no provider for ") {
-			ctx.Error(http.StatusNotFound)
-			return
-		}
-		ctx.ServerError("SignIn", err)
-	}
-}
-
-func SignInSAMLCallback(ctx *context.Context) {
-	provider := ctx.Params(":provider")
-	loginSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.SAML)
-	if err != nil || loginSource == nil {
-		ctx.NotFound("SignInSAMLCallback", err)
-		return
-	}
-
-	if loginSource == nil {
-		ctx.ServerError("SignIn", fmt.Errorf("no valid provider found, check configured callback url in provider"))
-		return
-	}
-
-	u, gothUser, err := samlUserLoginCallback(*ctx, loginSource, ctx.Req, ctx.Resp)
-	if err != nil {
-		ctx.ServerError("SignInSAMLCallback", err)
-		return
-	}
-
-	if u == nil {
-		if ctx.Doer != nil {
-			// attach user to already logged in user
-			err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser, auth.SAML)
-			if err != nil {
-				ctx.ServerError("LinkAccountToUser", err)
-				return
-			}
-
-			ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-			return
-		} else if !setting.Service.AllowOnlyInternalRegistration && false {
-			// TODO: allow auto registration from saml users (OAuth2 uses the following setting.OAuth2Client.EnableAutoRegistration)
-		} else {
-			// no existing user is found, request attach or new account
-			showLinkingLogin(ctx, gothUser, auth.SAML)
-			return
-		}
-	}
-
-	handleSamlSignIn(ctx, loginSource, u, gothUser)
-}
-
-func handleSamlSignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) {
-	if err := updateSession(ctx, nil, map[string]any{
-		"uid":   u.ID,
-		"uname": u.Name,
-	}); err != nil {
-		ctx.ServerError("updateSession", err)
-		return
-	}
-
-	// Clear whatever CSRF cookie has right now, force to generate a new one
-	ctx.Csrf.DeleteCookie(ctx)
-
-	// Register last login
-	u.SetLastLogin()
-
-	// update external user information
-	if err := externalaccount.UpdateExternalUser(ctx, u, gothUser, auth.SAML); err != nil {
-		if !errors.Is(err, util.ErrNotExist) {
-			log.Error("UpdateExternalUser failed: %v", err)
-		}
-	}
-
-	if err := resetLocale(ctx, u); err != nil {
-		ctx.ServerError("resetLocale", err)
-		return
-	}
-
-	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
-		middleware.DeleteRedirectToCookie(ctx.Resp)
-		ctx.RedirectToFirst(redirectTo)
-		return
-	}
-
-	ctx.Redirect(setting.AppSubURL + "/")
-}
-
-func samlUserLoginCallback(ctx context.Context, authSource *auth.Source, request *http.Request, response http.ResponseWriter) (*user_model.User, goth.User, error) {
-	samlSource := authSource.Cfg.(*saml.Source)
-
-	gothUser, err := samlSource.Callback(request, response)
-	if err != nil {
-		return nil, gothUser, err
-	}
-
-	user := &user_model.User{
-		LoginName:   gothUser.UserID,
-		LoginType:   auth.SAML,
-		LoginSource: authSource.ID,
-	}
-
-	hasUser, err := user_model.GetUser(ctx, user)
-	if err != nil {
-		return nil, goth.User{}, err
-	}
-
-	if hasUser {
-		return user, gothUser, nil
-	}
-
-	// search in external linked users
-	externalLoginUser := &user_model.ExternalLoginUser{
-		ExternalID:    gothUser.UserID,
-		LoginSourceID: authSource.ID,
-	}
-	hasUser, err = user_model.GetExternalLogin(ctx, externalLoginUser)
-	if err != nil {
-		return nil, goth.User{}, err
-	}
-	if hasUser {
-		user, err = user_model.GetUserByID(request.Context(), externalLoginUser.UserID)
-		return user, gothUser, err
-	}
-
-	// no user found to login
-	return nil, gothUser, nil
-}
-
-func SAMLMetadata(ctx *context.Context) {
-	provider := ctx.Params(":provider")
-	loginSource, err := auth.GetActiveAuthSourceByName(ctx, provider, auth.SAML)
-	if err != nil || loginSource == nil {
-		ctx.NotFound("SAMLMetadata", err)
-		return
-	}
-	if err = loginSource.Cfg.(*saml.Source).Metadata(ctx.Req, ctx.Resp); err != nil {
-		ctx.ServerError("SAMLMetadata", err)
-	}
-}
diff --git a/routers/web/web.go b/routers/web/web.go
index 5e18aac67d..a76b444e4f 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -667,11 +667,6 @@ func registerRoutes(m *web.Route) {
 			m.Get("/{provider}", auth.SignInOAuth)
 			m.Get("/{provider}/callback", auth.SignInOAuthCallback)
 		})
-		m.Group("/saml", func() {
-			m.Get("/{provider}", auth.SignInSAML) // redir to SAML IDP
-			m.Post("/{provider}/acs", auth.SignInSAMLCallback)
-			m.Get("/{provider}/metadata", auth.SAMLMetadata)
-		})
 	})
 	// ***** END: User *****
 
diff --git a/services/auth/source/saml/assert_interface_test.go b/services/auth/source/saml/assert_interface_test.go
deleted file mode 100644
index 2ca7057b8a..0000000000
--- a/services/auth/source/saml/assert_interface_test.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml_test
-
-import (
-	auth_model "code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/services/auth"
-	"code.gitea.io/gitea/services/auth/source/saml"
-)
-
-// This test file exists to assert that our Source exposes the interfaces that we expect
-// It tightly binds the interfaces and implementation without breaking go import cycles
-
-type sourceInterface interface {
-	auth_model.Config
-	auth_model.SourceSettable
-	auth_model.RegisterableSource
-	auth.PasswordAuthenticator
-}
-
-var _ (sourceInterface) = &saml.Source{}
diff --git a/services/auth/source/saml/init.go b/services/auth/source/saml/init.go
deleted file mode 100644
index f1d6d9fa4b..0000000000
--- a/services/auth/source/saml/init.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-import (
-	"context"
-	"sync"
-
-	"code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/modules/log"
-)
-
-var samlRWMutex = sync.RWMutex{}
-
-func Init(ctx context.Context) error {
-	loginSources, _ := auth.GetActiveAuthProviderSources(ctx, auth.SAML)
-	for _, source := range loginSources {
-		samlSource, ok := source.Cfg.(*Source)
-		if !ok {
-			continue
-		}
-		err := samlSource.RegisterSource()
-		if err != nil {
-			log.Error("Unable to register source: %s due to Error: %v.", source.Name, err)
-		}
-	}
-	return nil
-}
diff --git a/services/auth/source/saml/name_id_format.go b/services/auth/source/saml/name_id_format.go
deleted file mode 100644
index 1ddf047729..0000000000
--- a/services/auth/source/saml/name_id_format.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-type NameIDFormat int
-
-const (
-	SAML11Email NameIDFormat = iota + 1
-	SAML11Persistent
-	SAML11Unspecified
-	SAML20Email
-	SAML20Persistent
-	SAML20Transient
-	SAML20Unspecified
-)
-
-const DefaultNameIDFormat NameIDFormat = SAML20Persistent
-
-var NameIDFormatNames = map[NameIDFormat]string{
-	SAML11Email:       "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
-	SAML11Persistent:  "urn:oasis:names:tc:SAML:1.1:nameid-format:persistent",
-	SAML11Unspecified: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
-	SAML20Email:       "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress",
-	SAML20Persistent:  "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
-	SAML20Transient:   "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
-	SAML20Unspecified: "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
-}
-
-// String returns the name of the NameIDFormat
-func (n NameIDFormat) String() string {
-	return NameIDFormatNames[n]
-}
-
-// Int returns the int value of the NameIDFormat
-func (n NameIDFormat) Int() int {
-	return int(n)
-}
diff --git a/services/auth/source/saml/providers.go b/services/auth/source/saml/providers.go
deleted file mode 100644
index d0b36ff44d..0000000000
--- a/services/auth/source/saml/providers.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-import (
-	"context"
-	"fmt"
-	"html"
-	"html/template"
-	"io"
-	"net/http"
-	"sort"
-	"time"
-
-	"code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/httplib"
-	"code.gitea.io/gitea/modules/svg"
-	"code.gitea.io/gitea/modules/util"
-)
-
-// Providers is list of known/available providers.
-type Providers map[string]Source
-
-var providers = Providers{}
-
-// Provider is an interface for describing a single SAML provider
-type Provider interface {
-	Name() string
-	IconHTML(size int) template.HTML
-}
-
-// AuthSourceProvider is a SAML provider
-type AuthSourceProvider struct {
-	sourceName, iconURL string
-}
-
-func (p *AuthSourceProvider) Name() string {
-	return p.sourceName
-}
-
-func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
-	if p.iconURL != "" {
-		return template.HTML(fmt.Sprintf(`<img class="gt-object-contain gt-mr-3" width="%d" height="%d" src="%s" alt="%s">`,
-			size,
-			size,
-			html.EscapeString(p.iconURL), html.EscapeString(p.Name()),
-		))
-	}
-	return svg.RenderHTML("gitea-lock-cog", size, "gt-mr-3")
-}
-
-func readIdentityProviderMetadata(ctx context.Context, source *Source) ([]byte, error) {
-	if source.IdentityProviderMetadata != "" {
-		return []byte(source.IdentityProviderMetadata), nil
-	}
-
-	req := httplib.NewRequest(source.IdentityProviderMetadataURL, "GET")
-	req.SetTimeout(20*time.Second, time.Minute)
-	resp, err := req.Response()
-	if err != nil {
-		return nil, fmt.Errorf("Unable to contact gitea: %v", err)
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusOK {
-		return nil, err
-	}
-
-	data, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, err
-	}
-	return data, nil
-}
-
-func createProviderFromSource(source *auth.Source) (Provider, error) {
-	samlCfg, ok := source.Cfg.(*Source)
-	if !ok {
-		return nil, fmt.Errorf("invalid SAML source config: %v", samlCfg)
-	}
-	return &AuthSourceProvider{sourceName: source.Name, iconURL: samlCfg.IconURL}, nil
-}
-
-// GetSAMLProviders returns the list of configured SAML providers
-func GetSAMLProviders(ctx context.Context, isActive util.OptionalBool) ([]Provider, error) {
-	authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive:  isActive,
-		LoginType: auth.SAML,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	samlProviders := make([]Provider, 0, len(authSources))
-	for _, source := range authSources {
-		p, err := createProviderFromSource(source)
-		if err != nil {
-			return nil, err
-		}
-		samlProviders = append(samlProviders, p)
-	}
-
-	sort.Slice(samlProviders, func(i, j int) bool {
-		return samlProviders[i].Name() < samlProviders[j].Name()
-	})
-
-	return samlProviders, nil
-}
diff --git a/services/auth/source/saml/source.go b/services/auth/source/saml/source.go
deleted file mode 100644
index 52388646b5..0000000000
--- a/services/auth/source/saml/source.go
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-import (
-	"context"
-	"crypto/rand"
-	"crypto/rsa"
-	"crypto/tls"
-	"crypto/x509"
-	"encoding/base64"
-	"encoding/pem"
-	"encoding/xml"
-	"errors"
-	"fmt"
-	"math/big"
-	"net/url"
-	"time"
-
-	"code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/modules/json"
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-
-	saml2 "github.com/russellhaering/gosaml2"
-	"github.com/russellhaering/gosaml2/types"
-	dsig "github.com/russellhaering/goxmldsig"
-)
-
-// Source holds configuration for the SAML login source.
-type Source struct {
-	// IdentityProviderMetadata description: The SAML Identity Provider metadata XML contents (for static configuration of the SAML Service Provider). The value of this field should be an XML document whose root element is `<EntityDescriptor>` or `<EntityDescriptors>`. To escape the value into a JSON string, you may want to use a tool like https://json-escape-text.now.sh.
-	IdentityProviderMetadata string
-	// IdentityProviderMetadataURL description: The SAML Identity Provider metadata URL (for dynamic configuration of the SAML Service Provider).
-	IdentityProviderMetadataURL string
-	// InsecureSkipAssertionSignatureValidation description: Whether the Service Provider should (insecurely) accept assertions from the Identity Provider without a valid signature.
-	InsecureSkipAssertionSignatureValidation bool
-	// NameIDFormat description: The SAML NameID format to use when performing user authentication.
-	NameIDFormat NameIDFormat
-	// ServiceProviderCertificate description: The SAML Service Provider certificate in X.509 encoding (begins with "-----BEGIN CERTIFICATE-----"). This certificate is used by the Identity Provider to validate the Service Provider's AuthnRequests and LogoutRequests. It corresponds to the Service Provider's private key (`serviceProviderPrivateKey`). To escape the value into a JSON string, you may want to use a tool like https://json-escape-text.now.sh.
-	ServiceProviderCertificate string
-	// ServiceProviderIssuer description: The SAML Service Provider name, used to identify this Service Provider. This is required if the "externalURL" field is not set (as the SAML metadata endpoint is computed as "<externalURL>.auth/saml/metadata"), or when using multiple SAML authentication providers.
-	ServiceProviderIssuer string
-	// ServiceProviderPrivateKey description: The SAML Service Provider private key in PKCS#8 encoding (begins with "-----BEGIN PRIVATE KEY-----"). This private key is used to sign AuthnRequests and LogoutRequests. It corresponds to the Service Provider's certificate (`serviceProviderCertificate`). To escape the value into a JSON string, you may want to use a tool like https://json-escape-text.now.sh.
-	ServiceProviderPrivateKey string
-
-	CallbackURL string
-	IconURL     string
-
-	// EmailAssertionKey description: Assertion key for user.Email
-	EmailAssertionKey string
-	// NameAssertionKey description: Assertion key for user.NickName
-	NameAssertionKey string
-	// UsernameAssertionKey description: Assertion key for user.Name
-	UsernameAssertionKey string
-
-	// reference to the authSource
-	authSource *auth.Source
-
-	samlSP *saml2.SAMLServiceProvider
-}
-
-func GenerateSAMLSPKeypair() (string, string, error) {
-	key, err := rsa.GenerateKey(rand.Reader, 4096)
-	if err != nil {
-		return "", "", err
-	}
-
-	keyBytes := x509.MarshalPKCS1PrivateKey(key)
-	keyPem := pem.EncodeToMemory(
-		&pem.Block{
-			Type:  "RSA PRIVATE KEY",
-			Bytes: keyBytes,
-		},
-	)
-
-	now := time.Now()
-
-	template := &x509.Certificate{
-		SerialNumber: big.NewInt(0),
-		NotBefore:    now.Add(-5 * time.Minute),
-		NotAfter:     now.Add(365 * 24 * time.Hour),
-
-		KeyUsage:              x509.KeyUsageDigitalSignature,
-		ExtKeyUsage:           []x509.ExtKeyUsage{},
-		BasicConstraintsValid: true,
-	}
-
-	certificate, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
-	if err != nil {
-		return "", "", err
-	}
-
-	certPem := pem.EncodeToMemory(
-		&pem.Block{
-			Type:  "CERTIFICATE",
-			Bytes: certificate,
-		},
-	)
-
-	return string(keyPem), string(certPem), nil
-}
-
-func (source *Source) initSAMLSp() error {
-	source.CallbackURL = setting.AppURL + "user/saml/" + url.PathEscape(source.authSource.Name) + "/acs"
-
-	idpMetadata, err := readIdentityProviderMetadata(context.Background(), source)
-	if err != nil {
-		return err
-	}
-	{
-		if source.IdentityProviderMetadataURL != "" {
-			log.Trace(fmt.Sprintf("Identity Provider metadata: %s", source.IdentityProviderMetadataURL), string(idpMetadata))
-		}
-	}
-
-	metadata := &types.EntityDescriptor{}
-	err = xml.Unmarshal(idpMetadata, metadata)
-	if err != nil {
-		return err
-	}
-
-	certStore := dsig.MemoryX509CertificateStore{
-		Roots: []*x509.Certificate{},
-	}
-
-	if metadata.IDPSSODescriptor == nil {
-		return errors.New("saml idp metadata missing IDPSSODescriptor")
-	}
-
-	for _, kd := range metadata.IDPSSODescriptor.KeyDescriptors {
-		for idx, xcert := range kd.KeyInfo.X509Data.X509Certificates {
-			if xcert.Data == "" {
-				return fmt.Errorf("metadata certificate(%d) must not be empty", idx)
-			}
-			certData, err := base64.StdEncoding.DecodeString(xcert.Data)
-			if err != nil {
-				return err
-			}
-
-			idpCert, err := x509.ParseCertificate(certData)
-			if err != nil {
-				return err
-			}
-
-			certStore.Roots = append(certStore.Roots, idpCert)
-		}
-	}
-
-	var keyStore dsig.X509KeyStore
-
-	if source.ServiceProviderCertificate != "" && source.ServiceProviderPrivateKey != "" {
-		keyPair, err := tls.X509KeyPair([]byte(source.ServiceProviderCertificate), []byte(source.ServiceProviderPrivateKey))
-		if err != nil {
-			return err
-		}
-		keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
-		if err != nil {
-			return err
-		}
-		keyStore = dsig.TLSCertKeyStore(keyPair)
-	}
-
-	source.samlSP = &saml2.SAMLServiceProvider{
-		IdentityProviderSSOURL:      metadata.IDPSSODescriptor.SingleSignOnServices[0].Location,
-		IdentityProviderIssuer:      metadata.EntityID,
-		AudienceURI:                 setting.AppURL + "user/saml/" + url.PathEscape(source.authSource.Name) + "/metadata",
-		AssertionConsumerServiceURL: source.CallbackURL,
-		SkipSignatureValidation:     source.InsecureSkipAssertionSignatureValidation,
-		NameIdFormat:                source.NameIDFormat.String(),
-		IDPCertificateStore:         &certStore,
-		SignAuthnRequests:           source.ServiceProviderCertificate != "" && source.ServiceProviderPrivateKey != "",
-		SPKeyStore:                  keyStore,
-		ServiceProviderIssuer:       setting.AppURL + "user/saml/" + url.PathEscape(source.authSource.Name) + "/metadata",
-	}
-
-	return nil
-}
-
-// FromDB fills up a SAML from serialized format.
-func (source *Source) FromDB(bs []byte) error {
-	if err := json.UnmarshalHandleDoubleEncode(bs, &source); err != nil {
-		return err
-	}
-
-	return source.initSAMLSp()
-}
-
-// ToDB exports a SAML to a serialized format.
-func (source *Source) ToDB() ([]byte, error) {
-	return json.Marshal(source)
-}
-
-// SetAuthSource sets the related AuthSource
-func (source *Source) SetAuthSource(authSource *auth.Source) {
-	source.authSource = authSource
-}
-
-func init() {
-	auth.RegisterTypeConfig(auth.SAML, &Source{})
-}
diff --git a/services/auth/source/saml/source_authenticate.go b/services/auth/source/saml/source_authenticate.go
deleted file mode 100644
index d118917f87..0000000000
--- a/services/auth/source/saml/source_authenticate.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-import (
-	"context"
-
-	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/services/auth/source/db"
-)
-
-// Authenticate falls back to the db authenticator
-func (source *Source) Authenticate(ctx context.Context, user *user_model.User, login, password string) (*user_model.User, error) {
-	return db.Authenticate(ctx, user, login, password)
-}
diff --git a/services/auth/source/saml/source_callout.go b/services/auth/source/saml/source_callout.go
deleted file mode 100644
index 5366f8a527..0000000000
--- a/services/auth/source/saml/source_callout.go
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-import (
-	"fmt"
-	"net/http"
-	"strings"
-
-	"github.com/markbates/goth"
-)
-
-// Callout redirects request/response pair to authenticate against the provider
-func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
-	samlRWMutex.RLock()
-	defer samlRWMutex.RUnlock()
-	if _, ok := providers[source.authSource.Name]; !ok {
-		return fmt.Errorf("no provider for this saml")
-	}
-
-	authURL, err := providers[source.authSource.Name].samlSP.BuildAuthURL("")
-	if err == nil {
-		http.Redirect(response, request, authURL, http.StatusTemporaryRedirect)
-	}
-	return err
-}
-
-// Callback handles SAML callback, resolve to a goth user and send back to original url
-// this will trigger a new authentication request, but because we save it in the session we can use that
-func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
-	samlRWMutex.RLock()
-	defer samlRWMutex.RUnlock()
-
-	user := goth.User{
-		Provider: source.authSource.Name,
-	}
-	samlResponse := request.FormValue("SAMLResponse")
-	assertions, err := source.samlSP.RetrieveAssertionInfo(samlResponse)
-	if err != nil {
-		return user, err
-	}
-
-	if assertions.WarningInfo.OneTimeUse {
-		return user, fmt.Errorf("SAML response contains one time use warning")
-	}
-
-	if assertions.WarningInfo.ProxyRestriction != nil {
-		return user, fmt.Errorf("SAML response contains proxy restriction warning: %v", assertions.WarningInfo.ProxyRestriction)
-	}
-
-	if assertions.WarningInfo.NotInAudience {
-		return user, fmt.Errorf("SAML response contains audience warning")
-	}
-
-	if assertions.WarningInfo.InvalidTime {
-		return user, fmt.Errorf("SAML response contains invalid time warning")
-	}
-
-	samlMap := make(map[string]string)
-	for key, value := range assertions.Values {
-		keyParsed := strings.ToLower(key[strings.LastIndex(key, "/")+1:]) // Uses the trailing slug as the key name.
-		valueParsed := value.Values[0].Value
-		samlMap[keyParsed] = valueParsed
-
-	}
-
-	user.UserID = assertions.NameID
-	if user.UserID == "" {
-		return user, fmt.Errorf("no nameID found in SAML response")
-	}
-
-	// email
-	if _, ok := samlMap[source.EmailAssertionKey]; !ok {
-		user.Email = samlMap[source.EmailAssertionKey]
-	}
-	// name
-	if _, ok := samlMap[source.NameAssertionKey]; !ok {
-		user.NickName = samlMap[source.NameAssertionKey]
-	}
-	// username
-	if _, ok := samlMap[source.UsernameAssertionKey]; !ok {
-		user.Name = samlMap[source.UsernameAssertionKey]
-	}
-
-	// TODO: utilize groups once mapping is supported
-
-	return user, nil
-}
diff --git a/services/auth/source/saml/source_metadata.go b/services/auth/source/saml/source_metadata.go
deleted file mode 100644
index 9fb8c758e3..0000000000
--- a/services/auth/source/saml/source_metadata.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-import (
-	"encoding/xml"
-	"fmt"
-	"net/http"
-)
-
-// Metadata redirects request/response pair to authenticate against the provider
-func (source *Source) Metadata(request *http.Request, response http.ResponseWriter) error {
-	samlRWMutex.RLock()
-	defer samlRWMutex.RUnlock()
-	if _, ok := providers[source.authSource.Name]; !ok {
-		return fmt.Errorf("provider does not exist")
-	}
-
-	metadata, err := providers[source.authSource.Name].samlSP.Metadata()
-	if err != nil {
-		return err
-	}
-	buf, err := xml.Marshal(metadata)
-	if err != nil {
-		return err
-	}
-
-	response.Header().Set("Content-Type", "application/samlmetadata+xml; charset=utf-8")
-	_, _ = response.Write(buf)
-	return nil
-}
diff --git a/services/auth/source/saml/source_register.go b/services/auth/source/saml/source_register.go
deleted file mode 100644
index 93eaaa88b6..0000000000
--- a/services/auth/source/saml/source_register.go
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package saml
-
-// RegisterSource causes an OAuth2 configuration to be registered
-func (source *Source) RegisterSource() error {
-	samlRWMutex.Lock()
-	defer samlRWMutex.Unlock()
-	if err := source.initSAMLSp(); err != nil {
-		return err
-	}
-	providers[source.authSource.Name] = *source
-	return nil
-}
-
-// UnregisterSource causes an SAML configuration to be unregistered
-func (source *Source) UnregisterSource() error {
-	samlRWMutex.Lock()
-	defer samlRWMutex.Unlock()
-	delete(providers, source.authSource.Name)
-	return nil
-}
diff --git a/services/externalaccount/link.go b/services/externalaccount/link.go
index 1f4c6728b8..d6e2ea7e94 100644
--- a/services/externalaccount/link.go
+++ b/services/externalaccount/link.go
@@ -7,8 +7,9 @@ import (
 	"context"
 	"fmt"
 
-	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/markbates/goth"
 )
 
 // Store represents a thing that stores things
@@ -20,12 +21,10 @@ type Store interface {
 
 // LinkAccountFromStore links the provided user with a stored external user
 func LinkAccountFromStore(ctx context.Context, store Store, user *user_model.User) error {
-	externalLinkUserInterface := store.Get("linkAccountUser")
-	if externalLinkUserInterface == nil {
+	gothUser := store.Get("linkAccountGothUser")
+	if gothUser == nil {
 		return fmt.Errorf("not in LinkAccount session")
 	}
 
-	externalLinkUser := externalLinkUserInterface.(auth.LinkAccountUser)
-
-	return LinkAccountToUser(ctx, user, externalLinkUser.GothUser, externalLinkUser.Type)
+	return LinkAccountToUser(ctx, user, gothUser.(goth.User))
 }
diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go
index fa85a65669..e2de41da18 100644
--- a/services/externalaccount/user.go
+++ b/services/externalaccount/user.go
@@ -16,8 +16,8 @@ import (
 	"github.com/markbates/goth"
 )
 
-func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User, authType auth.Type) (*user_model.ExternalLoginUser, error) {
-	authSource, err := auth.GetActiveAuthSourceByName(ctx, gothUser.Provider, authType)
+func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User) (*user_model.ExternalLoginUser, error) {
+	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
 	if err != nil {
 		return nil, err
 	}
@@ -43,8 +43,8 @@ func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser go
 }
 
 // LinkAccountToUser link the gothUser to the user
-func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User, authType auth.Type) error {
-	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser, authType)
+func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
+	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
 	if err != nil {
 		return err
 	}
@@ -71,8 +71,8 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
 }
 
 // UpdateExternalUser updates external user's information
-func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User, authType auth.Type) error {
-	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser, authType)
+func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
+	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
 	if err != nil {
 		return err
 	}
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 85be38b403..25acbbb99e 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -1,4 +1,3 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
 // Copyright 2014 The Gogs Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
@@ -16,7 +15,7 @@ import (
 // AuthenticationForm form for authentication
 type AuthenticationForm struct {
 	ID                            int64
-	Type                          int    `binding:"Range(2,9)"`
+	Type                          int    `binding:"Range(2,7)"`
 	Name                          string `binding:"Required;MaxSize(30)"`
 	Host                          string
 	Port                          int
@@ -83,18 +82,6 @@ type AuthenticationForm struct {
 	SSPIDefaultLanguage           string
 	GroupTeamMap                  string `binding:"ValidGroupTeamMap"`
 	GroupTeamMapRemoval           bool
-
-	// SAML Settings
-	NameIDFormat                             int
-	IdentityProviderMetadata                 string
-	IdentityProviderMetadataURL              string
-	InsecureSkipAssertionSignatureValidation bool
-	ServiceProviderCertificate               string
-	ServiceProviderPrivateKey                string
-	EmailAssertionKey                        string
-	NameAssertionKey                         string
-	UsernameAssertionKey                     string
-	SAMLIconURL                              string
 }
 
 // Validate validates fields
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 2182d011e9..25abefae00 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -367,69 +367,6 @@
 					</div>
 				{{end}}
 
-				<!-- SAML -->
-				{{if .Source.IsSAML}}
-					{{$cfg:=.Source.Cfg}}
-					<div class="inline required field">
-						<label>{{ctx.Locale.Tr "admin.auths.saml_nameidformat"}}</label>
-						<div class="ui selection type dropdown">
-							<input type="hidden" id="name_id_format" name="name_id_format" value="{{$cfg.NameIDFormat}}">
-							<div class="text">{{.CurrentNameIDFormat}}</div>
-							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-							<div class="menu">
-								{{range .NameIDFormats}}
-									<div class="item" data-value="{{.Type.Int}}">{{.Name}}</div>
-								{{end}}
-							</div>
-						</div>
-					</div>
-
-					<div class="optional field">
-						<label for="saml_icon_url">{{ctx.Locale.Tr "admin.auths.saml_icon_url"}}</label>
-						<input id="saml_icon_url" name="saml_icon_url" value="{{$cfg.IconURL}}">
-					</div>
-
-					<div class="field">
-						<label for="identity_provider_metadata_url">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata_url"}}</label>
-						<input id="identity_provider_metadata_url" name="identity_provider_metadata_url" value="{{$cfg.IdentityProviderMetadataURL}}">
-					</div>
-					<div class="field">
-						<label for="identity_provider_metadata">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata"}}</label>
-						<textarea rows=2 id="identity_provider_metadata" name="identity_provider_metadata">{{$cfg.IdentityProviderMetadata}}</textarea>
-					</div>
-
-					<div class="inline field">
-						<div class="ui checkbox">
-							<label><strong>{{ctx.Locale.Tr "admin.auths.saml_insecure_skip_assertion_signature_validation"}}</strong></label>
-							<input name="insecure_skip_assertion_signature_validation" type="checkbox" {{if $cfg.InsecureSkipAssertionSignatureValidation}}checked{{end}}>
-						</div>
-					</div>
-
-					<div class=" field">
-						<label for="service_provider_certificate">{{ctx.Locale.Tr "admin.auths.saml_service_provider_certificate"}}</label>
-						<textarea rows=2 id="service_provider_certificate" name="service_provider_certificate">{{$cfg.ServiceProviderCertificate}}</textarea>
-					</div>
-					<div class=" field">
-						<label for="service_provider_private_key">{{ctx.Locale.Tr "admin.auths.saml_service_provider_private_key"}}</label>
-						<textarea rows=2 id="service_provider_private_key" name="service_provider_private_key">{{$cfg.ServiceProviderPrivateKey}}</textarea>
-					</div>
-
-					<div class="field">
-						<label for="email_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_email_assertion_key"}}</label>
-						<input id="email_assertion_key" name="email_assertion_key" value="{{if not $cfg.EmailAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress{{else}}{{$cfg.EmailAssertionKey}}{{end}}">
-					</div>
-
-					<div class="field">
-						<label for="name_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_name_assertion_key"}}</label>
-						<input id="name_assertion_key" name="name_assertion_key" value="{{if not $cfg.NameAssertionKey}}http://schemas.xmlsoap.org/claims/CommonName{{else}}{{$cfg.NameAssertionKey}}{{end}}">
-					</div>
-
-					<div class="field">
-						<label for="username_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_username_assertion_key"}}</label>
-						<input id="username_assertion_key" name="username_assertion_key" value="{{if not $cfg.UsernameAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name{{else}}{{$cfg.UsernameAssertionKey}}{{end}}">
-					</div>
-				{{end}}
-
 				<!-- SSPI -->
 				{{if .Source.IsSSPI}}
 					{{$cfg:=.Source.Cfg}}
@@ -504,9 +441,6 @@
 			<h5>GMail Settings:</h5>
 			<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
 
-			<h5>SAML Settings:</h5>
-			<p>{{ctx.Locale.Tr "admin.auths.tips.saml"}}</p>
-
 			<h5 class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general"}}:</h5>
 			<p class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general.tip"}} <b id="oauth2-callback-url"></b></p>
 		</div>
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index 665b0e3086..f32f77d5dc 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -53,9 +53,6 @@
 				<!-- SSPI -->
 				{{template "admin/auth/source/sspi" .}}
 
-				<!-- SAML -->
-				{{template "admin/auth/source/saml" .}}
-
 				<div class="ldap field">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "admin.auths.attributes_in_bind"}}</strong></label>
@@ -88,9 +85,6 @@
 			<h5>GMail Settings:</h5>
 			<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
 
-			<h5>SAML Settings:</h5>
-			<p>{{ctx.Locale.Tr "admin.auths.tips.saml"}}</p>
-
 			<h5 class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general"}}:</h5>
 			<p class="oauth2">{{ctx.Locale.Tr "admin.auths.tips.oauth2.general.tip"}} <b id="oauth2-callback-url"></b></p>
 
diff --git a/templates/admin/auth/source/saml.tmpl b/templates/admin/auth/source/saml.tmpl
deleted file mode 100644
index 050e22ddcc..0000000000
--- a/templates/admin/auth/source/saml.tmpl
+++ /dev/null
@@ -1,62 +0,0 @@
-<div class="saml field {{if not (eq .type 8)}}gt-hidden{{end}}">
-
-	<div class="inline required field">
-		<label>{{ctx.Locale.Tr "admin.auths.saml_nameidformat"}}</label>
-		<div class="ui selection type dropdown">
-			<input type="hidden" id="name_id_format" name="name_id_format" value="{{.name_id_format}}">
-			<div class="text">{{.CurrentNameIDFormat}}</div>
-			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-			<div class="menu">
-				{{range .NameIDFormats}}
-					<div class="item" data-value="{{.Type.Int}}">{{.Name}}</div>
-				{{end}}
-			</div>
-		</div>
-	</div>
-
-	<div class="optional field">
-		<label for="saml_icon_url">{{ctx.Locale.Tr "admin.auths.saml_icon_url"}}</label>
-		<input id="saml_icon_url" name="saml_icon_url" value="{{.SAMLIconURL}}">
-	</div>
-
-	<div class="field">
-		<label for="identity_provider_metadata_url">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata_url"}}</label>
-		<input id="identity_provider_metadata_url" name="identity_provider_metadata_url" value="{{.IdentityProviderMetadataURL}}">
-	</div>
-	<div class="field">
-		<label for="identity_provider_metadata">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_metadata"}}</label>
-		<textarea rows=2 id="identity_provider_metadata" name="identity_provider_metadata" value="{{.IdentityProviderMetadata}}"></textarea>
-	</div>
-
-	<div class="inline field">
-		<div class="ui checkbox">
-			<label><strong>{{ctx.Locale.Tr "admin.auths.saml_insecure_skip_assertion_signature_validation"}}</strong></label>
-			<input name="insecure_skip_assertion_signature_validation" type="checkbox" {{if .InsecureSkipAssertionSignatureValidation}}checked{{end}}>
-		</div>
-	</div>
-
-	<div class="field">
-		<label for="service_provider_certificate">{{ctx.Locale.Tr "admin.auths.saml_service_provider_certificate"}}</label>
-		<textarea rows=2 id="service_provider_certificate" name="service_provider_certificate" value="{{.ServiceProviderCertificate}}"></textarea>
-	</div>
-	<div class="field">
-		<label for="service_provider_private_key">{{ctx.Locale.Tr "admin.auths.saml_service_provider_private_key"}}</label>
-		<textarea rows=2 id="service_provider_private_key" name="service_provider_private_key" value="{{.ServiceProviderPrivateKey}}"></textarea>
-	</div>
-
-	<div class="field">
-		<label for="email_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_email_assertion_key"}}</label>
-		<input id="email_assertion_key" name="email_assertion_key" value="{{if not .EmailAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress{{else}}{{.EmailAssertionKey}}{{end}}">
-	</div>
-
-	<div class="field">
-		<label for="name_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_name_assertion_key"}}</label>
-		<input id="name_assertion_key" name="name_assertion_key" value="{{if not .NameAssertionKey}}http://schemas.xmlsoap.org/claims/CommonName{{else}}{{.NameAssertionKey}}{{end}}">
-	</div>
-
-	<div class="field">
-		<label for="username_assertion_key">{{ctx.Locale.Tr "admin.auths.saml_identity_provider_username_assertion_key"}}</label>
-		<input id="username_assertion_key" name="username_assertion_key" value="{{if not .UsernameAssertionKey}}http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name{{else}}{{.UsernameAssertionKey}}{{end}}">
-	</div>
-
-</div>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 1b4e2b25f9..0d0064b02a 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -69,22 +69,5 @@
 		</div>
 	</div>
 	{{end}}
-	{{if .SAMLProviders}}
-	<div class="divider divider-text">
-		{{.locale.Tr "sign_in_or"}}
-	</div>
-	<div id="saml-login-navigator" class="gt-py-2">
-		<div class="gt-df gt-fc gt-jc">
-			<div id="saml-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
-				{{range $provider := .SAMLProviders}}
-					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 saml-login-link" href="{{AppSubUrl}}/user/saml/{{$provider.Name}}">
-						{{.IconHTML 28}}
-						{{ctx.Locale.Tr "sign_in_with_provider" $provider.Name}}
-					</a>
-				{{end}}
-			</div>
-		</div>
-	</div>
-	{{end}}
 	</form>
 </div>
diff --git a/tests/integration/README.md b/tests/integration/README.md
index c691483511..f6f74ca21f 100644
--- a/tests/integration/README.md
+++ b/tests/integration/README.md
@@ -110,20 +110,3 @@ SLOW_FLUSH = 5S ; 5s is the default value
 ```bash
 GITEA_SLOW_TEST_TIME="10s" GITEA_SLOW_FLUSH_TIME="5s" make test-sqlite
 ```
-
-## Running SimpleSAML for testing SAML locally
-
-```shell
-docker run \
--p 8080:8080 \
--p 8443:8443 \
--e SIMPLESAMLPHP_SP_ENTITY_ID=http://localhost:3003/user/saml/test-sp/metadata \
--e SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://localhost:3003/user/saml/test-sp/acs \
--e SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE=http://localhost:3003/user/saml/test-sp/acs \
---add-host=localhost:192.168.65.2 \
--d allspice/simple-saml
-```
-
-```shell
-TEST_SIMPLESAML_URL=localhost:8080 make test-sqlite#TestSAMLRegistration
-```
diff --git a/tests/integration/saml_test.go b/tests/integration/saml_test.go
deleted file mode 100644
index 585fd35c5f..0000000000
--- a/tests/integration/saml_test.go
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package integration
-
-import (
-	"crypto/tls"
-	"crypto/x509"
-	"fmt"
-	"io"
-	"net/http"
-	"net/http/cookiejar"
-	"net/url"
-	"os"
-	"regexp"
-	"strings"
-	"testing"
-	"time"
-
-	"code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/models/db"
-	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/test"
-	"code.gitea.io/gitea/services/auth/source/saml"
-	"code.gitea.io/gitea/tests"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestSAMLRegistration(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
-
-	samlURL := "localhost:8080"
-
-	if os.Getenv("CI") == "" || !setting.Database.Type.IsPostgreSQL() {
-		// Make it possible to run tests against a local simplesaml instance
-		samlURL = os.Getenv("TEST_SIMPLESAML_URL")
-		if samlURL == "" {
-			t.Skip("TEST_SIMPLESAML_URL not set and not running in CI")
-			return
-		}
-	}
-
-	privateKey, cert, err := saml.GenerateSAMLSPKeypair()
-	assert.NoError(t, err)
-
-	// verify that the keypair can be parsed
-	keyPair, err := tls.X509KeyPair([]byte(cert), []byte(privateKey))
-	assert.NoError(t, err)
-	keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
-	assert.NoError(t, err)
-
-	assert.NoError(t, auth.CreateSource(db.DefaultContext, &auth.Source{
-		Type:          auth.SAML,
-		Name:          "test-sp",
-		IsActive:      true,
-		IsSyncEnabled: false,
-		Cfg: &saml.Source{
-			IdentityProviderMetadata:                 "",
-			IdentityProviderMetadataURL:              fmt.Sprintf("http://%s/simplesaml/saml2/idp/metadata.php", samlURL),
-			InsecureSkipAssertionSignatureValidation: false,
-			NameIDFormat:                             4,
-			ServiceProviderCertificate:               "", // SimpleSAMLPhp requires that the SP certificate be specified in the server configuration rather than SP metadata
-			ServiceProviderPrivateKey:                "",
-			EmailAssertionKey:                        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
-			NameAssertionKey:                         "http://schemas.xmlsoap.org/claims/CommonName",
-			UsernameAssertionKey:                     "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
-			IconURL:                                  "",
-		},
-	}))
-
-	// check the saml metadata url
-	req := NewRequest(t, "GET", "/user/saml/test-sp/metadata")
-	MakeRequest(t, req, http.StatusOK)
-
-	req = NewRequest(t, "GET", "/user/saml/test-sp")
-	resp := MakeRequest(t, req, http.StatusTemporaryRedirect)
-
-	jar, err := cookiejar.New(nil)
-	assert.NoError(t, err)
-
-	client := http.Client{
-		Timeout: 30 * time.Second,
-		Jar:     jar,
-	}
-
-	httpReq, err := http.NewRequest("GET", test.RedirectURL(resp), nil)
-	assert.NoError(t, err)
-
-	var formRedirectURL *url.URL
-	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
-		// capture the redirected destination to use in POST request
-		formRedirectURL = req.URL
-		return nil
-	}
-
-	res, err := client.Do(httpReq)
-	client.CheckRedirect = nil
-	assert.NoError(t, err)
-	assert.Equal(t, http.StatusOK, res.StatusCode)
-	assert.NotNil(t, formRedirectURL)
-
-	form := url.Values{
-		"username": {"user1"},
-		"password": {"user1pass"},
-	}
-
-	httpReq, err = http.NewRequest("POST", formRedirectURL.String(), strings.NewReader(form.Encode()))
-	assert.NoError(t, err)
-	httpReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-
-	res, err = client.Do(httpReq)
-	assert.NoError(t, err)
-	assert.Equal(t, http.StatusOK, res.StatusCode)
-
-	body, err := io.ReadAll(res.Body)
-	assert.NoError(t, err)
-
-	samlResMatcher := regexp.MustCompile(`<input.*?name="SAMLResponse".*?value="([^"]+)".*?>`)
-	matches := samlResMatcher.FindStringSubmatch(string(body))
-	assert.Len(t, matches, 2)
-	assert.NoError(t, res.Body.Close())
-
-	session := emptyTestSession(t)
-
-	req = NewRequestWithValues(t, "POST", "/user/saml/test-sp/acs", map[string]string{
-		"SAMLResponse": matches[1],
-	})
-	resp = session.MakeRequest(t, req, http.StatusSeeOther)
-	assert.Equal(t, test.RedirectURL(resp), "/user/link_account")
-
-	csrf := GetCSRF(t, session, test.RedirectURL(resp))
-
-	// link the account
-	req = NewRequestWithValues(t, "POST", "/user/link_account_signup", map[string]string{
-		"_csrf":     csrf,
-		"user_name": "samluser",
-		"email":     "saml@example.com",
-	})
-
-	resp = session.MakeRequest(t, req, http.StatusSeeOther)
-	assert.Equal(t, test.RedirectURL(resp), "/")
-
-	// verify that the user was created
-	u, err := user_model.GetUserByEmail(db.DefaultContext, "saml@example.com")
-	assert.NoError(t, err)
-	assert.NotNil(t, u)
-	assert.Equal(t, "samluser", u.Name)
-}
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 4804163971..044976ea7b 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -103,9 +103,9 @@ export function initAdminCommon() {
   // New authentication
   if ($('.admin.new.authentication').length > 0) {
     $('#auth_type').on('change', function () {
-      hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi, .saml'));
+      hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi'));
 
-      $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required], .saml input[required]').removeAttr('required');
+      $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]').removeAttr('required');
       $('.binddnrequired').removeClass('required');
 
       const authType = $(this).val();
@@ -137,10 +137,6 @@ export function initAdminCommon() {
           showElem($('.sspi'));
           $('.sspi div.required input').attr('required', 'required');
           break;
-        case '8': // SAML
-          showElem($('.saml'));
-          $('.saml div.required input').attr('required', 'required');
-          break;
       }
       if (authType === '2' || authType === '5') {
         onSecurityProtocolChange();
diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js
index 3bf84e31df..60d186e699 100644
--- a/web_src/js/features/user-auth.js
+++ b/web_src/js/features/user-auth.js
@@ -20,24 +20,3 @@ export function initUserAuthOauth2() {
     });
   }
 }
-
-export function initUserAuthSAML() {
-  const outer = document.getElementById('saml-login-navigator');
-  if (!outer) return;
-  const inner = document.getElementById('saml-login-navigator-inner');
-
-  checkAppUrl();
-
-  for (const link of outer.querySelectorAll('.saml-login-link')) {
-    link.addEventListener('click', () => {
-      inner.classList.add('gt-invisible');
-      outer.classList.add('is-loading');
-      setTimeout(() => {
-        // recover previous content to let user try again
-        // usually redirection will be performed before this action
-        outer.classList.remove('is-loading');
-        inner.classList.remove('gt-invisible');
-      }, 5000);
-    });
-  }
-}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 876e4291ee..d9cfff4084 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -23,10 +23,7 @@ import {initFindFileInRepo} from './features/repo-findfile.js';
 import {initCommentContent, initMarkupContent} from './markup/content.js';
 import {initPdfViewer} from './render/pdf.js';
 
-import {
-  initUserAuthOauth2,
-  initUserAuthSAML
-} from './features/user-auth.js';
+import {initUserAuthOauth2} from './features/user-auth.js';
 import {
   initRepoIssueDue,
   initRepoIssueReferenceRepositorySearch,
@@ -184,7 +181,6 @@ onDomReady(() => {
   initCaptcha();
 
   initUserAuthOauth2();
-  initUserAuthSAML();
   initUserAuthWebAuthn();
   initUserAuthWebAuthnRegister();
   initUserSettings();

From b79c30435f439af8243ee281310258cdf141e27b Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 24 Feb 2024 14:55:19 +0800
Subject: [PATCH 145/679] Use the database object format name but not read from
 git repoisitory everytime and fix possible migration wrong objectformat when
 migrating a sha256 repository (#29294)

Now we can get object format name from git command line or from the
database repository table. Assume the column is right, we don't need to
read from git command line every time.

This also fixed a possible bug that the object format is wrong when
migrating a sha256 repository from external.

<img width="658" alt="image"
src="https://github.com/go-gitea/gitea/assets/81045/6e9a9dcf-13bf-4267-928b-6bf2c2560423">
---
 modules/context/api.go                |  9 ++-------
 modules/context/repo.go               | 20 ++++++++------------
 routers/api/v1/utils/git.go           |  2 +-
 routers/private/hook_pre_receive.go   |  2 +-
 routers/web/repo/blame.go             |  7 ++-----
 routers/web/repo/compare.go           |  4 ++--
 routers/web/repo/setting/lfs.go       |  2 +-
 services/agit/agit.go                 |  3 +--
 services/migrations/gitea_uploader.go | 16 +++++++++++++---
 services/pull/check.go                |  5 +----
 services/pull/merge.go                |  2 +-
 services/release/release.go           |  2 +-
 services/repository/branch.go         |  7 ++-----
 services/repository/files/commit.go   |  6 ++----
 services/repository/files/tree.go     |  2 +-
 services/repository/lfs.go            |  2 +-
 services/repository/push.go           |  6 +-----
 17 files changed, 41 insertions(+), 56 deletions(-)

diff --git a/modules/context/api.go b/modules/context/api.go
index f8bc682fed..b18a206b5e 100644
--- a/modules/context/api.go
+++ b/modules/context/api.go
@@ -307,12 +307,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 			return
 		}
 
-		objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
-		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "GetCommit", err)
-			return
-		}
-
 		if ref := ctx.FormTrim("ref"); len(ref) > 0 {
 			commit, err := ctx.Repo.GitRepo.GetCommit(ref)
 			if err != nil {
@@ -331,6 +325,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 		}
 
 		refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
+		var err error
 
 		if ctx.Repo.GitRepo.IsBranchExist(refName) {
 			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
@@ -346,7 +341,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 				return
 			}
 			ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
-		} else if len(refName) == objectFormat.FullLength() {
+		} else if len(refName) == ctx.Repo.GetObjectFormat().FullLength() {
 			ctx.Repo.CommitID = refName
 			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
 			if err != nil {
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 8508d46cf4..a73d09ee21 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -83,6 +83,10 @@ func (r *Repository) CanCreateBranch() bool {
 	return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
 }
 
+func (r *Repository) GetObjectFormat() git.ObjectFormat {
+	return git.ObjectFormatFromName(r.Repository.ObjectFormatName)
+}
+
 // RepoMustNotBeArchived checks if a repo is archived
 func RepoMustNotBeArchived() func(ctx *Context) {
 	return func(ctx *Context) {
@@ -830,9 +834,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
 		}
 		// For legacy and API support only full commit sha
 		parts := strings.Split(path, "/")
-		objectFormat, _ := repo.GitRepo.GetObjectFormat()
 
-		if len(parts) > 0 && len(parts[0]) == objectFormat.FullLength() {
+		if len(parts) > 0 && len(parts[0]) == git.ObjectFormatFromName(repo.Repository.ObjectFormatName).FullLength() {
 			repo.TreePath = strings.Join(parts[1:], "/")
 			return parts[0]
 		}
@@ -876,9 +879,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
 		return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
 	case RepoRefCommit:
 		parts := strings.Split(path, "/")
-		objectFormat, _ := repo.GitRepo.GetObjectFormat()
 
-		if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= objectFormat.FullLength() {
+		if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= repo.GetObjectFormat().FullLength() {
 			repo.TreePath = strings.Join(parts[1:], "/")
 			return parts[0]
 		}
@@ -937,12 +939,6 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 			}
 		}
 
-		objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
-		if err != nil {
-			log.Error("Cannot determine objectFormat for repository: %w", err)
-			ctx.Repo.Repository.MarkAsBrokenEmpty()
-		}
-
 		// Get default branch.
 		if len(ctx.Params("*")) == 0 {
 			refName = ctx.Repo.Repository.DefaultBranch
@@ -1009,7 +1005,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 					return cancel
 				}
 				ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
-			} else if len(refName) >= 7 && len(refName) <= objectFormat.FullLength() {
+			} else if len(refName) >= 7 && len(refName) <= ctx.Repo.GetObjectFormat().FullLength() {
 				ctx.Repo.IsViewCommit = true
 				ctx.Repo.CommitID = refName
 
@@ -1019,7 +1015,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 					return cancel
 				}
 				// If short commit ID add canonical link header
-				if len(refName) < objectFormat.FullLength() {
+				if len(refName) < ctx.Repo.GetObjectFormat().FullLength() {
 					ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
 						util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
 				}
diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go
index 2299cdc247..5e80190017 100644
--- a/routers/api/v1/utils/git.go
+++ b/routers/api/v1/utils/git.go
@@ -72,7 +72,7 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str
 
 // ConvertToObjectID returns a full-length SHA1 from a potential ID string
 func ConvertToObjectID(ctx gocontext.Context, repo *context.Repository, commitID string) (git.ObjectID, error) {
-	objectFormat, _ := repo.GitRepo.GetObjectFormat()
+	objectFormat := repo.GetObjectFormat()
 	if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
 		sha, err := git.NewIDFromString(commitID)
 		if err == nil {
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index 90d8287f06..f28ae4c0eb 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -145,7 +145,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
 
 	repo := ctx.Repo.Repository
 	gitRepo := ctx.Repo.GitRepo
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	objectFormat := ctx.Repo.GetObjectFormat()
 
 	if branchName == repo.DefaultBranch && newCommitID == objectFormat.EmptyObjectID().String() {
 		log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index c7875ea0cb..7602b30d2b 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -132,11 +132,8 @@ type blameResult struct {
 }
 
 func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
-	objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
-	if err != nil {
-		ctx.NotFound("CreateBlameReader", err)
-		return nil, err
-	}
+	objectFormat := ctx.Repo.GetObjectFormat()
+
 	blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore)
 	if err != nil {
 		return nil, err
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index df41c750de..535487d5fd 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -312,14 +312,14 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
 	baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
 	baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
 	baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
-	objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat()
+
 	if !baseIsCommit && !baseIsBranch && !baseIsTag {
 		// Check if baseBranch is short sha commit hash
 		if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
 			ci.BaseBranch = baseCommit.ID.String()
 			ctx.Data["BaseBranch"] = ci.BaseBranch
 			baseIsCommit = true
-		} else if ci.BaseBranch == objectFormat.EmptyObjectID().String() {
+		} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
 			if isSameRepo {
 				ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
 			} else {
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index cd0f11d548..76a90a4ac5 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -388,7 +388,7 @@ func LFSFileFind(ctx *context.Context) {
 	sha := ctx.FormString("sha")
 	ctx.Data["Title"] = oid
 	ctx.Data["PageIsSettingsLFS"] = true
-	objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat()
+	objectFormat := ctx.Repo.GetObjectFormat()
 	var objectID git.ObjectID
 	if len(sha) == 0 {
 		pointer := lfs.Pointer{Oid: oid, Size: size}
diff --git a/services/agit/agit.go b/services/agit/agit.go
index 75b561581d..2233fe8547 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -36,7 +36,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 
 	topicBranch = opts.GitPushOptions["topic"]
 	_, forcePush = opts.GitPushOptions["force-push"]
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 
 	pusher, err := user_model.GetUserByID(ctx, opts.UserID)
 	if err != nil {
@@ -149,7 +149,6 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 
 			log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
 
-			objectFormat, _ := gitRepo.GetObjectFormat()
 			results = append(results, private.HookProcReceiveRefResult{
 				Ref:         pr.GetGitRefName(),
 				OriginalRef: opts.RefFullNames[i],
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 2891977c7c..468be6c9df 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -140,8 +140,18 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
 	if err != nil {
 		return err
 	}
-	g.gitRepo, err = gitrepo.OpenRepository(g.ctx, r)
-	return err
+	g.gitRepo, err = gitrepo.OpenRepository(g.ctx, g.repo)
+	if err != nil {
+		return err
+	}
+
+	// detect object format from git repository and update to database
+	objectFormat, err := g.gitRepo.GetObjectFormat()
+	if err != nil {
+		return err
+	}
+	g.repo.ObjectFormatName = objectFormat.Name()
+	return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "object_format_name")
 }
 
 // Close closes this uploader
@@ -896,7 +906,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 				comment.UpdatedAt = comment.CreatedAt
 			}
 
-			objectFormat, _ := g.gitRepo.GetObjectFormat()
+			objectFormat := git.ObjectFormatFromName(g.repo.ObjectFormatName)
 			if !objectFormat.IsValid(comment.CommitID) {
 				log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID)
 				comment.CommitID = headCommitID
diff --git a/services/pull/check.go b/services/pull/check.go
index dd6c3ed230..f4dd332b14 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -222,10 +222,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com
 	}
 	defer gitRepo.Close()
 
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return nil, fmt.Errorf("%-v GetObjectFormat: %w", pr.BaseRepo, err)
-	}
+	objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
 
 	// Get the commit from BaseBranch where the pull request got merged
 	mergeCommit, _, err := git.NewCommand(ctx, "rev-list", "--ancestry-path", "--merges", "--reverse").
diff --git a/services/pull/merge.go b/services/pull/merge.go
index d4c0c821d6..e37540a96f 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -497,7 +497,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
 			return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged}
 		}
 
-		objectFormat, _ := baseGitRepo.GetObjectFormat()
+		objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
 		if len(commitID) != objectFormat.FullLength() {
 			return fmt.Errorf("Wrong commit ID")
 		}
diff --git a/services/release/release.go b/services/release/release.go
index 4c522c18be..a359e5078e 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -88,7 +88,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
 			created = true
 			rel.LowerTagName = strings.ToLower(rel.TagName)
 
-			objectFormat, _ := gitRepo.GetObjectFormat()
+			objectFormat := git.ObjectFormatFromName(rel.Repo.ObjectFormatName)
 			commits := repository.NewPushCommits()
 			commits.HeadCommit = repository.CommitToPushCommit(commit)
 			commits.CompareURL = rel.Repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), commit.ID.String())
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 38781acb58..ec41173da8 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -380,11 +380,6 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
 		return fmt.Errorf("GetBranch: %vc", err)
 	}
 
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return err
-	}
-
 	if rawBranch.IsDeleted {
 		return nil
 	}
@@ -406,6 +401,8 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
 		return err
 	}
 
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
 	// Don't return error below this
 	if err := PushUpdate(
 		&repo_module.PushUpdateOptions{
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index 16a15e06a7..512aec7c81 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -30,10 +30,8 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
 	}
 	defer closer.Close()
 
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return fmt.Errorf("GetObjectFormat[%s]: %w", repoPath, err)
-	}
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
 	commit, err := gitRepo.GetCommit(sha)
 	if err != nil {
 		gitRepo.Close()
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index 9d3185c3fc..e3a7f3b8b0 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -37,7 +37,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 	}
 	apiURL := repo.APIURL()
 	apiURLLen := len(apiURL)
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 	hashLen := objectFormat.FullLength()
 
 	const gitBlobsPath = "/git/blobs/"
diff --git a/services/repository/lfs.go b/services/repository/lfs.go
index 4504f796bd..4d48881b87 100644
--- a/services/repository/lfs.go
+++ b/services/repository/lfs.go
@@ -79,7 +79,7 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R
 
 	store := lfs.NewContentStore()
 	errStop := errors.New("STOPERR")
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 
 	err = git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, metaObject *git_model.LFSMetaObject, count int64) error {
 		if opts.NumberToCheckPerRepo > 0 && total > opts.NumberToCheckPerRepo {
diff --git a/services/repository/push.go b/services/repository/push.go
index c76025b6a7..9aaf0e1c9b 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -93,11 +93,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 	}
 	defer gitRepo.Close()
 
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return fmt.Errorf("unknown repository ObjectFormat [%s]: %w", repo.FullName(), err)
-	}
-
 	if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
 		return fmt.Errorf("Failed to update size for repository: %v", err)
 	}
@@ -105,6 +100,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 	addTags := make([]string, 0, len(optsList))
 	delTags := make([]string, 0, len(optsList))
 	var pusher *user_model.User
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 
 	for _, opts := range optsList {
 		log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName)

From 6e5966597c2d498d1a8540dad965461d44ff8e57 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Br=C3=BCckner?= <code@nik.dev>
Date: Sat, 24 Feb 2024 07:49:16 +0000
Subject: [PATCH 146/679] Properly migrate target branch change GitLab comment
 (#29340)

GitLab generates "system notes" whenever an event happens within the
platform. Unlike Gitea, those events are stored and retrieved as text
comments with no semantic details. The only way to tell whether a
comment was generated in this manner is the `system` flag on the note
type.

This PR adds detection for a new specific kind of event: Changing the
target branch of a PR. When detected, it is downloaded using Gitea's
type for this event, and eventually uploaded into Gitea in the expected
format, i.e. with no text content in the comment.

This PR also updates the template used to render comments to add support
for migrated comments of this type.

ref:
https://gitlab.com/gitlab-org/gitlab/-/blob/11bd6dc826e0bea2832324a1d7356949a9398884/app/services/system_notes/merge_requests_service.rb#L102
---
 services/migrations/gitea_uploader.go         | 10 ++++++++--
 services/migrations/gitlab.go                 | 10 +++++++++-
 services/migrations/gitlab_test.go            | 19 ++++++++++++++++++-
 .../repo/issue/view_content/comments.tmpl     | 19 +++++++++++++++----
 4 files changed, 50 insertions(+), 8 deletions(-)

diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 468be6c9df..8bcf483947 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -492,10 +492,16 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 			}
 		case issues_model.CommentTypeChangeTitle:
 			if comment.Meta["OldTitle"] != nil {
-				cm.OldTitle = fmt.Sprintf("%s", comment.Meta["OldTitle"])
+				cm.OldTitle = fmt.Sprint(comment.Meta["OldTitle"])
 			}
 			if comment.Meta["NewTitle"] != nil {
-				cm.NewTitle = fmt.Sprintf("%s", comment.Meta["NewTitle"])
+				cm.NewTitle = fmt.Sprint(comment.Meta["NewTitle"])
+			}
+		case issues_model.CommentTypeChangeTargetBranch:
+			if comment.Meta["OldRef"] != nil && comment.Meta["NewRef"] != nil {
+				cm.OldRef = fmt.Sprint(comment.Meta["OldRef"])
+				cm.NewRef = fmt.Sprint(comment.Meta["NewRef"])
+				cm.Content = ""
 			}
 		case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge:
 			cm.Content = ""
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index d08eaf0f84..5e49ae6d57 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -11,6 +11,7 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"regexp"
 	"strings"
 	"time"
 
@@ -519,6 +520,8 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
 	return allComments, true, nil
 }
 
+var targetBranchChangeRegexp = regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$")
+
 func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment {
 	comment := &base.Comment{
 		IssueIndex:  localIndex,
@@ -528,11 +531,16 @@ func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.N
 		PosterEmail: note.Author.Email,
 		Content:     note.Body,
 		Created:     *note.CreatedAt,
+		Meta:        map[string]any{},
 	}
 
 	// Try to find the underlying event of system notes.
 	if note.System {
-		if strings.HasPrefix(note.Body, "enabled an automatic merge") {
+		if match := targetBranchChangeRegexp.FindStringSubmatch(note.Body); match != nil {
+			comment.CommentType = issues_model.CommentTypeChangeTargetBranch.String()
+			comment.Meta["OldRef"] = match[1]
+			comment.Meta["NewRef"] = match[2]
+		} else if strings.HasPrefix(note.Body, "enabled an automatic merge") {
 			comment.CommentType = issues_model.CommentTypePRScheduledToAutoMerge.String()
 		} else if note.Body == "canceled the automatic merge" {
 			comment.CommentType = issues_model.CommentTypePRUnScheduledToAutoMerge.String()
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 2b87a1dfe6..0b9eeaed54 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -545,7 +545,8 @@ func TestNoteToComment(t *testing.T) {
 	notes := []gitlab.Note{
 		makeTestNote(1, "This is a regular comment", false),
 		makeTestNote(2, "enabled an automatic merge for abcd1234", true),
-		makeTestNote(3, "canceled the automatic merge", true),
+		makeTestNote(3, "changed target branch from `master` to `main`", true),
+		makeTestNote(4, "canceled the automatic merge", true),
 	}
 	comments := []base.Comment{{
 		IssueIndex:  17,
@@ -556,6 +557,7 @@ func TestNoteToComment(t *testing.T) {
 		CommentType: "",
 		Content:     "This is a regular comment",
 		Created:     now,
+		Meta:        map[string]any{},
 	}, {
 		IssueIndex:  17,
 		Index:       2,
@@ -565,15 +567,30 @@ func TestNoteToComment(t *testing.T) {
 		CommentType: "pull_scheduled_merge",
 		Content:     "enabled an automatic merge for abcd1234",
 		Created:     now,
+		Meta:        map[string]any{},
 	}, {
 		IssueIndex:  17,
 		Index:       3,
 		PosterID:    72,
 		PosterName:  "test",
 		PosterEmail: "test@example.com",
+		CommentType: "change_target_branch",
+		Content:     "changed target branch from `master` to `main`",
+		Created:     now,
+		Meta: map[string]any{
+			"OldRef": "master",
+			"NewRef": "main",
+		},
+	}, {
+		IssueIndex:  17,
+		Index:       4,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
 		CommentType: "pull_cancel_scheduled_merge",
 		Content:     "canceled the automatic merge",
 		Created:     now,
+		Meta:        map[string]any{},
 	}}
 
 	for i, note := range notes {
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 597f025470..7bd7e8c35d 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -365,8 +365,7 @@
 		{{else if eq .Type 22}}
 			<div class="timeline-item-group" id="{{.HashTag}}">
 				<div class="timeline-item event">
-					{{if .OriginalAuthor}}
-					{{else}}
+					{{if not .OriginalAuthor}}
 					{{/* Some timeline avatars need a offset to correctly align with their speech
 							bubble. The condition depends on review type and for positive reviews whether
 							there is a comment element or not */}}
@@ -495,9 +494,21 @@
 		{{else if eq .Type 25}}
 			<div class="timeline-item event">
 				<span class="badge">{{svg "octicon-git-branch"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					<a{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.Name}}</a>
+					{{if .OriginalAuthor}}
+						<span class="text black">
+							{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
+							{{.OriginalAuthor}}
+						</span>
+						{{if $.Repository.OriginalURL}}
+						<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
+						{{end}}
+					{{else}}
+						{{template "shared/user/authorlink" .Poster}}
+					{{end}}
 					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr}}
 				</span>
 			</div>

From 0a426cc575734e5eff410d6a790f40473117f753 Mon Sep 17 00:00:00 2001
From: qwerty287 <80460567+qwerty287@users.noreply.github.com>
Date: Sat, 24 Feb 2024 09:18:39 +0100
Subject: [PATCH 147/679] Add API to get merged PR of a commit (#29243)

Adds a new API `/repos/{owner}/{repo}/commits/{sha}/pull` that allows
you to get the merged PR associated to a commit.

---------

Co-authored-by: 6543 <6543@obermui.de>
---
 models/fixtures/pull_request.yml |  1 +
 models/issues/pull.go            | 20 +++++++++++++
 models/issues/pull_test.go       | 12 ++++++++
 routers/api/v1/api.go            |  1 +
 routers/api/v1/repo/commits.go   | 51 ++++++++++++++++++++++++++++++++
 templates/swagger/v1_json.tmpl   | 43 +++++++++++++++++++++++++++
 6 files changed, 128 insertions(+)

diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml
index 560674c370..54590fb830 100644
--- a/models/fixtures/pull_request.yml
+++ b/models/fixtures/pull_request.yml
@@ -9,6 +9,7 @@
   head_branch: branch1
   base_branch: master
   merge_base: 4a357436d925b5c974181ff12a994538ddc5a269
+  merged_commit_id: 1a8823cd1a9549fde083f992f6b9b87a7ab74fb3
   has_merged: true
   merger_id: 2
 
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 2cb1e1b971..18e6b2776d 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -1093,3 +1093,23 @@ func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error {
 	}
 	return committer.Commit()
 }
+
+// GetPullRequestByMergedCommit returns a merged pull request by the given commit
+func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string) (*PullRequest, error) {
+	pr := new(PullRequest)
+	has, err := db.GetEngine(ctx).Where("base_repo_id = ? AND merged_commit_id = ?", repoID, sha).Get(pr)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""}
+	}
+
+	if err = pr.LoadAttributes(ctx); err != nil {
+		return nil, err
+	}
+	if err = pr.LoadIssue(ctx); err != nil {
+		return nil, err
+	}
+
+	return pr, nil
+}
diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go
index 173417136c..3a30b2f3de 100644
--- a/models/issues/pull_test.go
+++ b/models/issues/pull_test.go
@@ -339,6 +339,18 @@ func TestGetApprovers(t *testing.T) {
 	assert.EqualValues(t, expected, approvers)
 }
 
+func TestGetPullRequestByMergedCommit(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	pr, err := issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, pr.ID)
+
+	_, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 0, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
+	assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
+	_, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "")
+	assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
+}
+
 func TestMigrate_InsertPullRequests(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	reponame := "repo1"
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 3fafb96b8e..e7bdef1489 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1235,6 +1235,7 @@ func Routes() *web.Route {
 					m.Group("/{ref}", func() {
 						m.Get("/status", repo.GetCombinedCommitStatusByRef)
 						m.Get("/statuses", repo.GetCommitStatusesByRef)
+						m.Get("/pull", repo.GetCommitPullRequest)
 					}, context.ReferencesGitRepo())
 				}, reqRepoReader(unit.TypeCode))
 				m.Group("/git", func() {
diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go
index 43b6400009..d01cf6b8bc 100644
--- a/routers/api/v1/repo/commits.go
+++ b/routers/api/v1/repo/commits.go
@@ -10,6 +10,7 @@ import (
 	"net/http"
 	"strconv"
 
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
@@ -323,3 +324,53 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
 		return
 	}
 }
+
+// GetCommitPullRequest returns the pull request of the commit
+func GetCommitPullRequest(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest
+	// ---
+	// summary: Get the pull request of the commit
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: sha
+	//   in: path
+	//   description: SHA of the commit to get
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/PullRequest"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.Params(":sha"))
+	if err != nil {
+		if issues_model.IsErrPullRequestNotExist(err) {
+			ctx.Error(http.StatusNotFound, "GetPullRequestByMergedCommit", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	if err = pr.LoadBaseRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
+		return
+	}
+	if err = pr.LoadHeadRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index d26bed53aa..eaa1448b2b 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -4565,6 +4565,49 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/commits/{sha}/pull": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get the pull request of the commit",
+        "operationId": "repoGetCommitPullRequest",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "SHA of the commit to get",
+            "name": "sha",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/PullRequest"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/contents": {
       "get": {
         "produces": [

From d3982bcd814bac93e3cbce1c7eb749b17e413fbd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= <sahin@sahinakkaya.dev>
Date: Sat, 24 Feb 2024 13:22:51 +0300
Subject: [PATCH 148/679] Implement recent commits graph (#29210)

This is the implementation of Recent Commits page. This feature was
mentioned on #18262.

It adds another tab to Activity page called Recent Commits. Recent
Commits tab shows number of commits since last year for the repository.
---
 options/locale/locale_en-US.ini             |   4 +-
 routers/web/repo/recent_commits.go          |  41 ++++++
 routers/web/web.go                          |   4 +
 templates/repo/activity.tmpl                |   1 +
 templates/repo/navbar.tmpl                  |   3 +
 templates/repo/recent_commits.tmpl          |   9 ++
 web_src/js/components/RepoRecentCommits.vue | 149 ++++++++++++++++++++
 web_src/js/features/recent-commits.js       |  21 +++
 web_src/js/index.js                         |   2 +
 9 files changed, 233 insertions(+), 1 deletion(-)
 create mode 100644 routers/web/repo/recent_commits.go
 create mode 100644 templates/repo/recent_commits.tmpl
 create mode 100644 web_src/js/components/RepoRecentCommits.vue
 create mode 100644 web_src/js/features/recent-commits.js

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 2c92f40a17..ff6a3f1b8e 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1915,8 +1915,9 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend
 
 activity = Activity
 activity.navbar.pulse = Pulse
-activity.navbar.contributors = Contributors
 activity.navbar.code_frequency = Code Frequency
+activity.navbar.contributors = Contributors
+activity.navbar.recent_commits = Recent Commits
 activity.period.filter_label = Period:
 activity.period.daily = 1 day
 activity.period.halfweekly = 3 days
@@ -2597,6 +2598,7 @@ component_loading_info = This might take a bit…
 component_failed_to_load = An unexpected error happened.
 code_frequency.what = code frequency
 contributors.what = contributions
+recent_commits.what = recent commits
 
 [org]
 org_name_holder = Organization Name
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
new file mode 100644
index 0000000000..3507cb8752
--- /dev/null
+++ b/routers/web/repo/recent_commits.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplRecentCommits base.TplName = "repo/activity"
+)
+
+// RecentCommits renders the page to show recent commit frequency on repository
+func RecentCommits(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.recent_commits")
+
+	ctx.Data["PageIsActivity"] = true
+	ctx.Data["PageIsRecentCommits"] = true
+	ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+	ctx.HTML(http.StatusOK, tplRecentCommits)
+}
+
+// RecentCommitsData returns JSON of recent commits data
+func RecentCommitsData(ctx *context.Context) {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+			ctx.Status(http.StatusAccepted)
+			return
+		}
+		ctx.ServerError("RecentCommitsData", err)
+	} else {
+		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+	}
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index a76b444e4f..8505417c88 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1402,6 +1402,10 @@ func registerRoutes(m *web.Route) {
 				m.Get("", repo.CodeFrequency)
 				m.Get("/data", repo.CodeFrequencyData)
 			})
+			m.Group("/recent-commits", func() {
+				m.Get("", repo.RecentCommits)
+				m.Get("/data", repo.RecentCommitsData)
+			})
 		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
 
 		m.Group("/activity_author_data", func() {
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index 94f52b0e26..a19fb66261 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -9,6 +9,7 @@
 			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
 			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
 			{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
+			{{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}}
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
index aa5021e73a..b2471dc17e 100644
--- a/templates/repo/navbar.tmpl
+++ b/templates/repo/navbar.tmpl
@@ -8,4 +8,7 @@
 	<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
 		{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
 	</a>
+	<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
+		{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
+	</a>
 </div>
diff --git a/templates/repo/recent_commits.tmpl b/templates/repo/recent_commits.tmpl
new file mode 100644
index 0000000000..5c241d635c
--- /dev/null
+++ b/templates/repo/recent_commits.tmpl
@@ -0,0 +1,9 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+	<div id="repo-recent-commits-chart"
+		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
+		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
+		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
+	>
+	</div>
+{{end}}
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
new file mode 100644
index 0000000000..77697cd413
--- /dev/null
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -0,0 +1,149 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+  Chart,
+  Tooltip,
+  BarElement,
+  LinearScale,
+  TimeScale,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import {Bar} from 'vue-chartjs';
+import {
+  startDaysBetween,
+  firstStartDateAfterDate,
+  fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+
+const {pageData} = window.config;
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+  TimeScale,
+  LinearScale,
+  BarElement,
+  Tooltip,
+);
+
+export default {
+  components: {Bar, SvgIcon},
+  props: {
+    locale: {
+      type: Object,
+      required: true
+    },
+  },
+  data: () => ({
+    isLoading: false,
+    errorText: '',
+    repoLink: pageData.repoLink || [],
+    data: [],
+  }),
+  mounted() {
+    this.fetchGraphData();
+  },
+  methods: {
+    async fetchGraphData() {
+      this.isLoading = true;
+      try {
+        let response;
+        do {
+          response = await GET(`${this.repoLink}/activity/recent-commits/data`);
+          if (response.status === 202) {
+            await sleep(1000); // wait for 1 second before retrying
+          }
+        } while (response.status === 202);
+        if (response.ok) {
+          const data = await response.json();
+          const start = Object.values(data)[0].week;
+          const end = firstStartDateAfterDate(new Date());
+          const startDays = startDaysBetween(new Date(start), new Date(end));
+          this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
+          this.errorText = '';
+        } else {
+          this.errorText = response.statusText;
+        }
+      } catch (err) {
+        this.errorText = err.message;
+      } finally {
+        this.isLoading = false;
+      }
+    },
+
+    toGraphData(data) {
+      return {
+        datasets: [
+          {
+            data: data.map((i) => ({x: i.week, y: i.commits})),
+            label: 'Commits',
+            backgroundColor: chartJsColors['commits'],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+        ],
+      };
+    },
+
+    getOptions() {
+      return {
+        responsive: true,
+        maintainAspectRatio: false,
+        animation: true,
+        scales: {
+          x: {
+            type: 'time',
+            grid: {
+              display: false,
+            },
+            time: {
+              minUnit: 'week',
+            },
+            ticks: {
+              maxRotation: 0,
+              maxTicksLimit: 52
+            },
+          },
+          y: {
+            ticks: {
+              maxTicksLimit: 6
+            },
+          },
+        },
+      };
+    },
+  },
+};
+</script>
+<template>
+  <div>
+    <div class="ui header gt-df gt-ac gt-sb">
+      {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
+    </div>
+    <div class="gt-df ui segment main-graph">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
+        <div v-if="isLoading">
+          <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
+          {{ locale.loadingInfo }}
+        </div>
+        <div v-else class="text red">
+          <SvgIcon name="octicon-x-circle-fill"/>
+          {{ errorText }}
+        </div>
+      </div>
+      <Bar
+        v-memo="data" v-if="data.length !== 0"
+        :data="toGraphData(data)" :options="getOptions()"
+      />
+    </div>
+  </div>
+</template>
+<style scoped>
+.main-graph {
+  height: 250px;
+}
+</style>
diff --git a/web_src/js/features/recent-commits.js b/web_src/js/features/recent-commits.js
new file mode 100644
index 0000000000..ded10d39be
--- /dev/null
+++ b/web_src/js/features/recent-commits.js
@@ -0,0 +1,21 @@
+import {createApp} from 'vue';
+
+export async function initRepoRecentCommits() {
+  const el = document.getElementById('repo-recent-commits-chart');
+  if (!el) return;
+
+  const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue');
+  try {
+    const View = createApp(RepoRecentCommits, {
+      locale: {
+        loadingTitle: el.getAttribute('data-locale-loading-title'),
+        loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+        loadingInfo: el.getAttribute('data-locale-loading-info'),
+      }
+    });
+    View.mount(el);
+  } catch (err) {
+    console.error('RepoRecentCommits failed to load', err);
+    el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+  }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index d9cfff4084..b7f3ba99a0 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -85,6 +85,7 @@ import {initRepoIssueList} from './features/repo-issue-list.js';
 import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
 import {initRepoContributors} from './features/contributors.js';
 import {initRepoCodeFrequency} from './features/code-frequency.js';
+import {initRepoRecentCommits} from './features/recent-commits.js';
 import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 import {initDirAuto} from './modules/dirauto.js';
 
@@ -176,6 +177,7 @@ onDomReady(() => {
   initRepositoryActionView();
   initRepoContributors();
   initRepoCodeFrequency();
+  initRepoRecentCommits();
 
   initCommitStatuses();
   initCaptcha();

From 553d46e6f6a144905266d58315a2b0ff2e976380 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sat, 24 Feb 2024 12:45:59 +0100
Subject: [PATCH 149/679] Do not double close reader (#29354)

Fixes #29346

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/git/blob_nogogit.go | 12 +++++++++++-
 routers/web/repo/editor.go  |  3 ---
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go
index 9e1c2a0376..945a6bc432 100644
--- a/modules/git/blob_nogogit.go
+++ b/modules/git/blob_nogogit.go
@@ -102,7 +102,17 @@ func (b *blobReader) Read(p []byte) (n int, err error) {
 
 // Close implements io.Closer
 func (b *blobReader) Close() error {
+	if b.rd == nil {
+		return nil
+	}
+
 	defer b.cancel()
 
-	return DiscardFull(b.rd, b.n+1)
+	if err := DiscardFull(b.rd, b.n+1); err != nil {
+		return err
+	}
+
+	b.rd = nil
+
+	return nil
 }
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index bc3cb8801d..28644fbe3d 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -161,9 +161,6 @@ func editFile(ctx *context.Context, isNewFile bool) {
 		}
 
 		d, _ := io.ReadAll(dataRc)
-		if err := dataRc.Close(); err != nil {
-			log.Error("Error whilst closing blob data: %v", err)
-		}
 
 		buf = append(buf, d...)
 		if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {

From 267dbb4e938cc42dc09a4a893cca631b2f755557 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 24 Feb 2024 14:03:53 +0200
Subject: [PATCH 150/679] Remove jQuery from the issue reference context popup
 (#29367)

- Removed all jQuery calls
- Tested the context popup functionality and it works as before

# Demo without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/90b53de5-a8e9-4ed7-9236-1c9dfc324f38)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/components/ContextPopup.vue | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue
index d9e6da316c..3a1b828cca 100644
--- a/web_src/js/components/ContextPopup.vue
+++ b/web_src/js/components/ContextPopup.vue
@@ -1,8 +1,8 @@
 <script>
-import $ from 'jquery';
 import {SvgIcon} from '../svg.js';
 import {useLightTextOnBackground} from '../utils/color.js';
 import tinycolor from 'tinycolor2';
+import {GET} from '../modules/fetch.js';
 
 const {appSubUrl, i18n} = window.config;
 
@@ -80,20 +80,23 @@ export default {
     });
   },
   methods: {
-    load(data) {
+    async load(data) {
       this.loading = true;
       this.i18nErrorMessage = null;
-      $.get(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`).done((issue) => {
-        this.issue = issue;
-      }).fail((jqXHR) => {
-        if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
-          this.i18nErrorMessage = jqXHR.responseJSON.message;
-        } else {
-          this.i18nErrorMessage = i18n.network_error;
+
+      try {
+        const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`);
+        const respJson = await response.json();
+        if (!response.ok) {
+          this.i18nErrorMessage = respJson.message ?? i18n.network_error;
+          return;
         }
-      }).always(() => {
+        this.issue = respJson;
+      } catch {
+        this.i18nErrorMessage = i18n.network_error;
+      } finally {
         this.loading = false;
-      });
+      }
     }
   }
 };

From c42083a33950be6ee9f822c6d0de3c3a79d1f51b Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Sat, 24 Feb 2024 20:38:43 +0800
Subject: [PATCH 151/679] Allow non-admin users to delete review requests
 (#29057)

Fix #14459

The following users can add/remove review requests of a PR
- the poster of the PR
- the owner or collaborators of the repository
- members with read permission on the pull requests unit
---
 models/fixtures/access.yml                    |  24 +++
 models/fixtures/collaboration.yml             |   6 +
 models/fixtures/email_address.yml             |  24 +++
 models/fixtures/issue.yml                     |  34 ++++
 models/fixtures/org_user.yml                  |  18 +++
 models/fixtures/pull_request.yml              |  18 +++
 models/fixtures/repo_unit.yml                 |  42 +++++
 models/fixtures/repository.yml                |  62 ++++++++
 models/fixtures/team.yml                      |  22 +++
 models/fixtures/team_repo.yml                 |  12 ++
 models/fixtures/team_unit.yml                 |  36 +++++
 models/fixtures/team_user.yml                 |  18 +++
 models/fixtures/user.yml                      | 148 ++++++++++++++++++
 models/issues/issue_test.go                   |   2 +-
 models/repo/repo_list_test.go                 |   6 +-
 models/user/user_test.go                      |   6 +-
 modules/indexer/issues/indexer_test.go        |  14 +-
 routers/web/repo/issue.go                     |  21 +--
 services/issue/assignee.go                    | 145 ++++++++++-------
 .../org41/repo61.git/HEAD                     |   1 +
 .../org41/repo61.git/config                   |   6 +
 .../org41/repo61.git/description              |   1 +
 .../org41/repo61.git/info/exclude             |   6 +
 .../user40/repo60.git/HEAD                    |   1 +
 .../user40/repo60.git/config                  |   6 +
 .../user40/repo60.git/description             |   1 +
 .../user40/repo60.git/info/exclude            |   6 +
 tests/integration/api_issue_test.go           |   8 +-
 tests/integration/api_nodeinfo_test.go        |   4 +-
 tests/integration/api_org_test.go             |   4 +-
 tests/integration/api_pull_review_test.go     |  43 +++++
 tests/integration/api_repo_test.go            |   6 +-
 tests/integration/issue_test.go               |   8 +-
 33 files changed, 656 insertions(+), 103 deletions(-)
 create mode 100644 tests/gitea-repositories-meta/org41/repo61.git/HEAD
 create mode 100644 tests/gitea-repositories-meta/org41/repo61.git/config
 create mode 100644 tests/gitea-repositories-meta/org41/repo61.git/description
 create mode 100644 tests/gitea-repositories-meta/org41/repo61.git/info/exclude
 create mode 100644 tests/gitea-repositories-meta/user40/repo60.git/HEAD
 create mode 100644 tests/gitea-repositories-meta/user40/repo60.git/config
 create mode 100644 tests/gitea-repositories-meta/user40/repo60.git/description
 create mode 100644 tests/gitea-repositories-meta/user40/repo60.git/info/exclude

diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml
index 1bb6a9a8ac..641c453eb7 100644
--- a/models/fixtures/access.yml
+++ b/models/fixtures/access.yml
@@ -135,3 +135,27 @@
   user_id: 31
   repo_id: 28
   mode: 4
+
+-
+  id: 24
+  user_id: 38
+  repo_id: 60
+  mode: 2
+
+-
+  id: 25
+  user_id: 38
+  repo_id: 61
+  mode: 1
+
+-
+  id: 26
+  user_id: 39
+  repo_id: 61
+  mode: 1
+
+-
+  id: 27
+  user_id: 40
+  repo_id: 61
+  mode: 4
diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml
index ef77d22b24..7603bdad32 100644
--- a/models/fixtures/collaboration.yml
+++ b/models/fixtures/collaboration.yml
@@ -45,3 +45,9 @@
   repo_id: 22
   user_id: 18
   mode: 2 # write
+
+-
+  id: 9
+  repo_id: 60
+  user_id: 38
+  mode: 2 # write
diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml
index 67a99f43e2..b2a0432635 100644
--- a/models/fixtures/email_address.yml
+++ b/models/fixtures/email_address.yml
@@ -293,3 +293,27 @@
   lower_email: user37@example.com
   is_activated: true
   is_primary: true
+
+-
+  id: 38
+  uid: 38
+  email: user38@example.com
+  lower_email: user38@example.com
+  is_activated: true
+  is_primary: true
+
+-
+  id: 39
+  uid: 39
+  email: user39@example.com
+  lower_email: user39@example.com
+  is_activated: true
+  is_primary: true
+
+-
+  id: 40
+  uid: 40
+  email: user40@example.com
+  lower_email: user40@example.com
+  is_activated: true
+  is_primary: true
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index 0c9b6ff406..ca5b1c6cd1 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -338,3 +338,37 @@
   created_unix: 978307210
   updated_unix: 978307210
   is_locked: false
+
+-
+  id: 21
+  repo_id: 60
+  index: 1
+  poster_id: 39
+  original_author_id: 0
+  name: repo60 pull1
+  content: content for the 1st issue
+  milestone_id: 0
+  priority: 0
+  is_closed: false
+  is_pull: true
+  num_comments: 0
+  created_unix: 1707270422
+  updated_unix: 1707270422
+  is_locked: false
+
+-
+  id: 22
+  repo_id: 61
+  index: 1
+  poster_id: 40
+  original_author_id: 0
+  name: repo61 pull1
+  content: content for the 1st issue
+  milestone_id: 0
+  priority: 0
+  is_closed: false
+  is_pull: true
+  num_comments: 0
+  created_unix: 1707270422
+  updated_unix: 1707270422
+  is_locked: false
diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml
index 8d58169a32..a7fbcb2c5a 100644
--- a/models/fixtures/org_user.yml
+++ b/models/fixtures/org_user.yml
@@ -99,3 +99,21 @@
   uid: 5
   org_id: 36
   is_public: true
+
+-
+  id: 18
+  uid: 38
+  org_id: 41
+  is_public: true
+
+-
+  id: 19
+  uid: 39
+  org_id: 41
+  is_public: true
+
+-
+  id: 20
+  uid: 40
+  org_id: 41
+  is_public: true
diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml
index 54590fb830..3fc8ce630d 100644
--- a/models/fixtures/pull_request.yml
+++ b/models/fixtures/pull_request.yml
@@ -99,3 +99,21 @@
   index: 1
   head_repo_id: 23
   base_repo_id: 23
+
+-
+  id: 9
+  type: 0 # gitea pull request
+  status: 2 # mergable
+  issue_id: 21
+  index: 1
+  head_repo_id: 60
+  base_repo_id: 60
+
+-
+  id: 10
+  type: 0 # gitea pull request
+  status: 2 # mergable
+  issue_id: 22
+  index: 1
+  head_repo_id: 61
+  base_repo_id: 61
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index e6c59f527a..4b26674990 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -676,3 +676,45 @@
   type: 1
   config: "{}"
   created_unix: 946684810
+
+-
+  id: 102
+  repo_id: 60
+  type: 1
+  config: "{}"
+  created_unix: 946684810
+
+-
+  id: 103
+  repo_id: 60
+  type: 2
+  config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
+  created_unix: 946684810
+
+-
+  id: 104
+  repo_id: 60
+  type: 3
+  config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
+  created_unix: 946684810
+
+-
+  id: 105
+  repo_id: 61
+  type: 1
+  config: "{}"
+  created_unix: 946684810
+
+-
+  id: 106
+  repo_id: 61
+  type: 2
+  config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
+  created_unix: 946684810
+
+-
+  id: 107
+  repo_id: 61
+  type: 3
+  config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
+  created_unix: 946684810
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index f4e8376735..d094fe82d8 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -1706,3 +1706,65 @@
   is_private: true
   status: 0
   num_issues: 0
+
+-
+  id: 60
+  owner_id: 40
+  owner_name: user40
+  lower_name: repo60
+  name: repo60
+  default_branch: main
+  num_watches: 0
+  num_stars: 0
+  num_forks: 0
+  num_issues: 0
+  num_closed_issues: 0
+  num_pulls: 1
+  num_closed_pulls: 0
+  num_milestones: 0
+  num_closed_milestones: 0
+  num_projects: 0
+  num_closed_projects: 0
+  is_private: false
+  is_empty: false
+  is_archived: false
+  is_mirror: false
+  status: 0
+  is_fork: false
+  fork_id: 0
+  is_template: false
+  template_id: 0
+  size: 0
+  is_fsck_enabled: true
+  close_issues_via_commit_in_any_branch: false
+
+-
+  id: 61
+  owner_id: 41
+  owner_name: org41
+  lower_name: repo61
+  name: repo61
+  default_branch: main
+  num_watches: 0
+  num_stars: 0
+  num_forks: 0
+  num_issues: 0
+  num_closed_issues: 0
+  num_pulls: 1
+  num_closed_pulls: 0
+  num_milestones: 0
+  num_closed_milestones: 0
+  num_projects: 0
+  num_closed_projects: 0
+  is_private: false
+  is_empty: false
+  is_archived: false
+  is_mirror: false
+  status: 0
+  is_fork: false
+  fork_id: 0
+  is_template: false
+  template_id: 0
+  size: 0
+  is_fsck_enabled: true
+  close_issues_via_commit_in_any_branch: false
diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml
index 295e51e39c..149fe90888 100644
--- a/models/fixtures/team.yml
+++ b/models/fixtures/team.yml
@@ -217,3 +217,25 @@
   num_members: 1
   includes_all_repositories: false
   can_create_org_repo: true
+
+-
+  id: 21
+  org_id: 41
+  lower_name: owners
+  name: Owners
+  authorize: 4 # owner
+  num_repos: 1
+  num_members: 1
+  includes_all_repositories: true
+  can_create_org_repo: true
+
+-
+  id: 22
+  org_id: 41
+  lower_name: team1
+  name: Team1
+  authorize: 1 # read
+  num_repos: 1
+  num_members: 2
+  includes_all_repositories: false
+  can_create_org_repo: false
diff --git a/models/fixtures/team_repo.yml b/models/fixtures/team_repo.yml
index 8497720892..a29078107e 100644
--- a/models/fixtures/team_repo.yml
+++ b/models/fixtures/team_repo.yml
@@ -63,3 +63,15 @@
   org_id: 17
   team_id: 9
   repo_id: 24
+
+-
+  id: 12
+  org_id: 41
+  team_id: 21
+  repo_id: 61
+
+-
+  id: 13
+  org_id: 41
+  team_id: 22
+  repo_id: 61
diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml
index c5531aa57a..de0e8d738b 100644
--- a/models/fixtures/team_unit.yml
+++ b/models/fixtures/team_unit.yml
@@ -286,3 +286,39 @@
   team_id: 2
   type: 8
   access_mode: 2
+
+-
+  id: 49
+  team_id: 21
+  type: 1
+  access_mode: 4
+
+-
+  id: 50
+  team_id: 21
+  type: 2
+  access_mode: 4
+
+-
+  id: 51
+  team_id: 21
+  type: 3
+  access_mode: 4
+
+-
+  id: 52
+  team_id: 22
+  type: 1
+  access_mode: 1
+
+-
+  id: 53
+  team_id: 22
+  type: 2
+  access_mode: 1
+
+-
+  id: 54
+  team_id: 22
+  type: 3
+  access_mode: 1
diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml
index 9142fe609a..02d57ae644 100644
--- a/models/fixtures/team_user.yml
+++ b/models/fixtures/team_user.yml
@@ -129,3 +129,21 @@
   org_id: 17
   team_id: 9
   uid: 15
+
+-
+  id: 23
+  org_id: 41
+  team_id: 21
+  uid: 40
+
+-
+  id: 24
+  org_id: 41
+  team_id: 22
+  uid: 38
+
+-
+  id: 25
+  org_id: 41
+  team_id: 22
+  uid: 39
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index aa0daedd85..16b687ae04 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -1369,3 +1369,151 @@
   repo_admin_change_team_access: false
   theme: ""
   keep_activity_private: false
+
+-
+  id: 38
+  lower_name: user38
+  name: user38
+  full_name: User38
+  email: user38@example.com
+  keep_email_private: false
+  email_notifications_preference: enabled
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: user38
+  type: 0
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: true
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar38
+  avatar_email: user38@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 0
+  num_teams: 0
+  num_members: 0
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
+
+-
+  id: 39
+  lower_name: user39
+  name: user39
+  full_name: User39
+  email: user39@example.com
+  keep_email_private: false
+  email_notifications_preference: enabled
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: user39
+  type: 0
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: true
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar39
+  avatar_email: user39@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 0
+  num_teams: 0
+  num_members: 0
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
+
+-
+  id: 40
+  lower_name: user40
+  name: user40
+  full_name: User40
+  email: user40@example.com
+  keep_email_private: false
+  email_notifications_preference: onmention
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: user40
+  type: 0
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: true
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar40
+  avatar_email: user40@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 1
+  num_teams: 0
+  num_members: 0
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
+
+-
+  id: 41
+  lower_name: org41
+  name: org41
+  full_name: Org41
+  email: org41@example.com
+  keep_email_private: false
+  email_notifications_preference: onmention
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: org41
+  type: 1
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: false
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar41
+  avatar_email: org41@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 1
+  num_teams: 2
+  num_members: 3
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go
index cc363d2fae..1bbc0eee56 100644
--- a/models/issues/issue_test.go
+++ b/models/issues/issue_test.go
@@ -379,7 +379,7 @@ func TestCountIssues(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{})
 	assert.NoError(t, err)
-	assert.EqualValues(t, 20, count)
+	assert.EqualValues(t, 22, count)
 }
 
 func TestIssueLoadAttributes(t *testing.T) {
diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go
index 8a1799aac0..83e37a27fd 100644
--- a/models/repo/repo_list_test.go
+++ b/models/repo/repo_list_test.go
@@ -138,12 +138,12 @@ func getTestCases() []struct {
 		{
 			name:  "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
 			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
-			count: 31,
+			count: 33,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
 			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
-			count: 36,
+			count: 38,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
@@ -158,7 +158,7 @@ func getTestCases() []struct {
 		{
 			name:  "AllPublic/PublicRepositoriesOfOrganization",
 			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
-			count: 31,
+			count: 33,
 		},
 		{
 			name:  "AllTemplates",
diff --git a/models/user/user_test.go b/models/user/user_test.go
index f3e5a95b1e..68cee9cdbd 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -89,7 +89,7 @@ func TestSearchUsers(t *testing.T) {
 		[]int64{19, 25})
 
 	testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}},
-		[]int64{26})
+		[]int64{26, 41})
 
 	testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 5, PageSize: 2}},
 		[]int64{})
@@ -101,13 +101,13 @@ func TestSearchUsers(t *testing.T) {
 	}
 
 	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
-		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37})
+		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 
 	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
 		[]int64{9})
 
 	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
-		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37})
+		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 
 	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
 		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index da4fc9b878..3b96686d98 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -218,7 +218,7 @@ func searchIssueIsPull(t *testing.T) {
 			SearchOptions{
 				IsPull: util.OptionalBoolTrue,
 			},
-			[]int64{12, 11, 20, 19, 9, 8, 3, 2},
+			[]int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2},
 		},
 	}
 	for _, test := range tests {
@@ -239,7 +239,7 @@ func searchIssueIsClosed(t *testing.T) {
 			SearchOptions{
 				IsClosed: util.OptionalBoolFalse,
 			},
-			[]int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
+			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
 		},
 		{
 			SearchOptions{
@@ -305,7 +305,7 @@ func searchIssueByLabelID(t *testing.T) {
 			SearchOptions{
 				ExcludedLabelIDs: []int64{1},
 			},
-			[]int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3},
+			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3},
 		},
 	}
 	for _, test := range tests {
@@ -329,7 +329,7 @@ func searchIssueByTime(t *testing.T) {
 			SearchOptions{
 				UpdatedAfterUnix: int64Pointer(0),
 			},
-			[]int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
+			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
 		},
 	}
 	for _, test := range tests {
@@ -350,7 +350,7 @@ func searchIssueWithOrder(t *testing.T) {
 			SearchOptions{
 				SortBy: internal.SortByCreatedAsc,
 			},
-			[]int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17},
+			[]int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17, 21, 22},
 		},
 	}
 	for _, test := range tests {
@@ -410,8 +410,8 @@ func searchIssueWithPaginator(t *testing.T) {
 					PageSize: 5,
 				},
 			},
-			[]int64{17, 16, 15, 14, 13},
-			20,
+			[]int64{22, 21, 17, 16, 15},
+			22,
 		},
 	}
 	for _, test := range tests {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index d5e49960a1..9f08607642 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -711,16 +711,12 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
 			tmp.ItemID = -review.ReviewerTeamID
 		}
 
-		if ctx.Repo.IsAdmin() {
-			// Admin can dismiss or re-request any review requests
+		if canChooseReviewer {
+			// Users who can choose reviewers can also remove review requests
 			tmp.CanChange = true
 		} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
 			// A user can refuse review requests
 			tmp.CanChange = true
-		} else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
-			ctx.Doer.ID != review.ReviewerID {
-			// The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
-			tmp.CanChange = true
 		}
 
 		pullReviews = append(pullReviews, tmp)
@@ -1525,18 +1521,9 @@ func ViewIssue(ctx *context.Context) {
 	}
 
 	if issue.IsPull {
-		canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
+		canChooseReviewer := false
 		if ctx.Doer != nil && ctx.IsSigned {
-			if !canChooseReviewer {
-				canChooseReviewer = ctx.Doer.ID == issue.PosterID
-			}
-			if !canChooseReviewer {
-				canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
-				if err != nil {
-					ctx.ServerError("IsOfficialReviewer", err)
-					return
-				}
-			}
+			canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue)
 		}
 
 		RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
diff --git a/services/issue/assignee.go b/services/issue/assignee.go
index 27fc695533..b5f472ba53 100644
--- a/services/issue/assignee.go
+++ b/services/issue/assignee.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
@@ -113,10 +114,10 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 		return err
 	}
 
-	var pemResult bool
+	canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
+
 	if isAdd {
-		pemResult = permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests)
-		if !pemResult {
+		if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) {
 			return issues_model.ErrNotValidReviewRequest{
 				Reason: "Reviewer can't read",
 				UserID: doer.ID,
@@ -124,28 +125,6 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 			}
 		}
 
-		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
-			return nil
-		}
-
-		pemResult = doer.ID == issue.PosterID
-		if !pemResult {
-			pemResult = permDoer.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
-		}
-		if !pemResult {
-			pemResult, err = issues_model.IsOfficialReviewer(ctx, issue, doer)
-			if err != nil {
-				return err
-			}
-			if !pemResult {
-				return issues_model.ErrNotValidReviewRequest{
-					Reason: "Doer can't choose reviewer",
-					UserID: doer.ID,
-					RepoID: issue.Repo.ID,
-				}
-			}
-		}
-
 		if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 {
 			return issues_model.ErrNotValidReviewRequest{
 				Reason: "poster of pr can't be reviewer",
@@ -153,22 +132,35 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 				RepoID: issue.Repo.ID,
 			}
 		}
-	} else {
-		if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
+
+		if canDoerChangeReviewRequests {
 			return nil
 		}
 
-		pemResult = permDoer.IsAdmin()
-		if !pemResult {
-			return issues_model.ErrNotValidReviewRequest{
-				Reason: "Doer is not admin",
-				UserID: doer.ID,
-				RepoID: issue.Repo.ID,
-			}
+		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
+			return nil
+		}
+
+		return issues_model.ErrNotValidReviewRequest{
+			Reason: "Doer can't choose reviewer",
+			UserID: doer.ID,
+			RepoID: issue.Repo.ID,
 		}
 	}
 
-	return nil
+	if canDoerChangeReviewRequests {
+		return nil
+	}
+
+	if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
+		return nil
+	}
+
+	return issues_model.ErrNotValidReviewRequest{
+		Reason: "Doer can't remove reviewer",
+		UserID: doer.ID,
+		RepoID: issue.Repo.ID,
+	}
 }
 
 // IsValidTeamReviewRequest Check permission for ReviewRequest Team
@@ -181,11 +173,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 		}
 	}
 
-	permission, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
-	if err != nil {
-		log.Error("Unable to GetUserRepoPermission for %-v in %-v#%d", doer, issue.Repo, issue.Index)
-		return err
-	}
+	canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
 
 	if isAdd {
 		if issue.Repo.IsPrivate {
@@ -200,30 +188,26 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 			}
 		}
 
-		doerCanWrite := permission.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
-		if !doerCanWrite && doer.ID != issue.PosterID {
-			official, err := issues_model.IsOfficialReviewer(ctx, issue, doer)
-			if err != nil {
-				log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index)
-				return err
-			}
-			if !official {
-				return issues_model.ErrNotValidReviewRequest{
-					Reason: "Doer can't choose reviewer",
-					UserID: doer.ID,
-					RepoID: issue.Repo.ID,
-				}
-			}
+		if canDoerChangeReviewRequests {
+			return nil
 		}
-	} else if !permission.IsAdmin() {
+
 		return issues_model.ErrNotValidReviewRequest{
-			Reason: "Only admin users can remove team requests. Doer is not admin",
+			Reason: "Doer can't choose reviewer",
 			UserID: doer.ID,
 			RepoID: issue.Repo.ID,
 		}
 	}
 
-	return nil
+	if canDoerChangeReviewRequests {
+		return nil
+	}
+
+	return issues_model.ErrNotValidReviewRequest{
+		Reason: "Doer can't remove reviewer",
+		UserID: doer.ID,
+		RepoID: issue.Repo.ID,
+	}
 }
 
 // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
@@ -264,3 +248,50 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
 
 	return comment, err
 }
+
+// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
+func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool {
+	// The poster of the PR can change the reviewers
+	if doer.ID == issue.PosterID {
+		return true
+	}
+
+	// The owner of the repo can change the reviewers
+	if doer.ID == repo.OwnerID {
+		return true
+	}
+
+	// Collaborators of the repo can change the reviewers
+	isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID)
+	if err != nil {
+		log.Error("IsCollaborator: %v", err)
+		return false
+	}
+	if isCollaborator {
+		return true
+	}
+
+	// If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
+	if repo.Owner.IsOrganization() {
+		teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
+		if err != nil {
+			log.Error("GetTeamsWithAccessToRepo: %v", err)
+			return false
+		}
+		for _, team := range teams {
+			if !team.UnitEnabled(ctx, unit.TypePullRequests) {
+				continue
+			}
+			isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID)
+			if err != nil {
+				log.Error("IsTeamMember: %v", err)
+				continue
+			}
+			if isMember {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/HEAD b/tests/gitea-repositories-meta/org41/repo61.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/config b/tests/gitea-repositories-meta/org41/repo61.git/config
new file mode 100644
index 0000000000..64280b806c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/config
@@ -0,0 +1,6 @@
+[core]
+	repositoryformatversion = 0
+	filemode = false
+	bare = true
+	symlinks = false
+	ignorecase = true
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/description b/tests/gitea-repositories-meta/org41/repo61.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/info/exclude b/tests/gitea-repositories-meta/org41/repo61.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/HEAD b/tests/gitea-repositories-meta/user40/repo60.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/config b/tests/gitea-repositories-meta/user40/repo60.git/config
new file mode 100644
index 0000000000..64280b806c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/config
@@ -0,0 +1,6 @@
+[core]
+	repositoryformatversion = 0
+	filemode = false
+	bare = true
+	symlinks = false
+	ignorecase = true
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/description b/tests/gitea-repositories-meta/user40/repo60.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/info/exclude b/tests/gitea-repositories-meta/user40/repo60.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index f025806868..650bac2e32 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -217,7 +217,7 @@ func TestAPISearchIssues(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	// as this API was used in the frontend, it uses UI page size
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}
@@ -257,7 +257,7 @@ func TestAPISearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
 	resp = MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 20)
 
 	query.Add("limit", "10")
@@ -265,7 +265,7 @@ func TestAPISearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
 	resp = MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 10)
 
 	query = url.Values{"assigned": {"true"}, "state": {"all"}}
@@ -315,7 +315,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	// as this API was used in the frontend, it uses UI page size
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}
diff --git a/tests/integration/api_nodeinfo_test.go b/tests/integration/api_nodeinfo_test.go
index 876fb5ac13..75f8dbb4ba 100644
--- a/tests/integration/api_nodeinfo_test.go
+++ b/tests/integration/api_nodeinfo_test.go
@@ -32,8 +32,8 @@ func TestNodeinfo(t *testing.T) {
 		DecodeJSON(t, resp, &nodeinfo)
 		assert.True(t, nodeinfo.OpenRegistrations)
 		assert.Equal(t, "gitea", nodeinfo.Software.Name)
-		assert.Equal(t, 26, nodeinfo.Usage.Users.Total)
-		assert.Equal(t, 20, nodeinfo.Usage.LocalPosts)
+		assert.Equal(t, 29, nodeinfo.Usage.Users.Total)
+		assert.Equal(t, 22, nodeinfo.Usage.LocalPosts)
 		assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
 	})
 }
diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go
index 1cd82fe4e0..70d3a446f7 100644
--- a/tests/integration/api_org_test.go
+++ b/tests/integration/api_org_test.go
@@ -177,7 +177,7 @@ func TestAPIGetAll(t *testing.T) {
 	var apiOrgList []*api.Organization
 
 	DecodeJSON(t, resp, &apiOrgList)
-	assert.Len(t, apiOrgList, 11)
+	assert.Len(t, apiOrgList, 12)
 	assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
 	assert.Equal(t, "limited", apiOrgList[1].Visibility)
 
@@ -186,7 +186,7 @@ func TestAPIGetAll(t *testing.T) {
 	resp = MakeRequest(t, req, http.StatusOK)
 
 	DecodeJSON(t, resp, &apiOrgList)
-	assert.Len(t, apiOrgList, 7)
+	assert.Len(t, apiOrgList, 8)
 	assert.Equal(t, "org 17", apiOrgList[0].FullName)
 	assert.Equal(t, "public", apiOrgList[0].Visibility)
 }
diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go
index ab6d33cd5b..bc544a30b5 100644
--- a/tests/integration/api_pull_review_test.go
+++ b/tests/integration/api_pull_review_test.go
@@ -279,6 +279,49 @@ func TestAPIPullReviewRequest(t *testing.T) {
 	}).AddTokenAuth(token)
 	MakeRequest(t, req, http.StatusNoContent)
 
+	// a collaborator can add/remove a review request
+	pullIssue21 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21})
+	assert.NoError(t, pullIssue21.LoadAttributes(db.DefaultContext))
+	pull21Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue21.RepoID}) // repo60
+	user38Session := loginUser(t, "user38")
+	user38Token := getTokenForLoggedInUser(t, user38Session, auth_model.AccessTokenScopeWriteRepository)
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user4@example.com"},
+	}).AddTokenAuth(user38Token)
+	MakeRequest(t, req, http.StatusCreated)
+
+	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user4@example.com"},
+	}).AddTokenAuth(user38Token)
+	MakeRequest(t, req, http.StatusNoContent)
+
+	// the poster of the PR can add/remove a review request
+	user39Session := loginUser(t, "user39")
+	user39Token := getTokenForLoggedInUser(t, user39Session, auth_model.AccessTokenScopeWriteRepository)
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user8"},
+	}).AddTokenAuth(user39Token)
+	MakeRequest(t, req, http.StatusCreated)
+
+	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user8"},
+	}).AddTokenAuth(user39Token)
+	MakeRequest(t, req, http.StatusNoContent)
+
+	// user with read permission on pull requests unit can add/remove a review request
+	pullIssue22 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22})
+	assert.NoError(t, pullIssue22.LoadAttributes(db.DefaultContext))
+	pull22Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue22.RepoID}) // repo61
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user38"},
+	}).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit
+	MakeRequest(t, req, http.StatusCreated)
+
+	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user38"},
+	}).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit
+	MakeRequest(t, req, http.StatusNoContent)
+
 	// Test team review request
 	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
 	assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext))
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
index 90f84c794e..481732f8df 100644
--- a/tests/integration/api_repo_test.go
+++ b/tests/integration/api_repo_test.go
@@ -93,9 +93,9 @@ func TestAPISearchRepo(t *testing.T) {
 	}{
 		{
 			name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{
-				nil:   {count: 33},
-				user:  {count: 33},
-				user2: {count: 33},
+				nil:   {count: 35},
+				user:  {count: 35},
+				user2: {count: 35},
 			},
 		},
 		{
diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go
index 4b3f581c2b..44d362d9c7 100644
--- a/tests/integration/issue_test.go
+++ b/tests/integration/issue_test.go
@@ -407,7 +407,7 @@ func TestSearchIssues(t *testing.T) {
 
 	session := loginUser(t, "user2")
 
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}
@@ -444,7 +444,7 @@ func TestSearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String())
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 20)
 
 	query.Add("limit", "5")
@@ -452,7 +452,7 @@ func TestSearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String())
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 5)
 
 	query = url.Values{"assigned": {"true"}, "state": {"all"}}
@@ -501,7 +501,7 @@ func TestSearchIssues(t *testing.T) {
 func TestSearchIssuesWithLabels(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}

From 29a26d9d8c573c9fb7e79a66ac3d578e8b20dcae Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 24 Feb 2024 21:12:17 +0800
Subject: [PATCH 152/679] Customizable "Open with" applications for repository
 clone (#29320)

Users could customize the "clone" menu with their own application URLs on the admin panel.

Replace #22378
Close #21121
Close #22149
---
 modules/context/context.go                    |  1 +
 modules/setting/config.go                     | 49 +++++++++++++-
 modules/setting/config/value.go               | 35 +++++++---
 options/locale/locale_en-US.ini               |  5 +-
 .../img/svg/gitea-open-with-jetbrains.svg     |  1 +
 .../assets/img/svg/gitea-open-with-vscode.svg |  1 +
 .../img/svg/gitea-open-with-vscodium.svg      |  1 +
 public/assets/img/svg/gitea-vscode.svg        |  1 -
 routers/web/admin/config.go                   | 67 ++++++++++++++++---
 routers/web/repo/view.go                      | 29 +++++++-
 routers/web/web.go                            |  1 +
 templates/admin/config.tmpl                   | 21 ------
 templates/admin/config_settings.tmpl          | 42 ++++++++++++
 templates/admin/navbar.tmpl                   | 14 +++-
 templates/repo/clone_script.tmpl              |  4 +-
 templates/repo/home.tmpl                      |  4 +-
 web_src/svg/gitea-open-with-jetbrains.svg     | 62 +++++++++++++++++
 ...-vscode.svg => gitea-open-with-vscode.svg} |  0
 web_src/svg/gitea-open-with-vscodium.svg      |  1 +
 19 files changed, 286 insertions(+), 53 deletions(-)
 create mode 100644 public/assets/img/svg/gitea-open-with-jetbrains.svg
 create mode 100644 public/assets/img/svg/gitea-open-with-vscode.svg
 create mode 100644 public/assets/img/svg/gitea-open-with-vscodium.svg
 delete mode 100644 public/assets/img/svg/gitea-vscode.svg
 create mode 100644 templates/admin/config_settings.tmpl
 create mode 100644 web_src/svg/gitea-open-with-jetbrains.svg
 rename web_src/svg/{gitea-vscode.svg => gitea-open-with-vscode.svg} (100%)
 create mode 100644 web_src/svg/gitea-open-with-vscodium.svg

diff --git a/modules/context/context.go b/modules/context/context.go
index 66732eaa8a..4b318f7e33 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -192,6 +192,7 @@ func Contexter() func(next http.Handler) http.Handler {
 			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
 			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
 
+			ctx.Data["SystemConfig"] = setting.Config()
 			ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
 			ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
 
diff --git a/modules/setting/config.go b/modules/setting/config.go
index db189f44ac..03558574c2 100644
--- a/modules/setting/config.go
+++ b/modules/setting/config.go
@@ -15,8 +15,45 @@ type PictureStruct struct {
 	EnableFederatedAvatar *config.Value[bool]
 }
 
+type OpenWithEditorApp struct {
+	DisplayName string
+	OpenURL     string
+}
+
+type OpenWithEditorAppsType []OpenWithEditorApp
+
+func (t OpenWithEditorAppsType) ToTextareaString() string {
+	ret := ""
+	for _, app := range t {
+		ret += app.DisplayName + " = " + app.OpenURL + "\n"
+	}
+	return ret
+}
+
+func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
+	return OpenWithEditorAppsType{
+		{
+			DisplayName: "VS Code",
+			OpenURL:     "vscode://vscode.git/clone?url={url}",
+		},
+		{
+			DisplayName: "VSCodium",
+			OpenURL:     "vscodium://vscode.git/clone?url={url}",
+		},
+		{
+			DisplayName: "Intellij IDEA",
+			OpenURL:     "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
+		},
+	}
+}
+
+type RepositoryStruct struct {
+	OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
+}
+
 type ConfigStruct struct {
-	Picture *PictureStruct
+	Picture    *PictureStruct
+	Repository *RepositoryStruct
 }
 
 var (
@@ -28,8 +65,11 @@ func initDefaultConfig() {
 	config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
 	defaultConfig = &ConfigStruct{
 		Picture: &PictureStruct{
-			DisableGravatar:       config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
-			EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
+			DisableGravatar:       config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
+			EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
+		},
+		Repository: &RepositoryStruct{
+			OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
 		},
 	}
 }
@@ -42,6 +82,9 @@ func Config() *ConfigStruct {
 type cfgSecKeyGetter struct{}
 
 func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
+	if key == "" {
+		return "", false
+	}
 	cfgSec, err := CfgProvider.GetSection(sec)
 	if err != nil {
 		log.Error("Unable to get config section: %q", sec)
diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go
index 817fcdb786..f0ec120544 100644
--- a/modules/setting/config/value.go
+++ b/modules/setting/config/value.go
@@ -5,8 +5,11 @@ package config
 
 import (
 	"context"
-	"strconv"
 	"sync"
+
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 )
 
 type CfgSecKey struct {
@@ -23,14 +26,14 @@ type Value[T any] struct {
 	revision   int
 }
 
-func (value *Value[T]) parse(s string) (v T) {
-	switch any(v).(type) {
-	case bool:
-		b, _ := strconv.ParseBool(s)
-		return any(b).(T)
-	default:
-		panic("unsupported config type, please complete the code")
+func (value *Value[T]) parse(key, valStr string) (v T) {
+	v = value.def
+	if valStr != "" {
+		if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
+			log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
+		}
 	}
+	return v
 }
 
 func (value *Value[T]) Value(ctx context.Context) (v T) {
@@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
 	if valStr == nil {
 		v = value.def
 	} else {
-		v = value.parse(*valStr)
+		v = value.parse(value.dynKey, *valStr)
 	}
 
 	value.mu.Lock()
@@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
 	return value.dynKey
 }
 
-func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
-	return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
+func (value *Value[T]) WithDefault(def T) *Value[T] {
+	value.def = def
+	return value
+}
+
+func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
+	value.cfgSecKey = cfgSecKey
+	return value
+}
+
+func ValueJSON[T any](dynKey string) *Value[T] {
+	return &Value[T]{dynKey: dynKey}
 }
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index ff6a3f1b8e..a0ad09f776 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -956,7 +956,7 @@ fork_branch = Branch to be cloned to the fork
 all_branches = All branches
 fork_no_valid_owners = This repository can not be forked because there are no valid owners.
 use_template = Use this template
-clone_in_vsc = Clone in VS Code
+open_with_editor = Open with %s
 download_zip = Download ZIP
 download_tar = Download TAR.GZ
 download_bundle = Download BUNDLE
@@ -2737,6 +2737,8 @@ integrations = Integrations
 authentication = Authentication Sources
 emails = User Emails
 config = Configuration
+config_summary = Summary
+config_settings = Settings
 notices = System Notices
 monitor = Monitoring
 first_page = First
@@ -3176,6 +3178,7 @@ config.picture_config = Picture and Avatar Configuration
 config.picture_service = Picture Service
 config.disable_gravatar = Disable Gravatar
 config.enable_federated_avatar = Enable Federated Avatars
+config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
 
 config.git_config = Git Configuration
 config.git_disable_diff_highlight = Disable Diff Syntax Highlight
diff --git a/public/assets/img/svg/gitea-open-with-jetbrains.svg b/public/assets/img/svg/gitea-open-with-jetbrains.svg
new file mode 100644
index 0000000000..2b1491b541
--- /dev/null
+++ b/public/assets/img/svg/gitea-open-with-jetbrains.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-open-with-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-open-with-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-open-with-jetbrains__a)"/><linearGradient id="gitea-open-with-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__b)"/><linearGradient id="gitea-open-with-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__c)"/><linearGradient id="gitea-open-with-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-open-with-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-open-with-vscode.svg b/public/assets/img/svg/gitea-open-with-vscode.svg
new file mode 100644
index 0000000000..151c45e210
--- /dev/null
+++ b/public/assets/img/svg/gitea-open-with-vscode.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-open-with-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-open-with-vscodium.svg b/public/assets/img/svg/gitea-open-with-vscodium.svg
new file mode 100644
index 0000000000..9f70878ba6
--- /dev/null
+++ b/public/assets/img/svg/gitea-open-with-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-open-with-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-vscode.svg b/public/assets/img/svg/gitea-vscode.svg
deleted file mode 100644
index 453b9befcc..0000000000
--- a/public/assets/img/svg/gitea-vscode.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
\ No newline at end of file
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index c827f2a4f5..47f9201504 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -7,11 +7,11 @@ package admin
 import (
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
@@ -24,7 +24,10 @@ import (
 	"gitea.com/go-chi/session"
 )
 
-const tplConfig base.TplName = "admin/config"
+const (
+	tplConfig         base.TplName = "admin/config"
+	tplConfigSettings base.TplName = "admin/config_settings"
+)
 
 // SendTestMail send test mail to confirm mail service is OK
 func SendTestMail(ctx *context.Context) {
@@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {
 
 // Config show admin config page
 func Config(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("admin.config")
+	ctx.Data["Title"] = ctx.Tr("admin.config_summary")
 	ctx.Data["PageIsAdminConfig"] = true
+	ctx.Data["PageIsAdminConfigSummary"] = true
 
 	ctx.Data["CustomConf"] = setting.CustomConf
 	ctx.Data["AppUrl"] = setting.AppURL
@@ -161,23 +165,70 @@ func Config(ctx *context.Context) {
 
 	ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
 	config.GetDynGetter().InvalidateCache()
-	ctx.Data["SystemConfig"] = setting.Config()
 	prepareDeprecatedWarningsAlert(ctx)
 
 	ctx.HTML(http.StatusOK, tplConfig)
 }
 
+func ConfigSettings(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("admin.config_settings")
+	ctx.Data["PageIsAdminConfig"] = true
+	ctx.Data["PageIsAdminConfigSettings"] = true
+	ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
+	ctx.HTML(http.StatusOK, tplConfigSettings)
+}
+
 func ChangeConfig(ctx *context.Context) {
 	key := strings.TrimSpace(ctx.FormString("key"))
 	value := ctx.FormString("value")
 	cfg := setting.Config()
-	allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey())
-	if !allowedKeys.Contains(key) {
+
+	marshalBool := func(v string) (string, error) {
+		if b, _ := strconv.ParseBool(v); b {
+			return "true", nil
+		}
+		return "false", nil
+	}
+	marshalOpenWithApps := func(value string) (string, error) {
+		lines := strings.Split(value, "\n")
+		var openWithEditorApps setting.OpenWithEditorAppsType
+		for _, line := range lines {
+			line = strings.TrimSpace(line)
+			if line == "" {
+				continue
+			}
+			displayName, openURL, ok := strings.Cut(line, "=")
+			displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
+			if !ok || displayName == "" || openURL == "" {
+				continue
+			}
+			openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
+				DisplayName: strings.TrimSpace(displayName),
+				OpenURL:     strings.TrimSpace(openURL),
+			})
+		}
+		b, err := json.Marshal(openWithEditorApps)
+		if err != nil {
+			return "", err
+		}
+		return string(b), nil
+	}
+	marshallers := map[string]func(string) (string, error){
+		cfg.Picture.DisableGravatar.DynKey():       marshalBool,
+		cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
+		cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
+	}
+	marshaller, hasMarshaller := marshallers[key]
+	if !hasMarshaller {
 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
 		return
 	}
-	if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
-		log.Error("set setting failed: %v", err)
+	marshaledValue, err := marshaller(value)
+	if err != nil {
+		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
+		return
+	}
+	if err = system_model.SetSettings(ctx, map[string]string{key: marshaledValue}); err != nil {
 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
 		return
 	}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 15f22237a8..33a5941d36 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -45,6 +45,7 @@ import (
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/svg"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
@@ -792,7 +793,7 @@ func Home(ctx *context.Context) {
 		return
 	}
 
-	renderCode(ctx)
+	renderHomeCode(ctx)
 }
 
 // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
@@ -919,9 +920,33 @@ func renderRepoTopics(ctx *context.Context) {
 	ctx.Data["Topics"] = topics
 }
 
-func renderCode(ctx *context.Context) {
+func prepareOpenWithEditorApps(ctx *context.Context) {
+	var tmplApps []map[string]any
+	apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
+	if len(apps) == 0 {
+		apps = setting.DefaultOpenWithEditorApps()
+	}
+	for _, app := range apps {
+		schema, _, _ := strings.Cut(app.OpenURL, ":")
+		var iconHTML template.HTML
+		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
+			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-open-with-%s", schema), 16, "gt-mr-3")
+		} else {
+			iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future
+		}
+		tmplApps = append(tmplApps, map[string]any{
+			"DisplayName": app.DisplayName,
+			"OpenURL":     app.OpenURL,
+			"IconHTML":    iconHTML,
+		})
+	}
+	ctx.Data["OpenWithEditorApps"] = tmplApps
+}
+
+func renderHomeCode(ctx *context.Context) {
 	ctx.Data["PageIsViewCode"] = true
 	ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
+	prepareOpenWithEditorApps(ctx)
 
 	if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
 		showEmpty := true
diff --git a/routers/web/web.go b/routers/web/web.go
index 8505417c88..b1fa5cf355 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -686,6 +686,7 @@ func registerRoutes(m *web.Route) {
 			m.Get("", admin.Config)
 			m.Post("", admin.ChangeConfig)
 			m.Post("/test_mail", admin.SendTestMail)
+			m.Get("/settings", admin.ConfigSettings)
 		})
 
 		m.Group("/monitor", func() {
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 1cc4b7bb09..6bdda07e48 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -283,27 +283,6 @@
 			</dl>
 		</div>
 
-		<h4 class="ui top attached header">
-			{{ctx.Locale.Tr "admin.config.picture_config"}}
-		</h4>
-		<div class="ui attached table segment">
-			<dl class="admin-dl-horizontal">
-				<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
-				<dd>
-					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
-						<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
-					</div>
-				</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
-				<dd>
-					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
-						<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
-					</div>
-				</dd>
-			</dl>
-		</div>
-
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "admin.config.git_config"}}
 		</h4>
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
new file mode 100644
index 0000000000..22ad5c24ac
--- /dev/null
+++ b/templates/admin/config_settings.tmpl
@@ -0,0 +1,42 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "admin.config.picture_config"}}
+</h4>
+<div class="ui attached table segment">
+	<dl class="admin-dl-horizontal">
+		<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
+		<dd>
+			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
+				<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
+			</div>
+		</dd>
+		<div class="divider"></div>
+		<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
+		<dd>
+			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
+				<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
+			</div>
+		</dd>
+	</dl>
+</div>
+
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "repository"}}
+</h4>
+<div class="ui attached segment">
+	<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/admin/config?key={{.SystemConfig.Repository.OpenWithEditorApps.DynKey}}">
+		<div class="field">
+			<details>
+				<summary>{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}}</summary>
+				<pre class="gt-px-4">{{.DefaultOpenWithEditorAppsString}}</pre>
+			</details>
+		</div>
+		<div class="field">
+			<textarea name="value">{{(.SystemConfig.Repository.OpenWithEditorApps.Value ctx).ToTextareaString}}</textarea>
+		</div>
+		<div class="field">
+			<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
+		</div>
+	</form>
+</div>
+{{template "admin/layout_footer" .}}
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl
index fa79f0f759..d01a6ab964 100644
--- a/templates/admin/navbar.tmpl
+++ b/templates/admin/navbar.tmpl
@@ -75,9 +75,17 @@
 			</div>
 		</details>
 		{{end}}
-		<a class="{{if .PageIsAdminConfig}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
-			{{ctx.Locale.Tr "admin.config"}}
-		</a>
+		<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
+			<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
+			<div class="menu">
+				<a class="{{if .PageIsAdminConfigSummary}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
+					{{ctx.Locale.Tr "admin.config_summary"}}
+				</a>
+				<a class="{{if .PageIsAdminConfigSettings}}active {{end}}item" href="{{AppSubUrl}}/admin/config/settings">
+					{{ctx.Locale.Tr "admin.config_settings"}}
+				</a>
+			</div>
+		</details>
 		<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/admin/notices">
 			{{ctx.Locale.Tr "admin.notices"}}
 		</a>
diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl
index 0797b400d8..0376da4a71 100644
--- a/templates/repo/clone_script.tmpl
+++ b/templates/repo/clone_script.tmpl
@@ -35,8 +35,8 @@
 		for (const el of document.getElementsByClassName('js-clone-url')) {
 			el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
 		}
-		for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
-			el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link);
+		for (const el of document.getElementsByClassName('js-clone-url-editor')) {
+			el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link));
 		}
 	})();
 </script>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index d4b19978d3..f7c74c9aba 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -139,7 +139,9 @@
 										<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
 									{{end}}
 								{{end}}
-								<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{.CloneButtonOriginLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a>
+								{{range .OpenWithEditorApps}}
+									<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
+								{{end}}
 							</div>
 						</button>
 						{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
diff --git a/web_src/svg/gitea-open-with-jetbrains.svg b/web_src/svg/gitea-open-with-jetbrains.svg
new file mode 100644
index 0000000000..a7884c4289
--- /dev/null
+++ b/web_src/svg/gitea-open-with-jetbrains.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
+<g>
+	<g>
+		<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.7898" y1="40.0893" x2="33.3172" y2="40.0893">
+			<stop  offset="0.2581" style="stop-color:#F97A12"/>
+      <stop  offset="0.4591" style="stop-color:#B07B58"/>
+      <stop  offset="0.7241" style="stop-color:#577BAE"/>
+      <stop  offset="0.9105" style="stop-color:#1E7CE5"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_1_);" points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35 		"/>
+    <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="25.7674" y1="24.88" x2="79.424" y2="54.57">
+			<stop  offset="0" style="stop-color:#F97A12"/>
+      <stop  offset="7.179946e-002" style="stop-color:#CB7A3E"/>
+      <stop  offset="0.1541" style="stop-color:#9E7B6A"/>
+      <stop  offset="0.242" style="stop-color:#757B91"/>
+      <stop  offset="0.3344" style="stop-color:#537BB1"/>
+      <stop  offset="0.4324" style="stop-color:#387CCC"/>
+      <stop  offset="0.5381" style="stop-color:#237CE0"/>
+      <stop  offset="0.6552" style="stop-color:#147CEF"/>
+      <stop  offset="0.7925" style="stop-color:#0B7CF7"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_2_);" points="70,18.7 68.7,59.2 41.8,70 25.6,59.6 49.3,35 38.9,12.3 48.2,1.1 		"/>
+    <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="63.2277" y1="42.9153" x2="48.2903" y2="-1.7191">
+			<stop  offset="0" style="stop-color:#FE315D"/>
+      <stop  offset="7.840246e-002" style="stop-color:#CB417E"/>
+      <stop  offset="0.1601" style="stop-color:#9E4E9B"/>
+      <stop  offset="0.2474" style="stop-color:#755BB4"/>
+      <stop  offset="0.3392" style="stop-color:#5365CA"/>
+      <stop  offset="0.4365" style="stop-color:#386DDB"/>
+      <stop  offset="0.5414" style="stop-color:#2374E9"/>
+      <stop  offset="0.6576" style="stop-color:#1478F3"/>
+      <stop  offset="0.794" style="stop-color:#0B7BF8"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_3_);" points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1 		"/>
+    <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="10.7204" y1="16.473" x2="55.5237" y2="90.58">
+			<stop  offset="0" style="stop-color:#FE315D"/>
+      <stop  offset="4.023279e-002" style="stop-color:#F63462"/>
+      <stop  offset="0.1037" style="stop-color:#DF3A71"/>
+      <stop  offset="0.1667" style="stop-color:#C24383"/>
+      <stop  offset="0.2912" style="stop-color:#AD4A91"/>
+      <stop  offset="0.5498" style="stop-color:#755BB4"/>
+      <stop  offset="0.9175" style="stop-color:#1D76ED"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_4_);" points="33.7,58.1 5.6,68.3 10.1,52.5 16,33.1 0,27.7 10.1,0 32.1,2.7 53.7,27.4 		"/>
+	</g>
+  <g>
+		<rect x="13.7" y="13.5" style="fill:#000000;" width="43.2" height="43.2"/>
+    <rect x="17.7" y="48.6" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
+    <polygon style="fill:#FFFFFF;" points="29.4,22.4 29.4,19.1 20.4,19.1 20.4,22.4 23,22.4 23,33.7 20.4,33.7 20.4,37 29.4,37
+			29.4,33.7 26.9,33.7 26.9,22.4 		"/>
+    <path style="fill:#FFFFFF;" d="M38,37.3c-1.4,0-2.6-0.3-3.5-0.8c-0.9-0.5-1.7-1.2-2.3-1.9l2.5-2.8c0.5,0.6,1,1,1.5,1.3
+			c0.5,0.3,1.1,0.5,1.7,0.5c0.7,0,1.3-0.2,1.8-0.7c0.4-0.5,0.6-1.2,0.6-2.3V19.1h4v11.7c0,1.1-0.1,2-0.4,2.8c-0.3,0.8-0.7,1.4-1.3,2
+			c-0.5,0.5-1.2,1-2,1.2C39.8,37.1,39,37.3,38,37.3"/>
+	</g>
+</g>
+</svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-vscode.svg b/web_src/svg/gitea-open-with-vscode.svg
similarity index 100%
rename from web_src/svg/gitea-vscode.svg
rename to web_src/svg/gitea-open-with-vscode.svg
diff --git a/web_src/svg/gitea-open-with-vscodium.svg b/web_src/svg/gitea-open-with-vscodium.svg
new file mode 100644
index 0000000000..483676fe71
--- /dev/null
+++ b/web_src/svg/gitea-open-with-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="100%" height="100%" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" version="1.1" viewBox="0 0 16 16"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9a1046.4 1046.4 0 0 0 .8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3 .2 1.2 0 2.5-.2 3.7 0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8.2.4.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file

From 98ab9445d1020c515c3c789f0b27d952903a2978 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 24 Feb 2024 22:14:48 +0800
Subject: [PATCH 153/679] Users with `read` permission of pull requests can be
 assigned too (#27263)

This PR will also keep the consistent between list assigned users and
check assigned users.
---
 models/perm/access/repo_permission.go | 4 ++--
 models/repo/user_repo.go              | 4 +++-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go
index 395ecdf1a5..4175cb9b92 100644
--- a/models/perm/access/repo_permission.go
+++ b/models/perm/access/repo_permission.go
@@ -332,7 +332,6 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model.
 
 // CanBeAssigned return true if user can be assigned to issue or pull requests in repo
 // Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface.
-// FIXME: user could send PullRequest also could be assigned???
 func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) {
 	if user.IsOrganization() {
 		return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
@@ -341,7 +340,8 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.
 	if err != nil {
 		return false, err
 	}
-	return perm.CanAccessAny(perm_model.AccessModeWrite, unit.TypeCode, unit.TypeIssues, unit.TypePullRequests), nil
+	return perm.CanAccessAny(perm_model.AccessModeWrite, unit.AllRepoUnitTypes...) ||
+		perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil
 }
 
 // HasAccess returns true if user has access to repo
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index dd2ef62201..30c9db7474 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -8,6 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
+	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
 	api "code.gitea.io/gitea/modules/structs"
@@ -78,7 +79,8 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
 	if err = e.Table("team_user").
 		Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
 		Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
-		Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite).
+		Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
+			repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
 		Distinct("`team_user`.uid").
 		Select("`team_user`.uid").
 		Find(&additionalUserIDs); err != nil {

From 4197e2810081025a6614624e7b1731af91c8db72 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sat, 24 Feb 2024 19:46:49 +0100
Subject: [PATCH 154/679] Refactor git attributes (#29356)

---
 modules/git/attribute.go                   | 35 ++++++++++++++++++
 modules/git/repo_attribute.go              | 36 ++++++-------------
 modules/git/repo_attribute_test.go         | 10 +++---
 modules/git/repo_language_stats.go         | 19 ++++++++++
 modules/git/repo_language_stats_gogit.go   | 20 +++--------
 modules/git/repo_language_stats_nogogit.go | 20 +++--------
 routers/web/repo/view.go                   |  7 ++--
 services/gitdiff/gitdiff.go                | 42 +++++++++-------------
 services/repository/files/content.go       | 12 ++-----
 9 files changed, 101 insertions(+), 100 deletions(-)
 create mode 100644 modules/git/attribute.go

diff --git a/modules/git/attribute.go b/modules/git/attribute.go
new file mode 100644
index 0000000000..4dfa510369
--- /dev/null
+++ b/modules/git/attribute.go
@@ -0,0 +1,35 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"code.gitea.io/gitea/modules/optional"
+)
+
+const (
+	AttributeLinguistVendored      = "linguist-vendored"
+	AttributeLinguistGenerated     = "linguist-generated"
+	AttributeLinguistDocumentation = "linguist-documentation"
+	AttributeLinguistDetectable    = "linguist-detectable"
+	AttributeLinguistLanguage      = "linguist-language"
+	AttributeGitlabLanguage        = "gitlab-language"
+)
+
+// true if "set"/"true", false if "unset"/"false", none otherwise
+func AttributeToBool(attr map[string]string, name string) optional.Option[bool] {
+	switch attr[name] {
+	case "set", "true":
+		return optional.Some(true)
+	case "unset", "false":
+		return optional.Some(false)
+	}
+	return optional.None[bool]()
+}
+
+func AttributeToString(attr map[string]string, name string) optional.Option[string] {
+	if value, has := attr[name]; has && value != "unspecified" {
+		return optional.Some(value)
+	}
+	return optional.None[string]()
+}
diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index 44f13ddc2d..84f85d1b1a 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -11,7 +11,6 @@ import (
 	"os"
 
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/optional"
 )
 
 // CheckAttributeOpts represents the possible options to CheckAttribute
@@ -292,10 +291,17 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
 	}
 
 	checker := &CheckAttributeReader{
-		Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"},
-		Repo:       repo,
-		IndexFile:  indexFilename,
-		WorkTree:   worktree,
+		Attributes: []string{
+			AttributeLinguistVendored,
+			AttributeLinguistGenerated,
+			AttributeLinguistDocumentation,
+			AttributeLinguistDetectable,
+			AttributeLinguistLanguage,
+			AttributeGitlabLanguage,
+		},
+		Repo:      repo,
+		IndexFile: indexFilename,
+		WorkTree:  worktree,
 	}
 	ctx, cancel := context.WithCancel(repo.Ctx)
 	if err := checker.Init(ctx); err != nil {
@@ -317,23 +323,3 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
 
 	return checker, deferable
 }
-
-// true if "set"/"true", false if "unset"/"false", none otherwise
-func attributeToBool(attr map[string]string, name string) optional.Option[bool] {
-	if value, has := attr[name]; has && value != "unspecified" {
-		switch value {
-		case "set", "true":
-			return optional.Some(true)
-		case "unset", "false":
-			return optional.Some(false)
-		}
-	}
-	return optional.None[bool]()
-}
-
-func attributeToString(attr map[string]string, name string) optional.Option[string] {
-	if value, has := attr[name]; has && value != "unspecified" {
-		return optional.Some(value)
-	}
-	return optional.None[string]()
-}
diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go
index ed16dccbe4..0fcd94b4c7 100644
--- a/modules/git/repo_attribute_test.go
+++ b/modules/git/repo_attribute_test.go
@@ -24,7 +24,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
 	select {
 	case attr := <-wr.ReadAttribute():
 		assert.Equal(t, ".gitignore\"\n", attr.Filename)
-		assert.Equal(t, "linguist-vendored", attr.Attribute)
+		assert.Equal(t, AttributeLinguistVendored, attr.Attribute)
 		assert.Equal(t, "unspecified", attr.Value)
 	case <-time.After(100 * time.Millisecond):
 		assert.FailNow(t, "took too long to read an attribute from the list")
@@ -38,7 +38,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
 	select {
 	case attr := <-wr.ReadAttribute():
 		assert.Equal(t, ".gitignore\"\n", attr.Filename)
-		assert.Equal(t, "linguist-vendored", attr.Attribute)
+		assert.Equal(t, AttributeLinguistVendored, attr.Attribute)
 		assert.Equal(t, "unspecified", attr.Value)
 	case <-time.After(100 * time.Millisecond):
 		assert.FailNow(t, "took too long to read an attribute from the list")
@@ -77,21 +77,21 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, attributeTriple{
 		Filename:  "shouldbe.vendor",
-		Attribute: "linguist-vendored",
+		Attribute: AttributeLinguistVendored,
 		Value:     "set",
 	}, attr)
 	attr = <-wr.ReadAttribute()
 	assert.NoError(t, err)
 	assert.EqualValues(t, attributeTriple{
 		Filename:  "shouldbe.vendor",
-		Attribute: "linguist-generated",
+		Attribute: AttributeLinguistGenerated,
 		Value:     "unspecified",
 	}, attr)
 	attr = <-wr.ReadAttribute()
 	assert.NoError(t, err)
 	assert.EqualValues(t, attributeTriple{
 		Filename:  "shouldbe.vendor",
-		Attribute: "linguist-language",
+		Attribute: AttributeLinguistLanguage,
 		Value:     "unspecified",
 	}, attr)
 }
diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go
index c40d6937b5..8551ea9d24 100644
--- a/modules/git/repo_language_stats.go
+++ b/modules/git/repo_language_stats.go
@@ -6,6 +6,8 @@ package git
 import (
 	"strings"
 	"unicode"
+
+	"code.gitea.io/gitea/modules/optional"
 )
 
 const (
@@ -46,3 +48,20 @@ func mergeLanguageStats(stats map[string]int64) map[string]int64 {
 	}
 	return res
 }
+
+func TryReadLanguageAttribute(attrs map[string]string) optional.Option[string] {
+	language := AttributeToString(attrs, AttributeLinguistLanguage)
+	if language.Value() == "" {
+		language = AttributeToString(attrs, AttributeGitlabLanguage)
+		if language.Has() {
+			raw := language.Value()
+			// gitlab-language may have additional parameters after the language
+			// ignore them and just use the main language
+			// https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type
+			if idx := strings.IndexByte(raw, '?'); idx >= 0 {
+				language = optional.Some(raw[:idx])
+			}
+		}
+	}
+	return language
+}
diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go
index 99c7a894d5..a34c03c781 100644
--- a/modules/git/repo_language_stats_gogit.go
+++ b/modules/git/repo_language_stats_gogit.go
@@ -8,7 +8,6 @@ package git
 import (
 	"bytes"
 	"io"
-	"strings"
 
 	"code.gitea.io/gitea/modules/analyze"
 	"code.gitea.io/gitea/modules/optional"
@@ -66,36 +65,27 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 		if checker != nil {
 			attrs, err := checker.CheckPath(f.Name)
 			if err == nil {
-				isVendored = attributeToBool(attrs, "linguist-vendored")
+				isVendored = AttributeToBool(attrs, AttributeLinguistVendored)
 				if isVendored.ValueOrDefault(false) {
 					return nil
 				}
 
-				isGenerated = attributeToBool(attrs, "linguist-generated")
+				isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated)
 				if isGenerated.ValueOrDefault(false) {
 					return nil
 				}
 
-				isDocumentation = attributeToBool(attrs, "linguist-documentation")
+				isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation)
 				if isDocumentation.ValueOrDefault(false) {
 					return nil
 				}
 
-				isDetectable = attributeToBool(attrs, "linguist-detectable")
+				isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable)
 				if !isDetectable.ValueOrDefault(true) {
 					return nil
 				}
 
-				hasLanguage := attributeToString(attrs, "linguist-language")
-				if hasLanguage.Value() == "" {
-					hasLanguage = attributeToString(attrs, "gitlab-language")
-					if hasLanguage.Has() {
-						language := hasLanguage.Value()
-						if idx := strings.IndexByte(language, '?'); idx >= 0 {
-							hasLanguage = optional.Some(language[:idx])
-						}
-					}
-				}
+				hasLanguage := TryReadLanguageAttribute(attrs)
 				if hasLanguage.Value() != "" {
 					language := hasLanguage.Value()
 
diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go
index 16669924d6..318fc091ce 100644
--- a/modules/git/repo_language_stats_nogogit.go
+++ b/modules/git/repo_language_stats_nogogit.go
@@ -8,7 +8,6 @@ package git
 import (
 	"bytes"
 	"io"
-	"strings"
 
 	"code.gitea.io/gitea/modules/analyze"
 	"code.gitea.io/gitea/modules/log"
@@ -97,36 +96,27 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 		if checker != nil {
 			attrs, err := checker.CheckPath(f.Name())
 			if err == nil {
-				isVendored = attributeToBool(attrs, "linguist-vendored")
+				isVendored = AttributeToBool(attrs, AttributeLinguistVendored)
 				if isVendored.ValueOrDefault(false) {
 					continue
 				}
 
-				isGenerated = attributeToBool(attrs, "linguist-generated")
+				isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated)
 				if isGenerated.ValueOrDefault(false) {
 					continue
 				}
 
-				isDocumentation = attributeToBool(attrs, "linguist-documentation")
+				isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation)
 				if isDocumentation.ValueOrDefault(false) {
 					continue
 				}
 
-				isDetectable = attributeToBool(attrs, "linguist-detectable")
+				isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable)
 				if !isDetectable.ValueOrDefault(true) {
 					continue
 				}
 
-				hasLanguage := attributeToString(attrs, "linguist-language")
-				if hasLanguage.Value() == "" {
-					hasLanguage = attributeToString(attrs, "gitlab-language")
-					if hasLanguage.Has() {
-						language := hasLanguage.Value()
-						if idx := strings.IndexByte(language, '?'); idx >= 0 {
-							hasLanguage = optional.Some(language[:idx])
-						}
-					}
-				}
+				hasLanguage := TryReadLanguageAttribute(attrs)
 				if hasLanguage.Value() != "" {
 					language := hasLanguage.Value()
 
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 33a5941d36..48a35dd060 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -635,11 +635,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			defer deferable()
 			attrs, err := checker.CheckPath(ctx.Repo.TreePath)
 			if err == nil {
-				vendored, has := attrs["linguist-vendored"]
-				ctx.Data["IsVendored"] = has && (vendored == "set" || vendored == "true")
-
-				generated, has := attrs["linguist-generated"]
-				ctx.Data["IsGenerated"] = has && (generated == "set" || generated == "true")
+				ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
+				ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
 			}
 		}
 	}
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 0f6e2b6c17..740c748347 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -29,6 +29,7 @@ import (
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/translation"
 
@@ -1181,41 +1182,30 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 
 	for _, diffFile := range diff.Files {
 
-		gotVendor := false
-		gotGenerated := false
+		isVendored := optional.None[bool]()
+		isGenerated := optional.None[bool]()
 		if checker != nil {
 			attrs, err := checker.CheckPath(diffFile.Name)
 			if err == nil {
-				if vendored, has := attrs["linguist-vendored"]; has {
-					if vendored == "set" || vendored == "true" {
-						diffFile.IsVendored = true
-						gotVendor = true
-					} else {
-						gotVendor = vendored == "false"
-					}
-				}
-				if generated, has := attrs["linguist-generated"]; has {
-					if generated == "set" || generated == "true" {
-						diffFile.IsGenerated = true
-						gotGenerated = true
-					} else {
-						gotGenerated = generated == "false"
-					}
-				}
-				if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
-					diffFile.Language = language
-				} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
-					diffFile.Language = language
+				isVendored = git.AttributeToBool(attrs, git.AttributeLinguistVendored)
+				isGenerated = git.AttributeToBool(attrs, git.AttributeLinguistGenerated)
+
+				language := git.TryReadLanguageAttribute(attrs)
+				if language.Has() {
+					diffFile.Language = language.Value()
 				}
 			}
 		}
 
-		if !gotVendor {
-			diffFile.IsVendored = analyze.IsVendor(diffFile.Name)
+		if !isVendored.Has() {
+			isVendored = optional.Some(analyze.IsVendor(diffFile.Name))
 		}
-		if !gotGenerated {
-			diffFile.IsGenerated = analyze.IsGenerated(diffFile.Name)
+		diffFile.IsVendored = isVendored.Value()
+
+		if !isGenerated.Has() {
+			isGenerated = optional.Some(analyze.IsGenerated(diffFile.Name))
 		}
+		diffFile.IsGenerated = isGenerated.Value()
 
 		tailSection := diffFile.GetTailSection(gitRepo, opts.BeforeCommitID, opts.AfterCommitID)
 		if tailSection != nil {
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index f2a7677688..9500b8f46d 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -282,7 +282,7 @@ func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (
 
 	filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
 		CachedOnly: true,
-		Attributes: []string{"linguist-language", "gitlab-language"},
+		Attributes: []string{git.AttributeLinguistLanguage, git.AttributeGitlabLanguage},
 		Filenames:  []string{treePath},
 		IndexFile:  indexFilename,
 		WorkTree:   worktree,
@@ -291,13 +291,7 @@ func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (
 		return "", err
 	}
 
-	language := filename2attribute2info[treePath]["linguist-language"]
-	if language == "" || language == "unspecified" {
-		language = filename2attribute2info[treePath]["gitlab-language"]
-	}
-	if language == "unspecified" {
-		language = ""
-	}
+	language := git.TryReadLanguageAttribute(filename2attribute2info[treePath])
 
-	return language, nil
+	return language.Value(), nil
 }

From c86d033a3ec0514efcd9524d03dce1b6551e487f Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 24 Feb 2024 21:11:51 +0200
Subject: [PATCH 155/679] Remove jQuery from the Unicode escape button (#29369)

- Switched to plain JavaScript
- Tested the Unicode escape button functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/664f0ced-876b-4cb7-a668-bd62169fc843)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/repo-unicode-escape.js | 46 ++++++++++------------
 web_src/js/utils/dom.js                    |  4 ++
 2 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/web_src/js/features/repo-unicode-escape.js b/web_src/js/features/repo-unicode-escape.js
index 6a201ec4d1..d878532001 100644
--- a/web_src/js/features/repo-unicode-escape.js
+++ b/web_src/js/features/repo-unicode-escape.js
@@ -1,31 +1,27 @@
-import $ from 'jquery';
-import {hideElem, showElem} from '../utils/dom.js';
+import {hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.js';
 
 export function initUnicodeEscapeButton() {
-  $(document).on('click', '.escape-button', (e) => {
+  document.addEventListener('click', (e) => {
+    const btn = e.target.closest('.escape-button, .unescape-button, .toggle-escape-button');
+    if (!btn) return;
+
     e.preventDefault();
-    $(e.target).parents('.file-content, .non-diff-file-content').find('.file-code, .file-view').addClass('unicode-escaped');
-    hideElem($(e.target));
-    showElem($(e.target).siblings('.unescape-button'));
-  });
-  $(document).on('click', '.unescape-button', (e) => {
-    e.preventDefault();
-    $(e.target).parents('.file-content, .non-diff-file-content').find('.file-code, .file-view').removeClass('unicode-escaped');
-    hideElem($(e.target));
-    showElem($(e.target).siblings('.escape-button'));
-  });
-  $(document).on('click', '.toggle-escape-button', (e) => {
-    e.preventDefault();
-    const fileContent = $(e.target).parents('.file-content, .non-diff-file-content');
-    const fileView = fileContent.find('.file-code, .file-view');
-    if (fileView.hasClass('unicode-escaped')) {
-      fileView.removeClass('unicode-escaped');
-      hideElem(fileContent.find('.unescape-button'));
-      showElem(fileContent.find('.escape-button'));
-    } else {
-      fileView.addClass('unicode-escaped');
-      showElem(fileContent.find('.unescape-button'));
-      hideElem(fileContent.find('.escape-button'));
+
+    const fileContent = btn.closest('.file-content, .non-diff-file-content');
+    const fileView = fileContent?.querySelectorAll('.file-code, .file-view');
+    if (btn.matches('.escape-button')) {
+      for (const el of fileView) el.classList.add('unicode-escaped');
+      hideElem(btn);
+      showElem(queryElemSiblings(btn, '.unescape-button'));
+    } else if (btn.matches('.unescape-button')) {
+      for (const el of fileView) el.classList.remove('unicode-escaped');
+      hideElem(btn);
+      showElem(queryElemSiblings(btn, '.escape-button'));
+    } else if (btn.matches('.toggle-escape-button')) {
+      const isEscaped = fileView[0]?.classList.contains('unicode-escaped');
+      for (const el of fileView) el.classList.toggle('unicode-escaped', !isEscaped);
+      toggleElem(fileContent.querySelectorAll('.unescape-button'), !isEscaped);
+      toggleElem(fileContent.querySelectorAll('.escape-button'), isEscaped);
     }
   });
 }
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index ca24650f76..91535dc187 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -51,6 +51,10 @@ export function isElemHidden(el) {
   return res[0];
 }
 
+export function queryElemSiblings(el, selector) {
+  return Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector));
+}
+
 export function onDomReady(cb) {
   if (document.readyState === 'loading') {
     document.addEventListener('DOMContentLoaded', cb);

From ff9dc512438f1a3bc36cc8c419d8450f808517f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Nicas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Sat, 24 Feb 2024 21:19:49 +0100
Subject: [PATCH 156/679] Apply to become a maintainer (zokkis) (#29383)

---
 MAINTAINERS | 1 +
 1 file changed, 1 insertion(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 72171f80ed..2f95fdca50 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -59,3 +59,4 @@ Rui Chen  <rui@chenrui.dev> (@chenrui333)
 Nanguan Lin <nanguanlin6@gmail.com> (@lng2020)
 kerwin612 <kerwin612@qq.com> (@kerwin612)
 Gary Wang <git@blumia.net> (@BLumia)
+Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)

From 10c7996b5a5c705964fc6cc9c1817eea1fc436ef Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 25 Feb 2024 06:34:51 +0800
Subject: [PATCH 157/679] Remove RenderEmojiPlain from template helper (#29375)

RenderEmojiPlain(emoji.ReplaceAliases) should be called explicitly for
some contents, but not for everything.

Actually in modern days, in most cases it doesn't need such
"ReplaceAliases". So only keep it for issue/PR titles.

If anyone really needs to do ReplaceAliases for some contents, I will
propose a following fix.
---
 modules/templates/helper.go                | 12 ------------
 routers/web/repo/issue.go                  |  3 ++-
 routers/web/repo/pull.go                   |  3 ++-
 templates/base/head.tmpl                   |  2 +-
 templates/repo/issue/choose.tmpl           |  8 ++++----
 templates/repo/settings/lfs_file_find.tmpl |  2 +-
 6 files changed, 10 insertions(+), 20 deletions(-)

diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 6e42594b0b..691f754748 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -14,7 +14,6 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/svg"
@@ -159,7 +158,6 @@ func NewFuncMap() template.FuncMap {
 		"RenderCodeBlock":  RenderCodeBlock,
 		"RenderIssueTitle": RenderIssueTitle,
 		"RenderEmoji":      RenderEmoji,
-		"RenderEmojiPlain": RenderEmojiPlain,
 		"ReactionToEmoji":  ReactionToEmoji,
 
 		"RenderMarkdownToHtml": RenderMarkdownToHtml,
@@ -215,16 +213,6 @@ func JSEscapeSafe(s string) template.HTML {
 	return template.HTML(template.JSEscapeString(s))
 }
 
-func RenderEmojiPlain(s any) any {
-	switch v := s.(type) {
-	case string:
-		return emoji.ReplaceAliases(v)
-	case template.HTML:
-		return template.HTML(emoji.ReplaceAliases(string(v)))
-	}
-	panic(fmt.Sprintf("unexpected type %T", s))
-}
-
 // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
 func DotEscape(raw string) string {
 	return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 9f08607642..245ed2b2f2 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -32,6 +32,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 	issue_template "code.gitea.io/gitea/modules/issue/template"
@@ -1435,7 +1436,7 @@ func ViewIssue(ctx *context.Context) {
 		return
 	}
 
-	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
 
 	iw := new(issues_model.IssueWatch)
 	if ctx.Doer != nil {
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 14f1eb3102..7ab21f22b9 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -28,6 +28,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	issue_template "code.gitea.io/gitea/modules/issue/template"
@@ -335,7 +336,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
 		ctx.ServerError("LoadRepo", err)
 		return nil, false
 	}
-	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
 	ctx.Data["Issue"] = issue
 
 	if !issue.IsPull {
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index d7e28474e7..2de8f58235 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -2,7 +2,7 @@
 <html lang="{{ctx.Locale.Lang}}" data-theme="{{ThemeName .SignedUser}}">
 <head>
 	<meta name="viewport" content="width=device-width, initial-scale=1">
-	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
+	<title>{{if .Title}}{{.Title}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
 	{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
 	<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl
index 127b9d7d87..a8037482be 100644
--- a/templates/repo/issue/choose.tmpl
+++ b/templates/repo/issue/choose.tmpl
@@ -11,8 +11,8 @@
 			<div class="ui attached segment">
 				<div class="ui two column grid">
 					<div class="column left aligned">
-						<strong>{{.Name | RenderEmojiPlain}}</strong>
-						<br>{{.About | RenderEmojiPlain}}
+						<strong>{{.Name}}</strong>
+						<br>{{.About}}
 					</div>
 					<div class="column right aligned">
 						<a href="{{$.RepoLink}}/issues/new?template={{.FileName}}{{if $.milestone}}&milestone={{$.milestone}}{{end}}{{if $.project}}&project={{$.project}}{{end}}" class="ui primary button">{{ctx.Locale.Tr "repo.issues.choose.get_started"}}</a>
@@ -24,8 +24,8 @@
 			<div class="ui attached segment">
 				<div class="ui two column grid">
 					<div class="column left aligned">
-						<strong>{{.Name | RenderEmojiPlain}}</strong>
-						<br>{{.About | RenderEmojiPlain}}
+						<strong>{{.Name}}</strong>
+						<br>{{.About}}
 					</div>
 					<div class="column right aligned">
 						<a href="{{.URL}}" class="ui primary button">{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues.choose.open_external_link"}}</a>
diff --git a/templates/repo/settings/lfs_file_find.tmpl b/templates/repo/settings/lfs_file_find.tmpl
index fea9aa323f..809a028b2c 100644
--- a/templates/repo/settings/lfs_file_find.tmpl
+++ b/templates/repo/settings/lfs_file_find.tmpl
@@ -14,7 +14,7 @@
 							</td>
 							<td class="message">
 								<span class="truncate">
-									<a href="{{$.RepoLink}}/commit/{{.SHA}}" title="{{.Summary | RenderEmojiPlain}}">
+									<a href="{{$.RepoLink}}/commit/{{.SHA}}" title="{{.Summary}}">
 										{{.Summary | RenderEmoji $.Context}}
 									</a>
 								</span>

From 15d071f4f81a0ad09f260de83cb6402875b4de27 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 25 Feb 2024 01:08:51 +0200
Subject: [PATCH 158/679] Remove jQuery AJAX from repo collaborator mode
 dropdown (#29371)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the repo collaborator mode dropdown functionality and it works
as before

# Demo using `fetch` instead of jQuery AJAX

![action](https://github.com/go-gitea/gitea/assets/20454870/04466629-19b2-4469-9231-38820ee13c36)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-settings.js | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 75e624a6a7..0418f3a14a 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
 import {minimatch} from 'minimatch';
 import {createMonaco} from './codeeditor.js';
 import {onInputDebounce, toggleElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
 
 const {appSubUrl, csrfToken} = window.config;
 
@@ -11,18 +12,19 @@ export function initRepoSettingsCollaboration() {
     const $dropdown = $(e);
     const $text = $dropdown.find('> .text');
     $dropdown.dropdown({
-      action(_text, value) {
+      async action(_text, value) {
         const lastValue = $dropdown.attr('data-last-value');
-        $.post($dropdown.attr('data-url'), {
-          _csrf: csrfToken,
-          uid: $dropdown.attr('data-uid'),
-          mode: value,
-        }).fail(() => {
+        try {
+          $dropdown.attr('data-last-value', value);
+          $dropdown.dropdown('hide');
+          const data = new FormData();
+          data.append('uid', $dropdown.attr('data-uid'));
+          data.append('mode', value);
+          await POST($dropdown.attr('data-url'), {data});
+        } catch {
           $text.text('(error)'); // prevent from misleading users when error occurs
           $dropdown.attr('data-last-value', lastValue);
-        });
-        $dropdown.attr('data-last-value', value);
-        $dropdown.dropdown('hide');
+        }
       },
       onChange(_value, text, _$choice) {
         $text.text(text); // update the text when using keyboard navigating

From 328d908b4fb67da0d9e5a031ee0fcd78927baaa3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 25 Feb 2024 08:13:04 +0800
Subject: [PATCH 159/679] Move citiation button to proper place (#29374)

The citiation button shouldn't be controlled by
DisableDownloadSourceArchives (line 134)

So move it out of that "if" block.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 templates/repo/home.tmpl | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index f7c74c9aba..2c08fb02d5 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -135,9 +135,9 @@
 									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
 									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
 									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
-									{{if .CitiationExist}}
-										<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
-									{{end}}
+								{{end}}
+								{{if .CitiationExist}}
+									<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
 								{{end}}
 								{{range .OpenWithEditorApps}}
 									<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>

From b616f666b89f57f3c285b70c11693f50ba38bcaa Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 25 Feb 2024 06:09:55 +0200
Subject: [PATCH 160/679] Remove jQuery AJAX from the repo commit graph
 (#29373)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the repo collaborator mode dropdown functionality and it works
as before

# Demo using `fetch` instead of jQuery AJAX

![action](https://github.com/go-gitea/gitea/assets/20454870/7e2f166e-9941-4f26-9666-d00cdf3d9f60)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-graph.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
index e445ae1103..c83f448b76 100644
--- a/web_src/js/features/repo-graph.js
+++ b/web_src/js/features/repo-graph.js
@@ -1,4 +1,5 @@
 import $ from 'jquery';
+import {GET} from '../modules/fetch.js';
 
 export function initRepoGraphGit() {
   const graphContainer = document.getElementById('git-graph-container');
@@ -60,7 +61,9 @@ export function initRepoGraphGit() {
     $('#rev-container').addClass('gt-hidden');
     $('#loading-indicator').removeClass('gt-hidden');
     (async () => {
-      const div = $(await $.ajax(String(ajaxUrl)));
+      const response = await GET(String(ajaxUrl));
+      const html = await response.text();
+      const div = $(html);
       $('#pagination').html(div.find('#pagination').html());
       $('#rel-container').html(div.find('#rel-container').html());
       $('#rev-container').html(div.find('#rev-container').html());

From 736c98be5c2bed26cef9f7f679c49a95af8161ef Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 25 Feb 2024 12:17:11 +0800
Subject: [PATCH 161/679] Refactor `copy` button event handler (#29379)

Use "closest" instead of "for-loop"
---
 web_src/js/features/clipboard.js | 41 +++++++++++++-------------------
 1 file changed, 17 insertions(+), 24 deletions(-)

diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
index 224628658e..8be5505c8b 100644
--- a/web_src/js/features/clipboard.js
+++ b/web_src/js/features/clipboard.js
@@ -5,37 +5,30 @@ import {clippie} from 'clippie';
 const {copy_success, copy_error} = window.config.i18n;
 
 // Enable clipboard copy from HTML attributes. These properties are supported:
-// - data-clipboard-text: Direct text to copy, has highest precedence
+// - data-clipboard-text: Direct text to copy
 // - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied
 // - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
 export function initGlobalCopyToClipboardListener() {
-  document.addEventListener('click', (e) => {
-    let target = e.target;
-    // In case <button data-clipboard-text><svg></button>, so we just search
-    // up to 3 levels for performance
-    for (let i = 0; i < 3 && target; i++) {
-      let text = target.getAttribute('data-clipboard-text');
+  document.addEventListener('click', async (e) => {
+    const target = e.target.closest('[data-clipboard-text], [data-clipboard-target]');
+    if (!target) return;
 
-      if (!text && target.getAttribute('data-clipboard-target')) {
-        text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
-      }
+    e.preventDefault();
 
-      if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
-        text = toAbsoluteUrl(text);
-      }
+    let text;
+    if (target.hasAttribute('data-clipboard-text')) {
+      text = target.getAttribute('data-clipboard-text');
+    } else {
+      text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
+    }
 
-      if (text) {
-        e.preventDefault();
+    if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
+      text = toAbsoluteUrl(text);
+    }
 
-        (async() => {
-          const success = await clippie(text);
-          showTemporaryTooltip(target, success ? copy_success : copy_error);
-        })();
-
-        break;
-      }
-
-      target = target.parentElement;
+    if (text) {
+      const success = await clippie(text);
+      showTemporaryTooltip(target, success ? copy_success : copy_error);
     }
   });
 }

From 1f6de13897fa0ac74087b2d1ec00cbef06caf2f7 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 25 Feb 2024 06:42:29 +0200
Subject: [PATCH 162/679] Remove jQuery AJAX from the markdown editor preview
 (#29384)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the markdown editor preview button functionality and it works
as before

# Demo using `fetch` instead of jQuery AJAX

![action](https://github.com/go-gitea/gitea/assets/20454870/3fc7abb8-4fdc-46e9-95f6-087d9526bb52)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 .../js/features/comp/ComboMarkdownEditor.js   | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index d209f11ab2..4c973358e3 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -9,6 +9,7 @@ import {renderPreviewPanelContent} from '../repo-editor.js';
 import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
 import {initTextExpander} from './TextExpander.js';
 import {showErrorToast} from '../../modules/toast.js';
+import {POST} from '../../modules/fetch.js';
 
 let elementIdCounter = 0;
 
@@ -147,16 +148,15 @@ class ComboMarkdownEditor {
     this.previewContext = $tabPreviewer.attr('data-preview-context');
     this.previewMode = this.options.previewMode ?? 'comment';
     this.previewWiki = this.options.previewWiki ?? false;
-    $tabPreviewer.on('click', () => {
-      $.post(this.previewUrl, {
-        _csrf: window.config.csrfToken,
-        mode: this.previewMode,
-        context: this.previewContext,
-        text: this.value(),
-        wiki: this.previewWiki,
-      }, (data) => {
-        renderPreviewPanelContent($panelPreviewer, data);
-      });
+    $tabPreviewer.on('click', async () => {
+      const formData = new FormData();
+      formData.append('mode', this.previewMode);
+      formData.append('context', this.previewContext);
+      formData.append('text', this.value());
+      formData.append('wiki', this.previewWiki);
+      const response = await POST(this.previewUrl, {data: formData});
+      const data = await response.text();
+      renderPreviewPanelContent($panelPreviewer, data);
     });
   }
 

From 4e3d81e44ee3f504f7262966533305561e04101f Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 25 Feb 2024 07:07:23 +0200
Subject: [PATCH 163/679] Remove jQuery from the code diff expansion buttons
 (#29385)

- Removed all jQuery AJAX calls and replaced with htmx
- Tested the code diff expansion buttons functionality and it works as
before plus a loading indicator

# Demo using `htmx` instead of jQuery AJAX

![action](https://github.com/go-gitea/gitea/assets/20454870/afba7442-ed56-4d39-b764-835d1f6c3a9c)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 templates/repo/diff/blob_excerpt.tmpl    | 12 ++++++------
 templates/repo/diff/section_split.tmpl   |  6 +++---
 templates/repo/diff/section_unified.tmpl |  6 +++---
 web_src/js/features/repo-code.js         |  8 --------
 4 files changed, 12 insertions(+), 20 deletions(-)

diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl
index 2dff28a965..353f6db705 100644
--- a/templates/repo/diff/blob_excerpt.tmpl
+++ b/templates/repo/diff/blob_excerpt.tmpl
@@ -5,17 +5,17 @@
 			<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}">
 				<div class="gt-df">
 				{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-					<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+					<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 						{{svg "octicon-fold-down"}}
 					</button>
 				{{end}}
 				{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-					<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+					<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 						{{svg "octicon-fold-up"}}
 					</button>
 				{{end}}
 				{{if eq $line.GetExpandDirection 2}}
-					<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+					<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 						{{svg "octicon-fold"}}
 					</button>
 				{{end}}
@@ -51,17 +51,17 @@
 			<td colspan="2" class="lines-num">
 				<div class="gt-df">
 					{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-						<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+						<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 							{{svg "octicon-fold-down"}}
 						</button>
 					{{end}}
 					{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-						<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+						<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 							{{svg "octicon-fold-up"}}
 						</button>
 					{{end}}
 					{{if eq $line.GetExpandDirection 2}}
-						<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+						<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 							{{svg "octicon-fold"}}
 						</button>
 					{{end}}
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index 5b0d982e96..672193565b 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -18,17 +18,17 @@
 					<td class="lines-num lines-num-old">
 						<div class="gt-df">
 						{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-							<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 								{{svg "octicon-fold-down"}}
 							</button>
 						{{end}}
 						{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-							<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 								{{svg "octicon-fold-up"}}
 							</button>
 						{{end}}
 						{{if eq $line.GetExpandDirection 2}}
-							<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 								{{svg "octicon-fold"}}
 							</button>
 						{{end}}
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index 2b901411e2..2c271d0866 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -14,17 +14,17 @@
 					<td colspan="2" class="lines-num">
 						<div class="gt-df">
 							{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-								<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 									{{svg "octicon-fold-down"}}
 								</button>
 							{{end}}
 							{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-								<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 									{{svg "octicon-fold-up"}}
 								</button>
 							{{end}}
 							{{if eq $line.GetExpandDirection 2}}
-								<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 									{{svg "octicon-fold"}}
 								</button>
 							{{end}}
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index a142313211..c4a81ea165 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -186,14 +186,6 @@ export function initRepoCodeView() {
   $(document).on('click', '.fold-file', ({currentTarget}) => {
     invertFileFolding(currentTarget.closest('.file-content'), currentTarget);
   });
-  $(document).on('click', '.code-expander-button', async ({currentTarget}) => {
-    const url = currentTarget.getAttribute('data-url');
-    const query = currentTarget.getAttribute('data-query');
-    const anchor = currentTarget.getAttribute('data-anchor');
-    if (!url) return;
-    const blob = await $.get(`${url}?${query}&anchor=${anchor}`);
-    currentTarget.closest('tr').outerHTML = blob;
-  });
   $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => {
     await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url')));
   });

From 1ef87773b1e75b99b4b842303542fd17d9c2e6f7 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 25 Feb 2024 13:35:47 +0800
Subject: [PATCH 164/679] Refactor modules/git global variables (#29376)

Move some global variables into a struct to improve maintainability
---
 modules/git/git.go                   | 38 +++++++++++++++-------------
 modules/git/repo.go                  |  2 +-
 routers/private/hook_post_receive.go |  2 +-
 routers/private/hook_pre_receive.go  |  2 +-
 routers/private/hook_proc_receive.go |  2 +-
 routers/private/serv.go              |  2 +-
 routers/web/misc/misc.go             |  2 +-
 routers/web/repo/githttp.go          |  2 +-
 8 files changed, 27 insertions(+), 25 deletions(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index 8621df0f49..f688ea7488 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -33,16 +33,18 @@ var (
 	// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
 	DefaultContext context.Context
 
-	SupportProcReceive bool // >= 2.29
-	SupportHashSha256  bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
+	DefaultFeatures struct {
+		GitVersion *version.Version
 
-	gitVersion *version.Version
+		SupportProcReceive bool // >= 2.29
+		SupportHashSha256  bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
+	}
 )
 
 // loadGitVersion tries to get the current git version and stores it into a global variable
 func loadGitVersion() error {
 	// doesn't need RWMutex because it's executed by Init()
-	if gitVersion != nil {
+	if DefaultFeatures.GitVersion != nil {
 		return nil
 	}
 
@@ -53,7 +55,7 @@ func loadGitVersion() error {
 
 	ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
 	if err == nil {
-		gitVersion = ver
+		DefaultFeatures.GitVersion = ver
 	}
 	return err
 }
@@ -93,7 +95,7 @@ func SetExecutablePath(path string) error {
 		return err
 	}
 
-	if gitVersion.LessThan(versionRequired) {
+	if DefaultFeatures.GitVersion.LessThan(versionRequired) {
 		moreHint := "get git: https://git-scm.com/download/"
 		if runtime.GOOS == "linux" {
 			// there are a lot of CentOS/RHEL users using old git, so we add a special hint for them
@@ -102,22 +104,22 @@ func SetExecutablePath(path string) error {
 				moreHint = "get git: https://git-scm.com/download/linux and https://ius.io"
 			}
 		}
-		return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint)
+		return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures.GitVersion.Original(), RequiredVersion, moreHint)
 	}
 
-	if err = checkGitVersionCompatibility(gitVersion); err != nil {
-		return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", gitVersion.String(), err)
+	if err = checkGitVersionCompatibility(DefaultFeatures.GitVersion); err != nil {
+		return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", DefaultFeatures.GitVersion.String(), err)
 	}
 	return nil
 }
 
 // VersionInfo returns git version information
 func VersionInfo() string {
-	if gitVersion == nil {
+	if DefaultFeatures.GitVersion == nil {
 		return "(git not found)"
 	}
 	format := "%s"
-	args := []any{gitVersion.Original()}
+	args := []any{DefaultFeatures.GitVersion.Original()}
 	// Since git wire protocol has been released from git v2.18
 	if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
 		format += ", Wire Protocol %s Enabled"
@@ -187,9 +189,9 @@ func InitFull(ctx context.Context) (err error) {
 	if CheckGitVersionAtLeast("2.9") == nil {
 		globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
 	}
-	SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
-	SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
-	if SupportHashSha256 {
+	DefaultFeatures.SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
+	DefaultFeatures.SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
+	if DefaultFeatures.SupportHashSha256 {
 		SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
 	} else {
 		log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported")
@@ -254,7 +256,7 @@ func syncGitConfig() (err error) {
 		}
 	}
 
-	if SupportProcReceive {
+	if DefaultFeatures.SupportProcReceive {
 		// set support for AGit flow
 		if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
 			return err
@@ -309,15 +311,15 @@ func syncGitConfig() (err error) {
 
 // CheckGitVersionAtLeast check git version is at least the constraint version
 func CheckGitVersionAtLeast(atLeast string) error {
-	if gitVersion == nil {
+	if DefaultFeatures.GitVersion == nil {
 		panic("git module is not initialized") // it shouldn't happen
 	}
 	atLeastVersion, err := version.NewVersion(atLeast)
 	if err != nil {
 		return err
 	}
-	if gitVersion.Compare(atLeastVersion) < 0 {
-		return fmt.Errorf("installed git binary version %s is not at least %s", gitVersion.Original(), atLeast)
+	if DefaultFeatures.GitVersion.Compare(atLeastVersion) < 0 {
+		return fmt.Errorf("installed git binary version %s is not at least %s", DefaultFeatures.GitVersion.Original(), atLeast)
 	}
 	return nil
 }
diff --git a/modules/git/repo.go b/modules/git/repo.go
index 60078f3273..cef45c6af0 100644
--- a/modules/git/repo.go
+++ b/modules/git/repo.go
@@ -101,7 +101,7 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma
 	if !IsValidObjectFormat(objectFormatName) {
 		return fmt.Errorf("invalid object format: %s", objectFormatName)
 	}
-	if SupportHashSha256 {
+	if DefaultFeatures.SupportHashSha256 {
 		cmd.AddOptionValues("--object-format", objectFormatName)
 	}
 
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 1b274ae154..8b954a8130 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -124,7 +124,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 
 		// post update for agit pull request
 		// FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR
-		if git.SupportProcReceive && refFullName.IsPull() {
+		if git.DefaultFeatures.SupportProcReceive && refFullName.IsPull() {
 			if repo == nil {
 				repo = loadRepository(ctx, ownerName, repoName)
 				if ctx.Written() {
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index f28ae4c0eb..ad52f35084 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -122,7 +122,7 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
 			preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
 		case refFullName.IsTag():
 			preReceiveTag(ourCtx, oldCommitID, newCommitID, refFullName)
-		case git.SupportProcReceive && refFullName.IsFor():
+		case git.DefaultFeatures.SupportProcReceive && refFullName.IsFor():
 			preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
 		default:
 			ourCtx.AssertCanWriteCode()
diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go
index 5577120770..5805202bb5 100644
--- a/routers/private/hook_proc_receive.go
+++ b/routers/private/hook_proc_receive.go
@@ -18,7 +18,7 @@ import (
 // HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
 func HookProcReceive(ctx *gitea_context.PrivateContext) {
 	opts := web.GetForm(ctx).(*private.HookOptions)
-	if !git.SupportProcReceive {
+	if !git.DefaultFeatures.SupportProcReceive {
 		ctx.Status(http.StatusNotFound)
 		return
 	}
diff --git a/routers/private/serv.go b/routers/private/serv.go
index 00731947a5..3812ccb52b 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -297,7 +297,7 @@ func ServCommand(ctx *context.PrivateContext) {
 			}
 		} else {
 			// Because of the special ref "refs/for" we will need to delay write permission check
-			if git.SupportProcReceive && unitType == unit.TypeCode {
+			if git.DefaultFeatures.SupportProcReceive && unitType == unit.TypeCode {
 				mode = perm.AccessModeRead
 			}
 
diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go
index 54c93763f6..ac5496ce91 100644
--- a/routers/web/misc/misc.go
+++ b/routers/web/misc/misc.go
@@ -15,7 +15,7 @@ import (
 )
 
 func SSHInfo(rw http.ResponseWriter, req *http.Request) {
-	if !git.SupportProcReceive {
+	if !git.DefaultFeatures.SupportProcReceive {
 		rw.WriteHeader(http.StatusNotFound)
 		return
 	}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index f52abbfb02..27c7f4961d 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -183,7 +183,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
 
 		if repoExist {
 			// Because of special ref "refs/for" .. , need delay write permission check
-			if git.SupportProcReceive {
+			if git.DefaultFeatures.SupportProcReceive {
 				accessMode = perm.AccessModeRead
 			}
 

From 2e33671f2c1e98759e4fd2a90944c534cfdf5776 Mon Sep 17 00:00:00 2001
From: Jimmy Praet <jimmy.praet@telenet.be>
Date: Sun, 25 Feb 2024 07:00:55 +0100
Subject: [PATCH 165/679] Add attachment support for code review comments
 (#29220)

Fixes #27960, #24411, #12183

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/issues/comment.go                      |  37 +++---
 routers/api/v1/repo/pull_review.go            |   1 +
 routers/web/repo/issue.go                     |   4 +
 routers/web/repo/pull.go                      |  13 ++
 routers/web/repo/pull_review.go               |  19 +++
 routers/web/repo/pull_review_test.go          |   2 +-
 services/forms/repo_form.go                   |   1 +
 services/mailer/incoming/incoming_handler.go  |   1 +
 services/pull/review.go                       |   7 +-
 templates/repo/diff/box.tmpl                  |   5 +
 templates/repo/diff/comment_form.tmpl         |   6 +
 templates/repo/diff/comments.tmpl             |   5 +-
 .../repo/issue/view_content/conversation.tmpl |   3 +
 web_src/js/features/common-global.js          | 113 +++++++++---------
 web_src/js/features/repo-issue.js             |   7 ++
 15 files changed, 150 insertions(+), 74 deletions(-)

diff --git a/models/issues/comment.go b/models/issues/comment.go
index fa0eb3cc0f..c7b22f3cca 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -855,6 +855,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 	// Check comment type.
 	switch opts.Type {
 	case CommentTypeCode:
+		if err = updateAttachments(ctx, opts, comment); err != nil {
+			return err
+		}
 		if comment.ReviewID != 0 {
 			if comment.Review == nil {
 				if err := comment.loadReview(ctx); err != nil {
@@ -872,22 +875,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 		}
 		fallthrough
 	case CommentTypeReview:
-		// Check attachments
-		attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
-		if err != nil {
-			return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
+		if err = updateAttachments(ctx, opts, comment); err != nil {
+			return err
 		}
-
-		for i := range attachments {
-			attachments[i].IssueID = opts.Issue.ID
-			attachments[i].CommentID = comment.ID
-			// No assign value could be 0, so ignore AllCols().
-			if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
-				return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
-			}
-		}
-
-		comment.Attachments = attachments
 	case CommentTypeReopen, CommentTypeClose:
 		if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil {
 			return err
@@ -897,6 +887,23 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 	return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
 }
 
+func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error {
+	attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
+	if err != nil {
+		return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
+	}
+	for i := range attachments {
+		attachments[i].IssueID = opts.Issue.ID
+		attachments[i].CommentID = comment.ID
+		// No assign value could be 0, so ignore AllCols().
+		if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
+			return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
+		}
+	}
+	comment.Attachments = attachments
+	return nil
+}
+
 func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
 	var content string
 	var commentType CommentType
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 07d8f4877b..6338651aae 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -362,6 +362,7 @@ func CreatePullReview(ctx *context.APIContext) {
 			true, // pending review
 			0,    // no reply
 			opts.CommitID,
+			nil,
 		); err != nil {
 			ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err)
 			return
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 245ed2b2f2..46d48c4638 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1718,6 +1718,10 @@ func ViewIssue(ctx *context.Context) {
 			for _, codeComments := range comment.Review.CodeComments {
 				for _, lineComments := range codeComments {
 					for _, c := range lineComments {
+						if err := c.LoadAttachments(ctx); err != nil {
+							ctx.ServerError("LoadAttachments", err)
+							return
+						}
 						// Check tag.
 						role, ok = marked[c.PosterID]
 						if ok {
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 7ab21f22b9..af626dad30 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -970,6 +970,19 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 		return
 	}
 
+	for _, file := range diff.Files {
+		for _, section := range file.Sections {
+			for _, line := range section.Lines {
+				for _, comment := range line.Comments {
+					if err := comment.LoadAttachments(ctx); err != nil {
+						ctx.ServerError("LoadAttachments", err)
+						return
+					}
+				}
+			}
+		}
+	}
+
 	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
 	if err != nil {
 		ctx.ServerError("LoadProtectedBranch", err)
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index f84510b39d..92665af7e7 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/forms"
 	pull_service "code.gitea.io/gitea/services/pull"
@@ -50,6 +51,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
 		return
 	}
 	ctx.Data["AfterCommitID"] = pullHeadCommitID
+	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+	upload.AddUploadContext(ctx, "comment")
 	ctx.HTML(http.StatusOK, tplNewComment)
 }
 
@@ -75,6 +78,11 @@ func CreateCodeComment(ctx *context.Context) {
 		signedLine *= -1
 	}
 
+	var attachments []string
+	if setting.Attachment.Enabled {
+		attachments = form.Files
+	}
+
 	comment, err := pull_service.CreateCodeComment(ctx,
 		ctx.Doer,
 		ctx.Repo.GitRepo,
@@ -85,6 +93,7 @@ func CreateCodeComment(ctx *context.Context) {
 		!form.SingleReview,
 		form.Reply,
 		form.LatestCommitID,
+		attachments,
 	)
 	if err != nil {
 		ctx.ServerError("CreateCodeComment", err)
@@ -168,6 +177,16 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
 		return
 	}
 
+	for _, c := range comments {
+		if err := c.LoadAttachments(ctx); err != nil {
+			ctx.ServerError("LoadAttachments", err)
+			return
+		}
+	}
+
+	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+	upload.AddUploadContext(ctx, "comment")
+
 	ctx.Data["comments"] = comments
 	if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer); err != nil {
 		ctx.ServerError("CanMarkConversation", err)
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
index 7e6594774a..8fc9cecaf3 100644
--- a/routers/web/repo/pull_review_test.go
+++ b/routers/web/repo/pull_review_test.go
@@ -39,7 +39,7 @@ func TestRenderConversation(t *testing.T) {
 
 	var preparedComment *issues_model.Comment
 	run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
-		comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID)
+		comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID, nil)
 		if !assert.NoError(t, err) {
 			return
 		}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 98d556b946..98b8d610d0 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -626,6 +626,7 @@ type CodeCommentForm struct {
 	SingleReview   bool   `form:"single_review"`
 	Reply          int64  `form:"reply"`
 	LatestCommitID string
+	Files          []string
 }
 
 // Validate validates the fields
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
index 9682c52456..5ce2cd5fd5 100644
--- a/services/mailer/incoming/incoming_handler.go
+++ b/services/mailer/incoming/incoming_handler.go
@@ -130,6 +130,7 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u
 				false, // not pending review but a single review
 				comment.ReviewID,
 				"",
+				nil,
 			)
 			if err != nil {
 				return fmt.Errorf("CreateCodeComment failed: %w", err)
diff --git a/services/pull/review.go b/services/pull/review.go
index d4ea975612..3ffc276778 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -71,7 +71,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
 }
 
 // CreateCodeComment creates a comment on the code line
-func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string) (*issues_model.Comment, error) {
+func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) {
 	var (
 		existsReview bool
 		err          error
@@ -104,6 +104,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 			treePath,
 			line,
 			replyReviewID,
+			attachments,
 		)
 		if err != nil {
 			return nil, err
@@ -144,6 +145,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 		treePath,
 		line,
 		review.ID,
+		attachments,
 	)
 	if err != nil {
 		return nil, err
@@ -162,7 +164,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 }
 
 // createCodeComment creates a plain code comment at the specified line / path
-func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) {
+func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64, attachments []string) (*issues_model.Comment, error) {
 	var commitID, patch string
 	if err := issue.LoadPullRequest(ctx); err != nil {
 		return nil, fmt.Errorf("LoadPullRequest: %w", err)
@@ -260,6 +262,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
 		ReviewID:    reviewID,
 		Patch:       patch,
 		Invalidated: invalidated,
+		Attachments: attachments,
 	})
 }
 
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index abeeacead0..b9a43a0612 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -237,6 +237,11 @@
 					"TextareaName" "content"
 					"DropzoneParentContainer" ".ui.form"
 				)}}
+				{{if .IsAttachmentEnabled}}
+					<div class="field">
+						{{template "repo/upload" .}}
+					</div>
+				{{end}}
 				<div class="text right edit buttons">
 					<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
 					<button class="ui primary save button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl
index 767c2613a0..54817d4740 100644
--- a/templates/repo/diff/comment_form.tmpl
+++ b/templates/repo/diff/comment_form.tmpl
@@ -19,6 +19,12 @@
 			"DisableAutosize" "true"
 		)}}
 
+		{{if $.root.IsAttachmentEnabled}}
+			<div class="field">
+				{{template "repo/upload" $.root}}
+			</div>
+		{{end}}
+
 		<div class="field footer gt-mx-3">
 			<span class="markup-info">{{svg "octicon-markup"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
 			<div class="gt-text-right">
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index b3d06ed6bc..b795074e49 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -61,7 +61,10 @@
 			{{end}}
 			</div>
 			<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-			<div class="edit-content-zone gt-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}"></div>
+			<div class="edit-content-zone gt-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
+			{{if .Attachments}}
+				{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+			{{end}}
 		</div>
 		{{$reactions := .Reactions.GroupByType}}
 		{{if $reactions}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 1bc850d8cf..56f1af19b2 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -94,6 +94,9 @@
 							</div>
 							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+							{{if .Attachments}}
+								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+							{{end}}
 						</div>
 						{{$reactions := .Reactions.GroupByType}}
 						{{if $reactions}}
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index e8b546970f..cd0fc6d6a9 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -200,65 +200,68 @@ export function initGlobalCommon() {
 }
 
 export function initGlobalDropzone() {
-  // Dropzone
   for (const el of document.querySelectorAll('.dropzone')) {
-    const $dropzone = $(el);
-    const _promise = createDropzone(el, {
-      url: $dropzone.data('upload-url'),
-      headers: {'X-Csrf-Token': csrfToken},
-      maxFiles: $dropzone.data('max-file'),
-      maxFilesize: $dropzone.data('max-size'),
-      acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
-      addRemoveLinks: true,
-      dictDefaultMessage: $dropzone.data('default-message'),
-      dictInvalidFileType: $dropzone.data('invalid-input-type'),
-      dictFileTooBig: $dropzone.data('file-too-big'),
-      dictRemoveFile: $dropzone.data('remove-file'),
-      timeout: 0,
-      thumbnailMethod: 'contain',
-      thumbnailWidth: 480,
-      thumbnailHeight: 480,
-      init() {
-        this.on('success', (file, data) => {
-          file.uuid = data.uuid;
-          const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
-          $dropzone.find('.files').append(input);
-          // Create a "Copy Link" element, to conveniently copy the image
-          // or file link as Markdown to the clipboard
-          const copyLinkElement = document.createElement('div');
-          copyLinkElement.className = 'gt-text-center';
-          // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
-          copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
-          copyLinkElement.addEventListener('click', async (e) => {
-            e.preventDefault();
-            let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
-            if (file.type.startsWith('image/')) {
-              fileMarkdown = `!${fileMarkdown}`;
-            } else if (file.type.startsWith('video/')) {
-              fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`;
-            }
-            const success = await clippie(fileMarkdown);
-            showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
-          });
-          file.previewTemplate.append(copyLinkElement);
-        });
-        this.on('removedfile', (file) => {
-          $(`#${file.uuid}`).remove();
-          if ($dropzone.data('remove-url')) {
-            POST($dropzone.data('remove-url'), {
-              data: new URLSearchParams({file: file.uuid}),
-            });
-          }
-        });
-        this.on('error', function (file, message) {
-          showErrorToast(message);
-          this.removeFile(file);
-        });
-      },
-    });
+    initDropzone(el);
   }
 }
 
+export function initDropzone(el) {
+  const $dropzone = $(el);
+  const _promise = createDropzone(el, {
+    url: $dropzone.data('upload-url'),
+    headers: {'X-Csrf-Token': csrfToken},
+    maxFiles: $dropzone.data('max-file'),
+    maxFilesize: $dropzone.data('max-size'),
+    acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
+    addRemoveLinks: true,
+    dictDefaultMessage: $dropzone.data('default-message'),
+    dictInvalidFileType: $dropzone.data('invalid-input-type'),
+    dictFileTooBig: $dropzone.data('file-too-big'),
+    dictRemoveFile: $dropzone.data('remove-file'),
+    timeout: 0,
+    thumbnailMethod: 'contain',
+    thumbnailWidth: 480,
+    thumbnailHeight: 480,
+    init() {
+      this.on('success', (file, data) => {
+        file.uuid = data.uuid;
+        const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
+        $dropzone.find('.files').append(input);
+        // Create a "Copy Link" element, to conveniently copy the image
+        // or file link as Markdown to the clipboard
+        const copyLinkElement = document.createElement('div');
+        copyLinkElement.className = 'gt-text-center';
+        // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
+        copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
+        copyLinkElement.addEventListener('click', async (e) => {
+          e.preventDefault();
+          let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
+          if (file.type.startsWith('image/')) {
+            fileMarkdown = `!${fileMarkdown}`;
+          } else if (file.type.startsWith('video/')) {
+            fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`;
+          }
+          const success = await clippie(fileMarkdown);
+          showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
+        });
+        file.previewTemplate.append(copyLinkElement);
+      });
+      this.on('removedfile', (file) => {
+        $(`#${file.uuid}`).remove();
+        if ($dropzone.data('remove-url')) {
+          POST($dropzone.data('remove-url'), {
+            data: new URLSearchParams({file: file.uuid}),
+          });
+        }
+      });
+      this.on('error', function (file, message) {
+        showErrorToast(message);
+        this.removeFile(file);
+      });
+    },
+  });
+}
+
 async function linkAction(e) {
   // A "link-action" can post AJAX request to its "data-url"
   // Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 3437565c80..10faeb135d 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -5,6 +5,7 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.js';
 import {setFileFolding} from './file-fold.js';
 import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {toAbsoluteUrl} from '../utils.js';
+import {initDropzone} from './common-global.js';
 
 const {appSubUrl, csrfToken} = window.config;
 
@@ -382,6 +383,11 @@ export async function handleReply($el) {
   const $textarea = form.find('textarea');
   let editor = getComboMarkdownEditor($textarea);
   if (!editor) {
+    // FIXME: the initialization of the dropzone is not consistent.
+    // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
+    // When the form is submitted and partially reload, none of them is initialized.
+    const dropzone = form.find('.dropzone')[0];
+    if (!dropzone.dropzone) initDropzone(dropzone);
     editor = await initComboMarkdownEditor(form.find('.combo-markdown-editor'));
   }
   editor.focus();
@@ -511,6 +517,7 @@ export function initRepoPullRequestReview() {
       td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
       td.find("input[name='path']").val(path);
 
+      initDropzone(td.find('.dropzone')[0]);
       const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor'));
       editor.focus();
     }

From f9207b09479df964872d68842469991042b5497f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 25 Feb 2024 18:45:56 +0800
Subject: [PATCH 166/679] Refactor Safe modifier (#29392)

After this PR: no need to play with the Safe/Escape tricks anymore. See
the changes for more details.
---
 .../administration/mail-templates.en-us.md    |  2 +-
 .../administration/mail-templates.zh-cn.md    | 16 ++++++------
 modules/templates/helper.go                   | 25 ++++++++++++++++---
 modules/templates/helper_test.go              |  5 ++++
 templates/admin/packages/list.tmpl            |  2 +-
 templates/admin/repo/list.tmpl                |  2 +-
 templates/admin/stacktrace.tmpl               |  2 +-
 templates/mail/issue/assigned.tmpl            |  8 +++---
 templates/mail/issue/default.tmpl             |  8 +++---
 templates/mail/notify/repo_transfer.tmpl      |  4 +--
 templates/mail/release.tmpl                   |  6 ++---
 templates/org/member/members.tmpl             |  4 +--
 templates/org/team/members.tmpl               |  2 +-
 templates/org/team/sidebar.tmpl               |  2 +-
 templates/org/team/teams.tmpl                 |  2 +-
 templates/repo/commit_page.tmpl               |  4 +--
 templates/repo/editor/cherry_pick.tmpl        |  6 ++---
 .../repo/issue/view_content/comments.tmpl     | 14 +++++------
 templates/repo/issue/view_content/pull.tmpl   |  2 +-
 templates/repo/issue/view_title.tmpl          | 18 ++++++-------
 templates/repo/migrate/migrate.tmpl           |  2 +-
 templates/repo/settings/lfs_file.tmpl         |  4 +--
 templates/repo/settings/webhook/settings.tmpl |  2 +-
 templates/repo/wiki/view.tmpl                 |  8 +++---
 templates/user/settings/applications.tmpl     |  2 +-
 templates/user/settings/organization.tmpl     |  2 +-
 26 files changed, 89 insertions(+), 65 deletions(-)

diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index 05c41a6a02..b642ff4aa7 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -266,7 +266,7 @@ the messages. Here's a list of some of them:
 | `AppDomain`      | -           | Any       | Gitea's host name                                                           |
 | `EllipsisString` | string, int | Any       | Truncates a string to the specified length; adds ellipsis as needed         |
 | `Str2html`       | string      | Body only | Sanitizes text by removing any HTML tags from it.                           |
-| `Safe`           | string      | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. |
+| `SafeHTML`       | string      | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. |
 
 These are _functions_, not metadata, so they have to be used:
 
diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md
index 4846f6f398..fd455ef3a8 100644
--- a/docs/content/administration/mail-templates.zh-cn.md
+++ b/docs/content/administration/mail-templates.zh-cn.md
@@ -242,14 +242,14 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 
 模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
 
-| 函数名            | 参数        | 可用于       | 用法                                                                              |
-| ----------------- | ----------- | ------------ | --------------------------------------------------------------------------------- |
-| `AppUrl`          | -           | 任何地方     | Gitea 的 URL                                                                     |
-| `AppName`         | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"                                               |
-| `AppDomain`       | -           | 任何地方     | Gitea 的主机名                                                                   |
-| `EllipsisString`  | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号                                        |
-| `Str2html`        | string      | 仅正文部分   | 通过删除其中的 HTML 标签对文本进行清理                                              |
-| `Safe`            | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段               |
+| 函数名              | 参数        | 可用于       | 用法                                                                              |
+|------------------| ----------- | ------------ | --------------------------------------------------------------------------------- |
+| `AppUrl`         | -           | 任何地方     | Gitea 的 URL                                                                     |
+| `AppName`        | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"                                               |
+| `AppDomain`      | -           | 任何地方     | Gitea 的主机名                                                                   |
+| `EllipsisString` | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号                                        |
+| `Str2html`       | string      | 仅正文部分   | 通过删除其中的 HTML 标签对文本进行清理                                              |
+| `SafeHTML`       | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段               |
 
 这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
 
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 691f754748..5679487498 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -9,6 +9,7 @@ import (
 	"html"
 	"html/template"
 	"net/url"
+	"slices"
 	"strings"
 	"time"
 
@@ -34,7 +35,8 @@ func NewFuncMap() template.FuncMap {
 		// html/template related functions
 		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
 		"Eval":        Eval,
-		"Safe":        Safe,
+		"SafeHTML":    SafeHTML,
+		"HTMLFormat":  HTMLFormat,
 		"Escape":      Escape,
 		"QueryEscape": url.QueryEscape,
 		"JSEscape":    JSEscapeSafe,
@@ -177,8 +179,25 @@ func NewFuncMap() template.FuncMap {
 	}
 }
 
-// Safe render raw as HTML
-func Safe(s any) template.HTML {
+func HTMLFormat(s string, rawArgs ...any) template.HTML {
+	args := slices.Clone(rawArgs)
+	for i, v := range args {
+		switch v := v.(type) {
+		case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
+			// for most basic types (including template.HTML which is safe), just do nothing and use it
+		case string:
+			args[i] = template.HTMLEscapeString(v)
+		case fmt.Stringer:
+			args[i] = template.HTMLEscapeString(v.String())
+		default:
+			args[i] = template.HTMLEscapeString(fmt.Sprint(v))
+		}
+	}
+	return template.HTML(fmt.Sprintf(s, args...))
+}
+
+// SafeHTML render raw as HTML
+func SafeHTML(s any) template.HTML {
 	switch v := s.(type) {
 	case string:
 		return template.HTML(v)
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index 739a92f34f..8f5d633d4f 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -4,6 +4,7 @@
 package templates
 
 import (
+	"html/template"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -56,3 +57,7 @@ func TestSubjectBodySeparator(t *testing.T) {
 func TestJSEscapeSafe(t *testing.T) {
 	assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`))
 }
+
+func TestHTMLFormat(t *testing.T) {
+	assert.Equal(t, template.HTML("<a>&lt; < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1))
+}
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 04f76748d0..cf860dab2a 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -88,7 +88,7 @@
 		{{ctx.Locale.Tr "packages.settings.delete"}}
 	</div>
 	<div class="content">
-		{{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|Safe) (`<span class="dataVersion"></span>`|Safe)}}
+		{{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|SafeHTML) (`<span class="dataVersion"></span>`|SafeHTML)}}
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index c7a6ec7e4e..e11247aed4 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -101,7 +101,7 @@
 	</div>
 	<div class="content">
 		<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
-		{{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|Safe)}}<br>
+		{{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|SafeHTML)}}<br>
 		{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}<br>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index aa5e810cd7..42944615c3 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -39,7 +39,7 @@
 		{{ctx.Locale.Tr "admin.monitor.process.cancel"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|Safe)}}</p>
+		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|SafeHTML)}}</p>
 		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
diff --git a/templates/mail/issue/assigned.tmpl b/templates/mail/issue/assigned.tmpl
index e80bd2fc31..5720319ee8 100644
--- a/templates/mail/issue/assigned.tmpl
+++ b/templates/mail/issue/assigned.tmpl
@@ -8,14 +8,14 @@
 	<title>{{.Subject}}</title>
 </head>
 
-{{$repo_url := printf "<a href='%s'>%s</a>" (Escape .Issue.Repo.HTMLURL) (Escape .Issue.Repo.FullName)}}
-{{$link := printf "<a href='%s'>#%d</a>" (Escape .Link) .Issue.Index}}
+{{$repo_url := HTMLFormat "<a href='%s'>%s</a>" .Issue.Repo.HTMLURL .Issue.Repo.FullName}}
+{{$link := HTMLFormat "<a href='%s'>#%d</a>" .Link .Issue.Index}}
 <body>
 	<p>
 		{{if .IsPull}}
-			{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name ($link|Safe) ($repo_url|Safe)}}
+			{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url}}
 		{{else}}
-			{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name ($link|Safe) ($repo_url|Safe)}}
+			{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url}}
 		{{end}}
 	</p>
 	<div class="footer">
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index 54ae726d71..c48797d827 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -22,13 +22,13 @@
 			{{if .Comment.IsForcePush}}
 				{{$oldCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.OldCommit}}
 				{{$oldShortSha := ShortSha .Comment.OldCommit}}
-				{{$oldCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $oldCommitUrl) (Escape $oldShortSha)}}
+				{{$oldCommitLink := HTMLFormat "<a href='%[1]s'><b>%[2]s</b></a>" $oldCommitUrl $oldShortSha}}
 
 				{{$newCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.NewCommit}}
 				{{$newShortSha := ShortSha .Comment.NewCommit}}
-				{{$newCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $newCommitUrl) (Escape $newShortSha)}}
+				{{$newCommitLink := HTMLFormat "<a href='%[1]s'><b>%[2]s</b></a>" $newCommitUrl $newShortSha}}
 
-				{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch ($oldCommitLink|Safe) ($newCommitLink|Safe)}}
+				{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink}}
 			{{else}}
 				{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits)}}
 			{{end}}
@@ -65,7 +65,7 @@
 			{{$.locale.Tr "mail.issue.in_tree_path" .TreePath}}
 			<div class="review">
 				<pre>{{.Patch}}</pre>
-				<div>{{.RenderedContent | Safe}}</div>
+				<div>{{.RenderedContent | SafeHTML}}</div>
 			</div>
 		{{end -}}
 		{{if eq .ActionName "push"}}
diff --git a/templates/mail/notify/repo_transfer.tmpl b/templates/mail/notify/repo_transfer.tmpl
index 1b23593f6b..597048ddf4 100644
--- a/templates/mail/notify/repo_transfer.tmpl
+++ b/templates/mail/notify/repo_transfer.tmpl
@@ -5,10 +5,10 @@
 	<title>{{.Subject}}</title>
 </head>
 
-{{$url := printf "<a href='%[1]s'>%[2]s</a>" (Escape .Link) (Escape .Repo)}}
+{{$url := HTMLFormat "<a href='%[1]s'>%[2]s</a>" .Link .Repo)}}
 <body>
 	<p>{{.Subject}}.
-		{{.locale.Tr "mail.repo.transfer.body" ($url|Safe)}}
+		{{.locale.Tr "mail.repo.transfer.body" $url}}
 	</p>
 	<p>
 		---
diff --git a/templates/mail/release.tmpl b/templates/mail/release.tmpl
index 96dc769993..62a16573c6 100644
--- a/templates/mail/release.tmpl
+++ b/templates/mail/release.tmpl
@@ -11,11 +11,11 @@
 
 </head>
 
-{{$release_url := printf "<a href='%s'>%s</a>" (.Release.HTMLURL | Escape) (.Release.TagName | Escape)}}
-{{$repo_url := printf "<a href='%s'>%s</a>" (.Release.Repo.HTMLURL | Escape) (.Release.Repo.FullName | Escape)}}
+{{$release_url := HTMLFormat "<a href='%s'>%s</a>" .Release.HTMLURL .Release.TagName}}
+{{$repo_url := HTMLFormat "<a href='%s'>%s</a>" .Release.Repo.HTMLURL .Release.Repo.FullName}}
 <body>
 	<p>
-		{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name ($release_url|Safe) ($repo_url|Safe)}}
+		{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url}}
 	</p>
 	<h4>{{.locale.Tr "mail.release.title" .Release.Title}}</h4>
 	<p>
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl
index 64f1aaa7d2..54f84450eb 100644
--- a/templates/org/member/members.tmpl
+++ b/templates/org/member/members.tmpl
@@ -73,7 +73,7 @@
 		{{ctx.Locale.Tr "org.members.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
+		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
@@ -82,7 +82,7 @@
 		{{ctx.Locale.Tr "org.members.remove"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|Safe) (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
+		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index dd4ece1433..adaf83ae15 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -81,7 +81,7 @@
 		{{ctx.Locale.Tr "org.members.remove"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|Safe) (`<span class="dataTeamName"></span>`|Safe)}}</p>
+		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataTeamName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl
index 440fa11dc9..9311a46e38 100644
--- a/templates/org/team/sidebar.tmpl
+++ b/templates/org/team/sidebar.tmpl
@@ -88,7 +88,7 @@
 		{{ctx.Locale.Tr "org.teams.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|Safe)}}</p>
+		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl
index b518d7d9d7..53c909ee9c 100644
--- a/templates/org/team/teams.tmpl
+++ b/templates/org/team/teams.tmpl
@@ -49,7 +49,7 @@
 		{{ctx.Locale.Tr "org.teams.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|Safe)}}</p>
+		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index fbfaa19411..115ee92955 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -88,7 +88,7 @@
 												{{.CsrfTokenHtml}}
 												<div class="field">
 													<label>
-														{{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|Safe)}}
+														{{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|SafeHTML)}}
 													</label>
 												</div>
 												<div class="required field">
@@ -113,7 +113,7 @@
 												<input type="hidden" name="create_tag" value="true">
 												<div class="field">
 													<label>
-														{{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|Safe)}}
+														{{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|SafeHTML)}}
 													</label>
 												</div>
 												<div class="required field">
diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl
index b65c3a3033..f9c9eef5aa 100644
--- a/templates/repo/editor/cherry_pick.tmpl
+++ b/templates/repo/editor/cherry_pick.tmpl
@@ -11,11 +11,11 @@
 			<div class="repo-editor-header">
 				<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
 					{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
-					{{$shalink := printf `<a class="ui primary sha label" href="%s">%s</a>` (Escape $shaurl) (ShortSha .SHA)}}
+					{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}}
 					{{if eq .CherryPickType "revert"}}
-						{{ctx.Locale.Tr "repo.editor.revert" ($shalink|Safe)}}
+						{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.editor.cherry_pick" ($shalink|Safe)}}
+						{{ctx.Locale.Tr "repo.editor.cherry_pick" $shalink}}
 					{{end}}
 					<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
 					<div class="breadcrumb-divider">:</div>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 7bd7e8c35d..e41f804043 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -112,9 +112,9 @@
 					{{template "shared/user/authorlink" .Poster}}
 					{{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}}
 					{{if eq $.Issue.PullRequest.Status 3}}
-						{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID) | Safe) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape) | Safe) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (HTMLFormat `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "<b>%[1]s</b>" $.BaseTarget) $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID) | Safe) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape) | Safe) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (HTMLFormat `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "<b>%[1]s</b>" $.BaseTarget) $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -595,19 +595,19 @@
 					{{$oldProjectDisplayHtml := "Unknown Project"}}
 					{{if .OldProject}}
 						{{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}}
-						{{$oldProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}}
+						{{$oldProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .OldProject.Title}}
 					{{end}}
 					{{$newProjectDisplayHtml := "Unknown Project"}}
 					{{if .Project}}
 						{{$trKey := printf "projects.type-%d.display_name" .Project.Type}}
-						{{$newProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}}
+						{{$newProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .Project.Title}}
 					{{end}}
 					{{if and (gt .OldProjectID 0) (gt .ProjectID 0)}}
-						{{ctx.Locale.Tr "repo.issues.change_project_at" ($oldProjectDisplayHtml|Safe) ($newProjectDisplayHtml|Safe) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr}}
 					{{else if gt .OldProjectID 0}}
-						{{ctx.Locale.Tr "repo.issues.remove_project_at" ($oldProjectDisplayHtml|Safe) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr}}
 					{{else if gt .ProjectID 0}}
-						{{ctx.Locale.Tr "repo.issues.add_project_at" ($newProjectDisplayHtml|Safe) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr}}
 					{{end}}
 				</span>
 			</div>
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 13d49b61b7..371c9db6f0 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -39,7 +39,7 @@
 								{{ctx.Locale.Tr "repo.pulls.merged_success"}}
 							</h3>
 							<div class="merge-section-info">
-								{{ctx.Locale.Tr "repo.pulls.merged_info_text" (printf "<code>%s</code>" (.HeadTarget | Escape) | Safe)}}
+								{{ctx.Locale.Tr "repo.pulls.merged_info_text" (HTMLFormat "<code>%s</code>" .HeadTarget)}}
 							</div>
 						</div>
 						<div class="item-section-right">
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 9b4657b634..37cad26c9b 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -43,31 +43,31 @@
 		{{end}}
 		<div class="gt-ml-3">
 			{{if .Issue.IsPull}}
-				{{$headHref := .HeadTarget|Escape}}
+				{{$headHref := .HeadTarget}}
 				{{if .HeadBranchLink}}
-					{{$headHref = printf `<a href="%s">%s</a>` (.HeadBranchLink | Escape) $headHref}}
+					{{$headHref = HTMLFormat `<a href="%s">%s</a>` .HeadBranchLink $headHref}}
 				{{end}}
-				{{$headHref = printf `%s <button class="btn interact-fg" data-tooltip-content="%s" data-clipboard-text="%s">%s</button>` $headHref (ctx.Locale.Tr "copy_branch") (.HeadTarget | Escape) (svg "octicon-copy" 14)}}
-				{{$baseHref := .BaseTarget|Escape}}
+				{{$headHref = HTMLFormat `%s <button class="btn interact-fg" data-tooltip-content="%s" data-clipboard-text="%s">%s</button>` $headHref (ctx.Locale.Tr "copy_branch") .HeadTarget (svg "octicon-copy" 14)}}
+				{{$baseHref := .BaseTarget}}
 				{{if .BaseBranchLink}}
-					{{$baseHref = printf `<a href="%s">%s</a>` (.BaseBranchLink | Escape) $baseHref}}
+					{{$baseHref = HTMLFormat `<a href="%s">%s</a>` .BaseBranchLink $baseHref}}
 				{{end}}
 				{{if .Issue.PullRequest.HasMerged}}
 					{{$mergedStr:= TimeSinceUnix .Issue.PullRequest.MergedUnix ctx.Locale}}
 					{{if .Issue.OriginalAuthor}}
 						{{.Issue.OriginalAuthor}}
-						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe) $mergedStr}}</span>
+						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
 					{{else}}
 						<a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a>
-						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe) $mergedStr}}</span>
+						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
 					{{end}}
 				{{else}}
 					{{if .Issue.OriginalAuthor}}
-						<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe)}}</span>
+						<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
 					{{else}}
 						<span id="pull-desc" class="pull-desc">
 							<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
-							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe)}}
+							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
 						</span>
 					{{end}}
 					<span id="pull-desc-edit" class="gt-hidden flex-text-block">
diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl
index c686f0b832..d1abb15374 100644
--- a/templates/repo/migrate/migrate.tmpl
+++ b/templates/repo/migrate/migrate.tmpl
@@ -20,7 +20,7 @@
 								{{.Title}}
 							</div>
 							<div class="description gt-text-center">
-								{{(printf "repo.migrate.%s.description" .Name) | ctx.Locale.Tr}}
+								{{ctx.Locale.Tr (printf "repo.migrate.%s.description" .Name)}}
 							</div>
 						</div>
 					</a>
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
index 0aeb2af178..7f1d07e46f 100644
--- a/templates/repo/settings/lfs_file.tmpl
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -15,9 +15,9 @@
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
 				<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
 					{{if .IsMarkup}}
-						{{if .FileContent}}{{.FileContent | Safe}}{{end}}
+						{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}
 					{{else if .IsPlainText}}
-						<pre>{{if .FileContent}}{{.FileContent | Safe}}{{end}}</pre>
+						<pre>{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}</pre>
 					{{else if not .IsTextFile}}
 						<div class="view-raw">
 							{{if .IsImageFile}}
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index f636108b37..3ef8894444 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -263,7 +263,7 @@
 	<label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label>
 	<input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}>
 	{{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}}
-		<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Safe)}}</span>
+		<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | SafeHTML)}}</span>
 	{{end}}
 </div>
 
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 5b296dc2af..f3b6be97cf 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -67,13 +67,13 @@
 		<div class="wiki-content-parts">
 			{{if .sidebarTocContent}}
 			<div class="markup wiki-content-sidebar wiki-content-toc">
-				{{.sidebarTocContent | Safe}}
+				{{.sidebarTocContent | SafeHTML}}
 			</div>
 			{{end}}
 
 			<div class="markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}">
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
-				{{.content | Safe}}
+				{{.content | SafeHTML}}
 			</div>
 
 			{{if .sidebarPresent}}
@@ -82,7 +82,7 @@
 					<a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
 				{{end}}
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
-				{{.sidebarContent | Safe}}
+				{{.sidebarContent | SafeHTML}}
 			</div>
 			{{end}}
 
@@ -94,7 +94,7 @@
 					<a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
 				{{end}}
 				{{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}}
-				{{.footerContent | Safe}}
+				{{.footerContent | SafeHTML}}
 			</div>
 			{{end}}
 		</div>
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 8cf76d80a5..7ce9a4b70f 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -75,7 +75,7 @@
 						{{ctx.Locale.Tr "settings.select_permissions"}}
 					</summary>
 					<p class="activity meta">
-						<i>{{ctx.Locale.Tr "settings.access_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`)}}</i>
+						<i>{{ctx.Locale.Tr "settings.access_token_desc" (`href="/api/swagger" target="_blank"`|SafeHTML) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i>
 					</p>
 					<div class="scoped-access-token-mount">
 						<scoped-access-token-selector
diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl
index 102ff2e95b..16c27b52cd 100644
--- a/templates/user/settings/organization.tmpl
+++ b/templates/user/settings/organization.tmpl
@@ -47,7 +47,7 @@
 		{{ctx.Locale.Tr "org.members.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
+		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>

From 0676bf52f95b9c9ac6f5679bd263d844e6a83fa1 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 25 Feb 2024 14:36:11 +0200
Subject: [PATCH 167/679] Remove jQuery AJAX from the notice selection deletion
 button (#29381)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the repo notice selection deletion button functionality and it
works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/admin/common.js | 17 +++++++----------
 1 file changed, 7 insertions(+), 10 deletions(-)

diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 044976ea7b..5354216e3d 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -1,8 +1,9 @@
 import $ from 'jquery';
 import {checkAppUrl} from '../common-global.js';
 import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
+import {POST} from '../../modules/fetch.js';
 
-const {csrfToken, appSubUrl} = window.config;
+const {appSubUrl} = window.config;
 
 export function initAdminCommon() {
   if ($('.page-content.admin').length === 0) {
@@ -204,22 +205,18 @@ export function initAdminCommon() {
           break;
       }
     });
-    $('#delete-selection').on('click', function (e) {
+    $('#delete-selection').on('click', async function (e) {
       e.preventDefault();
       const $this = $(this);
       $this.addClass('loading disabled');
-      const ids = [];
+      const data = new FormData();
       $checkboxes.each(function () {
         if ($(this).checkbox('is checked')) {
-          ids.push($(this).data('id'));
+          data.append('ids[]', $(this).data('id'));
         }
       });
-      $.post($this.data('link'), {
-        _csrf: csrfToken,
-        ids
-      }).done(() => {
-        window.location.href = $this.data('redirect');
-      });
+      await POST($this.data('link'), {data});
+      window.location.href = $this.data('redirect');
     });
   }
 }

From ad0a34b492c3d41952ff4648c8bfb7b54c376151 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sun, 25 Feb 2024 14:05:23 +0100
Subject: [PATCH 168/679] Add `io.Closer` guidelines (#29387)

Co-authored-by: Yarden Shoham <git@yardenshoham.com>
---
 docs/content/contributing/guidelines-backend.en-us.md | 4 ++++
 modules/git/blame.go                                  | 4 ++++
 modules/git/repo_base_gogit.go                        | 7 ++++---
 modules/git/repo_base_nogogit.go                      | 4 ++--
 modules/indexer/internal/bleve/indexer.go             | 2 +-
 modules/indexer/internal/meilisearch/indexer.go       | 3 ---
 modules/util/filebuffer/file_backed_buffer.go         | 1 +
 7 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/docs/content/contributing/guidelines-backend.en-us.md b/docs/content/contributing/guidelines-backend.en-us.md
index 084b3886e8..3159a5ff7d 100644
--- a/docs/content/contributing/guidelines-backend.en-us.md
+++ b/docs/content/contributing/guidelines-backend.en-us.md
@@ -101,6 +101,10 @@ i.e. `services/user`, `models/repository`.
 Since there are some packages which use the same package name, it is possible that you find packages like `modules/user`, `models/user`, and `services/user`. When these packages are imported in one Go file, it's difficult to know which package we are using and if it's a variable name or an import name. So, we always recommend to use import aliases. To differ from package variables which are commonly in camelCase, just use **snake_case** for import aliases.
 i.e. `import user_service "code.gitea.io/gitea/services/user"`
 
+### Implementing `io.Closer`
+
+If a type implements `io.Closer`, calling `Close` multiple times must not fail or `panic` but return an error or `nil`.
+
 ### Important Gotchas
 
 - Never write `x.Update(exemplar)` without an explicit `WHERE` clause:
diff --git a/modules/git/blame.go b/modules/git/blame.go
index 64095a218a..69e1b08f93 100644
--- a/modules/git/blame.go
+++ b/modules/git/blame.go
@@ -115,6 +115,10 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
 
 // Close BlameReader - don't run NextPart after invoking that
 func (r *BlameReader) Close() error {
+	if r.bufferedReader == nil {
+		return nil
+	}
+
 	err := <-r.done
 	r.bufferedReader = nil
 	_ = r.reader.Close()
diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go
index 9270bb70f0..3ca5eb36c6 100644
--- a/modules/git/repo_base_gogit.go
+++ b/modules/git/repo_base_gogit.go
@@ -88,16 +88,17 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 }
 
 // Close this repository, in particular close the underlying gogitStorage if this is not nil
-func (repo *Repository) Close() (err error) {
+func (repo *Repository) Close() error {
 	if repo == nil || repo.gogitStorage == nil {
-		return
+		return nil
 	}
 	if err := repo.gogitStorage.Close(); err != nil {
 		gitealog.Error("Error closing storage: %v", err)
 	}
+	repo.gogitStorage = nil
 	repo.LastCommitCache = nil
 	repo.tagCache = nil
-	return
+	return nil
 }
 
 // GoGitRepo gets the go-git repo representation
diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
index 8c6eae5897..86b6a93567 100644
--- a/modules/git/repo_base_nogogit.go
+++ b/modules/git/repo_base_nogogit.go
@@ -103,7 +103,7 @@ func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError
 	}
 }
 
-func (repo *Repository) Close() (err error) {
+func (repo *Repository) Close() error {
 	if repo == nil {
 		return nil
 	}
@@ -123,5 +123,5 @@ func (repo *Repository) Close() (err error) {
 	}
 	repo.LastCommitCache = nil
 	repo.tagCache = nil
-	return err
+	return nil
 }
diff --git a/modules/indexer/internal/bleve/indexer.go b/modules/indexer/internal/bleve/indexer.go
index ce06b5afcb..01e53ca636 100644
--- a/modules/indexer/internal/bleve/indexer.go
+++ b/modules/indexer/internal/bleve/indexer.go
@@ -92,7 +92,7 @@ func (i *Indexer) Ping(_ context.Context) error {
 }
 
 func (i *Indexer) Close() {
-	if i == nil {
+	if i == nil || i.Indexer == nil {
 		return
 	}
 
diff --git a/modules/indexer/internal/meilisearch/indexer.go b/modules/indexer/internal/meilisearch/indexer.go
index b037249d43..f4004849c1 100644
--- a/modules/indexer/internal/meilisearch/indexer.go
+++ b/modules/indexer/internal/meilisearch/indexer.go
@@ -87,8 +87,5 @@ func (i *Indexer) Close() {
 	if i == nil {
 		return
 	}
-	if i.Client == nil {
-		return
-	}
 	i.Client = nil
 }
diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go
index 6b07bd0413..739543e297 100644
--- a/modules/util/filebuffer/file_backed_buffer.go
+++ b/modules/util/filebuffer/file_backed_buffer.go
@@ -149,6 +149,7 @@ func (b *FileBackedBuffer) Close() error {
 	if b.file != nil {
 		err := b.file.Close()
 		os.Remove(b.file.Name())
+		b.file = nil
 		return err
 	}
 	return nil

From f79c9e817abaef279c0b33d5460a066170dd3ea6 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sun, 25 Feb 2024 14:32:13 +0100
Subject: [PATCH 169/679] Use `crypto/sha256` (#29386)

Go 1.21 improved the performance of `crypto/sha256`. It's now similar to
`minio/sha256-simd`, so we should just use the standard libs.

https://go.dev/doc/go1.21#crypto/sha256
https://go-review.googlesource.com/c/go/+/408795
https://github.com/multiformats/go-multihash/pull/173
---
 go.mod                                           | 2 +-
 models/auth/twofactor.go                         | 2 +-
 models/migrations/base/hash.go                   | 2 +-
 models/migrations/v1_14/v166.go                  | 2 +-
 modules/auth/password/hash/pbkdf2.go             | 2 +-
 modules/avatar/hash.go                           | 3 +--
 modules/avatar/identicon/identicon.go            | 3 +--
 modules/base/tool.go                             | 2 +-
 modules/git/last_commit_cache.go                 | 3 +--
 modules/lfs/content_store.go                     | 3 +--
 modules/lfs/pointer.go                           | 3 +--
 modules/secret/secret.go                         | 3 +--
 modules/util/keypair.go                          | 3 +--
 modules/util/keypair_test.go                     | 2 +-
 routers/api/packages/chef/auth.go                | 3 +--
 routers/api/packages/maven/maven.go              | 3 +--
 services/lfs/server.go                           | 2 +-
 services/mailer/token/token.go                   | 3 +--
 services/webhook/deliver.go                      | 2 +-
 tests/integration/api_packages_chef_test.go      | 2 +-
 tests/integration/api_packages_container_test.go | 2 +-
 tests/integration/api_packages_test.go           | 2 +-
 22 files changed, 22 insertions(+), 32 deletions(-)

diff --git a/go.mod b/go.mod
index 7a752ec874..03f6ad1215 100644
--- a/go.mod
+++ b/go.mod
@@ -78,7 +78,6 @@ require (
 	github.com/mholt/archiver/v3 v3.5.1
 	github.com/microcosm-cc/bluemonday v1.0.26
 	github.com/minio/minio-go/v7 v7.0.66
-	github.com/minio/sha256-simd v1.0.1
 	github.com/msteinert/pam v1.2.0
 	github.com/nektos/act v0.2.52
 	github.com/niklasfasching/go-org v1.7.0
@@ -230,6 +229,7 @@ require (
 	github.com/mholt/acmez v1.2.0 // indirect
 	github.com/miekg/dns v1.1.58 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/minio/sha256-simd v1.0.1 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go
index 51061e5205..d0c341a192 100644
--- a/models/auth/twofactor.go
+++ b/models/auth/twofactor.go
@@ -6,6 +6,7 @@ package auth
 import (
 	"context"
 	"crypto/md5"
+	"crypto/sha256"
 	"crypto/subtle"
 	"encoding/base32"
 	"encoding/base64"
@@ -18,7 +19,6 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/minio/sha256-simd"
 	"github.com/pquerna/otp/totp"
 	"golang.org/x/crypto/pbkdf2"
 )
diff --git a/models/migrations/base/hash.go b/models/migrations/base/hash.go
index 0debec272b..00fd1efd4a 100644
--- a/models/migrations/base/hash.go
+++ b/models/migrations/base/hash.go
@@ -4,9 +4,9 @@
 package base
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/pbkdf2"
 )
 
diff --git a/models/migrations/v1_14/v166.go b/models/migrations/v1_14/v166.go
index 78f33e8f9b..e5731582fd 100644
--- a/models/migrations/v1_14/v166.go
+++ b/models/migrations/v1_14/v166.go
@@ -4,9 +4,9 @@
 package v1_14 //nolint
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/argon2"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/pbkdf2"
diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go
index 9ff6d162fc..27382fedb8 100644
--- a/modules/auth/password/hash/pbkdf2.go
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -4,12 +4,12 @@
 package hash
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/pbkdf2"
 )
 
diff --git a/modules/avatar/hash.go b/modules/avatar/hash.go
index 4fc28a7739..50db9c1943 100644
--- a/modules/avatar/hash.go
+++ b/modules/avatar/hash.go
@@ -4,10 +4,9 @@
 package avatar
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"strconv"
-
-	"github.com/minio/sha256-simd"
 )
 
 // HashAvatar will generate a unique string, which ensures that when there's a
diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go
index 9b7a2faf05..63926d5f19 100644
--- a/modules/avatar/identicon/identicon.go
+++ b/modules/avatar/identicon/identicon.go
@@ -7,11 +7,10 @@
 package identicon
 
 import (
+	"crypto/sha256"
 	"fmt"
 	"image"
 	"image/color"
-
-	"github.com/minio/sha256-simd"
 )
 
 const minImageSize = 16
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 19fb2c451f..168a2220b2 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -5,6 +5,7 @@ package base
 
 import (
 	"crypto/sha1"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
@@ -22,7 +23,6 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/dustin/go-humanize"
-	"github.com/minio/sha256-simd"
 )
 
 // EncodeSha1 string to sha1 hex value.
diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go
index 7c7baedd2f..5b62b90b27 100644
--- a/modules/git/last_commit_cache.go
+++ b/modules/git/last_commit_cache.go
@@ -4,12 +4,11 @@
 package git
 
 import (
+	"crypto/sha256"
 	"fmt"
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-
-	"github.com/minio/sha256-simd"
 )
 
 // Cache represents a caching interface
diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go
index daf8c6cfdd..0d9c0c98ac 100644
--- a/modules/lfs/content_store.go
+++ b/modules/lfs/content_store.go
@@ -4,6 +4,7 @@
 package lfs
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"hash"
@@ -12,8 +13,6 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
-
-	"github.com/minio/sha256-simd"
 )
 
 var (
diff --git a/modules/lfs/pointer.go b/modules/lfs/pointer.go
index 3e5bb8f91d..ebde20f826 100644
--- a/modules/lfs/pointer.go
+++ b/modules/lfs/pointer.go
@@ -4,6 +4,7 @@
 package lfs
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"fmt"
@@ -12,8 +13,6 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/modules/secret/secret.go b/modules/secret/secret.go
index 9c2ecd181d..e70ae1839c 100644
--- a/modules/secret/secret.go
+++ b/modules/secret/secret.go
@@ -7,13 +7,12 @@ import (
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/rand"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
 	"fmt"
 	"io"
-
-	"github.com/minio/sha256-simd"
 )
 
 // AesEncrypt encrypts text and given key with AES.
diff --git a/modules/util/keypair.go b/modules/util/keypair.go
index 97f2d9ebca..8b86c142af 100644
--- a/modules/util/keypair.go
+++ b/modules/util/keypair.go
@@ -7,10 +7,9 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/pem"
-
-	"github.com/minio/sha256-simd"
 )
 
 // GenerateKeyPair generates a public and private keypair
diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go
index c9925f7988..c6f68c845a 100644
--- a/modules/util/keypair_test.go
+++ b/modules/util/keypair_test.go
@@ -7,12 +7,12 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/pem"
 	"regexp"
 	"testing"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go
index 3aef8281a4..a790e9a363 100644
--- a/routers/api/packages/chef/auth.go
+++ b/routers/api/packages/chef/auth.go
@@ -8,6 +8,7 @@ import (
 	"crypto"
 	"crypto/rsa"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/pem"
@@ -26,8 +27,6 @@ import (
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/auth"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 0b93382b01..5106395eb1 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -6,6 +6,7 @@ package maven
 import (
 	"crypto/md5"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/sha512"
 	"encoding/hex"
 	"encoding/xml"
@@ -26,8 +27,6 @@ import (
 	maven_module "code.gitea.io/gitea/modules/packages/maven"
 	"code.gitea.io/gitea/routers/api/packages/helper"
 	packages_service "code.gitea.io/gitea/services/packages"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 62134b7d60..56714120ad 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -5,6 +5,7 @@ package lfs
 
 import (
 	stdCtx "context"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
@@ -33,7 +34,6 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 
 	"github.com/golang-jwt/jwt/v5"
-	"github.com/minio/sha256-simd"
 )
 
 // requestContext contain variables from the HTTP request.
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go
index aa7b567188..8a5a762d6b 100644
--- a/services/mailer/token/token.go
+++ b/services/mailer/token/token.go
@@ -6,14 +6,13 @@ package token
 import (
 	"context"
 	crypto_hmac "crypto/hmac"
+	"crypto/sha256"
 	"encoding/base32"
 	"fmt"
 	"time"
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/util"
-
-	"github.com/minio/sha256-simd"
 )
 
 // A token is a verifiable container describing an action.
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 8f728d3aa6..935981d29c 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"crypto/hmac"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/tls"
 	"encoding/hex"
 	"fmt"
@@ -29,7 +30,6 @@ import (
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/gobwas/glob"
-	"github.com/minio/sha256-simd"
 )
 
 // Deliver deliver hook task
diff --git a/tests/integration/api_packages_chef_test.go b/tests/integration/api_packages_chef_test.go
index 4123c7216c..05545f11a6 100644
--- a/tests/integration/api_packages_chef_test.go
+++ b/tests/integration/api_packages_chef_test.go
@@ -11,6 +11,7 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/pem"
@@ -33,7 +34,6 @@ import (
 	chef_router "code.gitea.io/gitea/routers/api/packages/chef"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go
index 509ad424e6..9ac6e5256b 100644
--- a/tests/integration/api_packages_container_test.go
+++ b/tests/integration/api_packages_container_test.go
@@ -5,6 +5,7 @@ package integration
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"encoding/base64"
 	"fmt"
 	"net/http"
@@ -24,7 +25,6 @@ import (
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	oci "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go
index 8c981566b6..daf32e82f9 100644
--- a/tests/integration/api_packages_test.go
+++ b/tests/integration/api_packages_test.go
@@ -5,6 +5,7 @@ package integration
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"fmt"
 	"net/http"
 	"strings"
@@ -24,7 +25,6 @@ import (
 	packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 

From ea164aba4b697aa08e4d20d896a8f318c09a6523 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 25 Feb 2024 21:37:35 +0800
Subject: [PATCH 170/679] Make actions animation rotate counterclockwisely
 (#29378)

Because the icon is:

![image](https://github.com/go-gitea/gitea/assets/2114189/be7e78ab-bc64-46d9-8259-fd7f0037471a)

So it must rotate counterclockwisely
---
 web_src/js/components/RepoActionView.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index c4a7389bc5..3801848519 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -760,7 +760,7 @@ export function initRepositoryActionView() {
 
 @keyframes job-status-rotate-keyframes {
   100% {
-    transform: rotate(360deg);
+    transform: rotate(-360deg);
   }
 }
 

From d2f6588b66549b33adf8bac7044d03c89d668470 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 25 Feb 2024 22:02:20 +0800
Subject: [PATCH 171/679] Remove incorrect and unnecessary Escape from
 templates (#29394)

Follow #29165

* some of them are incorrect, which would lead to double escaping (eg:
`(print (Escape $.RepoLink)`)
* other of them are not necessary, because `Tr` handles strings&HTML
automatically

Suggest to review by "unified view":
https://github.com/go-gitea/gitea/pull/29394/files?diff=unified&w=0
---
 modules/templates/helper.go                   |  4 +-
 templates/code/searchcombo.tmpl               |  2 +-
 templates/explore/repo_search.tmpl            |  2 +-
 templates/mail/auth/register_notify.tmpl      |  2 +-
 templates/mail/issue/default.tmpl             | 18 +++----
 templates/package/shared/list.tmpl            |  4 +-
 templates/package/shared/versionlist.tmpl     |  2 +-
 templates/package/view.tmpl                   |  4 +-
 .../code/recently_pushed_new_branches.tmpl    |  2 +-
 templates/repo/create_helper.tmpl             |  2 +-
 templates/repo/diff/comments.tmpl             |  6 +--
 templates/repo/diff/compare.tmpl              |  8 ++--
 templates/repo/editor/commit_form.tmpl        |  2 +-
 templates/repo/issue/card.tmpl                |  6 +--
 templates/repo/issue/new_form.tmpl            |  2 +-
 templates/repo/issue/view_content.tmpl        |  8 ++--
 .../repo/issue/view_content/comments.tmpl     | 38 +++++++--------
 .../repo/issue/view_content/conversation.tmpl |  2 +-
 templates/repo/issue/view_content/pull.tmpl   |  4 +-
 .../repo/issue/view_content/sidebar.tmpl      |  8 ++--
 templates/repo/issue/view_title.tmpl          |  6 +--
 templates/repo/search.tmpl                    |  2 +-
 templates/repo/settings/protected_branch.tmpl |  2 +-
 templates/repo/wiki/view.tmpl                 |  2 +-
 templates/shared/issuelist.tmpl               |  6 +--
 templates/user/auth/activate.tmpl             |  6 +--
 templates/user/auth/forgot_passwd.tmpl        |  2 +-
 templates/user/dashboard/feeds.tmpl           | 48 +++++++++----------
 28 files changed, 100 insertions(+), 100 deletions(-)

diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 5679487498..0f39767586 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -37,7 +37,7 @@ func NewFuncMap() template.FuncMap {
 		"Eval":        Eval,
 		"SafeHTML":    SafeHTML,
 		"HTMLFormat":  HTMLFormat,
-		"Escape":      Escape,
+		"HTMLEscape":  HTMLEscape,
 		"QueryEscape": url.QueryEscape,
 		"JSEscape":    JSEscapeSafe,
 		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
@@ -218,7 +218,7 @@ func Str2html(s any) template.HTML {
 	panic(fmt.Sprintf("unexpected type %T", s))
 }
 
-func Escape(s any) template.HTML {
+func HTMLEscape(s any) template.HTML {
 	switch v := s.(type) {
 	case string:
 		return template.HTML(html.EscapeString(v))
diff --git a/templates/code/searchcombo.tmpl b/templates/code/searchcombo.tmpl
index d256890918..d451bc0ad8 100644
--- a/templates/code/searchcombo.tmpl
+++ b/templates/code/searchcombo.tmpl
@@ -7,7 +7,7 @@
 		</div>
 	{{else if .SearchResults}}
 		<h3>
-			{{ctx.Locale.Tr "explore.code_search_results" (.Keyword|Escape)}}
+			{{ctx.Locale.Tr "explore.code_search_results" .Keyword}}
 		</h3>
 		{{template "code/searchresults" .}}
 	{{else if .Keyword}}
diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl
index 7ae4a4ed6f..e268670e93 100644
--- a/templates/explore/repo_search.tmpl
+++ b/templates/explore/repo_search.tmpl
@@ -36,7 +36,7 @@
 </div>
 {{if and .PageIsExploreRepositories .OnlyShowRelevant}}
 	<div class="ui message explore-relevancy-note">
-		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" ((printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))|Escape)}}</span>
+		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}</span>
 	</div>
 {{end}}
 <div class="divider"></div>
diff --git a/templates/mail/auth/register_notify.tmpl b/templates/mail/auth/register_notify.tmpl
index ec3e09dd5f..62dbf7d927 100644
--- a/templates/mail/auth/register_notify.tmpl
+++ b/templates/mail/auth/register_notify.tmpl
@@ -11,7 +11,7 @@
 	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
 	<p>{{.locale.Tr "mail.register_notify.text_1" AppName}}</p><br>
 	<p>{{.locale.Tr "mail.register_notify.text_2" .Username}}</p><p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p><br>
-	<p>{{.locale.Tr "mail.register_notify.text_3" ($set_pwd_url | Escape)}}</p><br>
+	<p>{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}</p><br>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
 </body>
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index c48797d827..79dbe897cc 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -36,26 +36,26 @@
 	{{end}}
 	<p>
 		{{if eq .ActionName "close"}}
-			{{.locale.Tr "mail.issue.action.close" (Escape .Doer.Name) .Issue.Index}}
+			{{.locale.Tr "mail.issue.action.close" .Doer.Name .Issue.Index}}
 		{{else if eq .ActionName "reopen"}}
-			{{.locale.Tr "mail.issue.action.reopen" (Escape .Doer.Name) .Issue.Index}}
+			{{.locale.Tr "mail.issue.action.reopen" .Doer.Name .Issue.Index}}
 		{{else if eq .ActionName "merge"}}
-			{{.locale.Tr "mail.issue.action.merge" (Escape .Doer.Name) .Issue.Index (Escape .Issue.PullRequest.BaseBranch)}}
+			{{.locale.Tr "mail.issue.action.merge" .Doer.Name .Issue.Index .Issue.PullRequest.BaseBranch}}
 		{{else if eq .ActionName "approve"}}
-			{{.locale.Tr "mail.issue.action.approve" (Escape .Doer.Name)}}
+			{{.locale.Tr "mail.issue.action.approve" .Doer.Name}}
 		{{else if eq .ActionName "reject"}}
-			{{.locale.Tr "mail.issue.action.reject" (Escape .Doer.Name)}}
+			{{.locale.Tr "mail.issue.action.reject" .Doer.Name}}
 		{{else if eq .ActionName "review"}}
-			{{.locale.Tr "mail.issue.action.review" (Escape .Doer.Name)}}
+			{{.locale.Tr "mail.issue.action.review" .Doer.Name}}
 		{{else if eq .ActionName "review_dismissed"}}
-			{{.locale.Tr "mail.issue.action.review_dismissed" (Escape .Doer.Name) (Escape .Comment.Review.Reviewer.Name)}}
+			{{.locale.Tr "mail.issue.action.review_dismissed" .Doer.Name .Comment.Review.Reviewer.Name}}
 		{{else if eq .ActionName "ready_for_review"}}
-			{{.locale.Tr "mail.issue.action.ready_for_review" (Escape .Doer.Name)}}
+			{{.locale.Tr "mail.issue.action.ready_for_review" .Doer.Name}}
 		{{end}}
 
 		{{- if eq .Body ""}}
 			{{if eq .ActionName "new"}}
-				{{.locale.Tr "mail.issue.action.new" (Escape .Doer.Name) .Issue.Index}}
+				{{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}}
 			{{end}}
 		{{else}}
 			{{.Body | Str2html}}
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index 8c8b113c97..51e080f495 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -30,9 +30,9 @@
 						{{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}}
 					{{end}}
 					{{if $hasRepositoryAccess}}
-						{{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.Link (.Repository.FullName | Escape)}}
+						{{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink .Creator.GetDisplayName .Repository.Link .Repository.FullName}}
 					{{else}}
-						{{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape)}}
+						{{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}}
 					{{end}}
 				</div>
 			</div>
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl
index 4b22dc22b2..eee952c096 100644
--- a/templates/package/shared/versionlist.tmpl
+++ b/templates/package/shared/versionlist.tmpl
@@ -25,7 +25,7 @@
 			<div class="flex-item-main">
 				<a class="flex-item-title" href="{{.FullWebLink}}">{{.Version.LowerVersion}}</a>
 				<div class="flex-item-body">
-					{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape)}}
+					{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink .Creator.GetDisplayName}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 65502a6e4d..0fa23d67fd 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -10,9 +10,9 @@
 			<div>
 				{{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}
 				{{if .HasRepositoryAccess}}
-					{{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.Link (.PackageDescriptor.Repository.FullName | Escape)}}
+					{{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName .PackageDescriptor.Repository.Link .PackageDescriptor.Repository.FullName}}
 				{{else}}
-					{{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape)}}
+					{{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName}}
 				{{end}}
 			</div>
 		</div>
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index 73c9c45178..fedba06fad 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -2,7 +2,7 @@
 	<div class="ui positive message gt-df gt-ac">
 		<div class="gt-f1">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
-			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince}}
+			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" .Name $timeSince}}
 		</div>
 		<a role="button" class="ui compact positive button gt-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
 			{{ctx.Locale.Tr "repo.pulls.compare_changes"}}
diff --git a/templates/repo/create_helper.tmpl b/templates/repo/create_helper.tmpl
index 6ca691592c..70c28b72e8 100644
--- a/templates/repo/create_helper.tmpl
+++ b/templates/repo/create_helper.tmpl
@@ -1,3 +1,3 @@
 {{if not $.DisableMigrations}}
-	<p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" ((print AppSubUrl "/repo/migrate")|Escape)}}</p>
+	<p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" (print AppSubUrl "/repo/migrate")}}</p>
 {{end}}
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index b795074e49..e567417fa6 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -16,17 +16,17 @@
 						{{.OriginalAuthor}}
 					</span>
 					<span class="text grey muted-links">
-						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}}
 					</span>
 					<span class="text migrate">
 						{{if $.root.Repository.OriginalURL}}
-							({{ctx.Locale.Tr "repo.migrated_from" ($.root.Repository.OriginalURL | Escape) ($.root.Repository.GetOriginalURLHostname | Escape)}})
+							({{ctx.Locale.Tr "repo.migrated_from" $.root.Repository.OriginalURL $.root.Repository.GetOriginalURLHostname}})
 						{{end}}
 					</span>
 				{{else}}
 					<span class="text grey muted-links">
 						{{template "shared/user/namelink" .Poster}}
-						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}}
 					</span>
 				{{end}}
 			</div>
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index 7a618ba8e6..819bd8a2f0 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -194,7 +194,7 @@
 		{{if .HasPullRequest}}
 			<div class="ui segment grid title">
 				<div class="twelve wide column issue-title">
-					{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index}}
+					{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print $.RepoLink "/pulls/" .PullRequest.Issue.Index) $.RepoRelPath .PullRequest.Index}}
 					<h1>
 						<span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}}</span>
 						<span class="index">#{{.PullRequest.Issue.Index}}</span>
@@ -202,11 +202,11 @@
 				</div>
 				<div class="four wide column middle aligned text right">
 				{{- if .PullRequest.HasMerged -}}
-				<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
+				<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
 				{{else if .Issue.IsClosed}}
-				<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
+				<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
 				{{else}}
-				<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button primary show-form">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
+				<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button primary show-form">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
 				{{end}}
 				</div>
 			</div>
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index c8f062b5c5..f0f0f47826 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -26,7 +26,7 @@
 					<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
 					<label>
 						{{svg "octicon-git-commit"}}
-						{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" (.BranchName|Escape)}}
+						{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
 						{{if not .CanCommitToBranch.CanCommitToBranch}}
 						<div class="ui visible small warning message">
 							{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 7b71bd724e..7fb3d82827 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -23,11 +23,11 @@
 				{{if not $.Page.Repository}}{{.Repo.FullName}}{{end}}#{{.Index}}
 				{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
 				{{if .OriginalAuthor}}
-					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape)}}
+					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}}
 				{{else if gt .Poster.ID 0}}
-					{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape)}}
+					{{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}}
 				{{else}}
-					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape)}}
+					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}}
 				{{end}}
 			</span>
 		</div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index d1cbba6873..8e4310f0bb 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -13,7 +13,7 @@
 					<div class="field">
 						<input name="title" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" autofocus required maxlength="255" autocomplete="off">
 						{{if .PageIsComparePull}}
-							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape)}}</div>
+							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}</div>
 						{{end}}
 					</div>
 					{{if .Fields}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 906f880140..b5441872a3 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -28,10 +28,10 @@
 									{{.Issue.OriginalAuthor}}
 								</span>
 								<span class="text grey muted-links">
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}}
 								</span>
 								<span class="text migrate">
-									{{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" (.Repository.OriginalURL|Escape) (.Repository.GetOriginalURLHostname|Escape)}}){{end}}
+									{{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" .Repository.OriginalURL .Repository.GetOriginalURLHostname}}){{end}}
 								</span>
 							{{else}}
 								<a class="inline-timeline-avatar" href="{{.Issue.Poster.HomeLink}}">
@@ -39,7 +39,7 @@
 								</a>
 								<span class="text grey muted-links">
 									{{template "shared/user/authorlink" .Issue.Poster}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}}
 								</span>
 							{{end}}
 						</div>
@@ -133,7 +133,7 @@
 					</div>
 				{{else}}
 					<div class="ui warning message">
-						{{ctx.Locale.Tr "repo.issues.sign_in_require_desc" (.SignInLink|Escape)}}
+						{{ctx.Locale.Tr "repo.issues.sign_in_require_desc" .SignInLink}}
 					</div>
 				{{end}}
 			{{end}}{{/* end if: .IsSigned */}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index e41f804043..b25a5ad1b4 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -33,10 +33,10 @@
 									{{.OriginalAuthor}}
 								</span>
 								<span class="text grey muted-links">
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}} {{if $.Repository.OriginalURL}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} {{if $.Repository.OriginalURL}}
 								</span>
 								<span class="text migrate">
-									({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape)}}){{end}}
+									({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}}
 								</span>
 							{{else}}
 								{{if gt .Poster.ID 0}}
@@ -46,7 +46,7 @@
 								{{end}}
 								<span class="text grey muted-links">
 									{{template "shared/user/authorlink" .Poster}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}}
 								</span>
 							{{end}}
 						</div>
@@ -121,7 +121,7 @@
 		{{else if eq .Type 3 5 6}}
 			{{$refFrom:= ""}}
 			{{if ne .RefRepoID .Issue.RepoID}}
-				{{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}}
+				{{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" .RefRepo.FullName}}
 			{{end}}
 			{{$refTr := "repo.issues.ref_issue_from"}}
 			{{if .Issue.IsPull}}
@@ -138,7 +138,7 @@
 				{{if eq .RefAction 3}}<del>{{end}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom}}
+					{{ctx.Locale.Tr $refTr .EventTag $createdStr (.RefCommentLink ctx) $refFrom}}
 				</span>
 				{{if eq .RefAction 3}}</del>{{end}}
 
@@ -182,7 +182,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr}}{{end}}
+					{{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" .OldMilestone.Name .Milestone.Name $createdStr}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" .OldMilestone.Name $createdStr}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" .Milestone.Name $createdStr}}{{end}}
 				</span>
 			</div>
 		{{else if and (eq .Type 9) (gt .AssigneeID 0)}}
@@ -195,7 +195,7 @@
 						{{if eq .Poster.ID .Assignee.ID}}
 							{{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr}}
+							{{ctx.Locale.Tr "repo.issues.remove_assignee_at" .Poster.GetDisplayName $createdStr}}
 						{{end}}
 					</span>
 				{{else}}
@@ -205,7 +205,7 @@
 						{{if eq .Poster.ID .AssigneeID}}
 							{{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr}}
+							{{ctx.Locale.Tr "repo.issues.add_assignee_at" .Poster.GetDisplayName $createdStr}}
 						{{end}}
 					</span>
 				{{end}}
@@ -225,7 +225,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr}}
+					{{ctx.Locale.Tr "repo.issues.delete_branch_at" .OldRef $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 12}}
@@ -418,7 +418,7 @@
 											{{.OriginalAuthor}}
 										</span>
 										<span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span>
-										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape)}}){{end}}</span>
+										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}}</span>
 									{{else}}
 										{{template "shared/user/authorlink" .Poster}}
 									{{end}}
@@ -509,7 +509,7 @@
 					{{else}}
 						{{template "shared/user/authorlink" .Poster}}
 					{{end}}
-					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr}}
+					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" .OldRef .NewRef $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 26}}
@@ -542,10 +542,10 @@
 							{{if eq .PosterID .AssigneeID}}
 								{{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr}}
 							{{else}}
-								{{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr}}
+								{{ctx.Locale.Tr "repo.issues.review.remove_review_request" .Assignee.GetDisplayName $createdStr}}
 							{{end}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr}}
+							{{ctx.Locale.Tr "repo.issues.review.add_review_request" .Assignee.GetDisplayName $createdStr}}
 						{{end}}
 					{{else}}
 						<!-- If the assigned team is deleted, just displaying "Ghost Team" in the comment -->
@@ -554,9 +554,9 @@
 							{{$teamName = .AssigneeTeam.Name}}
 						{{end}}
 						{{if .RemovedAssignee}}
-							{{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr}}
+							{{ctx.Locale.Tr "repo.issues.review.remove_review_request" $teamName $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr}}
+							{{ctx.Locale.Tr "repo.issues.review.add_review_request" $teamName $createdStr}}
 						{{end}}
 					{{end}}
 				</span>
@@ -571,7 +571,7 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if .IsForcePush}}
-						{{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.force_push_codes" $.Issue.PullRequest.HeadBranch (ShortSha .OldCommit) ($.Issue.Repo.CommitLink .OldCommit) (ShortSha .NewCommit) ($.Issue.Repo.CommitLink .NewCommit) $createdStr}}
 					{{else}}
 						{{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr}}
 					{{end}}
@@ -663,11 +663,11 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if and .OldRef .NewRef}}
-						{{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.change_ref_at" .OldRef .NewRef $createdStr}}
 					{{else if .OldRef}}
-						{{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.remove_ref_at" .OldRef $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr}}
+						{{ctx.Locale.Tr "repo.issues.add_ref_at" .NewRef $createdStr}}
 					{{end}}
 				</span>
 			</div>
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 56f1af19b2..1bad0e9b55 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -73,7 +73,7 @@
 									{{else}}
 										{{template "shared/user/authorlink" .Poster}}
 									{{end}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdSubStr}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdSubStr}}
 								</span>
 							</div>
 							<div class="comment-header-right actions gt-df gt-ac">
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 371c9db6f0..c8e8038438 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -81,14 +81,14 @@
 					{{ctx.Locale.Tr "repo.pulls.data_broken"}}
 				</div>
 			{{else if .IsPullWorkInProgress}}
-				<div class="item toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{(.WorkInProgressPrefix|Escape)}}" data-update-url="{{.Issue.Link}}/title">
+				<div class="item toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title">
 					<div class="item-section-left flex-text-inline gt-f1">
 						{{svg "octicon-x"}}
 						{{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}}
 					</div>
 					{{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}}
 						<button class="ui compact button">
-							{{ctx.Locale.Tr "repo.pulls.remove_prefix" (.WorkInProgressPrefix|Escape)}}
+							{{ctx.Locale.Tr "repo.pulls.remove_prefix" .WorkInProgressPrefix}}
 						</button>
 					{{end}}
 				</div>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index bb45b07421..f5b6751d6d 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -101,7 +101,7 @@
 				{{range .OriginalReviews}}
 					<div class="item gt-df gt-ac gt-py-3">
 						<div class="gt-df gt-ac gt-f1">
-							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" ($.Repository.GetOriginalURLHostname|Escape)}}">
+							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
 								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "gt-mr-3"}}
 								{{.OriginalAuthor}}
 							</a>
@@ -114,9 +114,9 @@
 			</div>
 		</div>
 		{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
-			<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{(index .PullRequestWorkInProgressPrefixes 0| Escape)}}" data-update-url="{{.Issue.Link}}/title">
+			<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
 				<a class="muted">
-					{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0| Escape)}}
+					{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
 				</a>
 			</div>
 		{{end}}
@@ -300,7 +300,7 @@
 					{{else}}
 						{{if .HasUserStopwatch}}
 							<div class="ui warning message">
-								{{ctx.Locale.Tr "repo.issues.tracking_already_started" (.OtherStopwatchURL|Escape)}}
+								{{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}
 							</div>
 						{{end}}
 						<button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'>
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 37cad26c9b..e98e27924d 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -104,11 +104,11 @@
 				{{$createdStr:= TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
 				<span class="time-desc">
 					{{if .Issue.OriginalAuthor}}
-						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.OriginalAuthor|Escape)}}
+						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.OriginalAuthor}}
 					{{else if gt .Issue.Poster.ID 0}}
-						{{ctx.Locale.Tr "repo.issues.opened_by" $createdStr (.Issue.Poster.HomeLink|Escape) (.Issue.Poster.GetDisplayName|Escape)}}
+						{{ctx.Locale.Tr "repo.issues.opened_by" $createdStr .Issue.Poster.HomeLink .Issue.Poster.GetDisplayName}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.Poster.GetDisplayName|Escape)}}
+						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.Poster.GetDisplayName}}
 					{{end}}
 					·
 					{{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}}
diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl
index 495620300f..7b3ad7282e 100644
--- a/templates/repo/search.tmpl
+++ b/templates/repo/search.tmpl
@@ -24,7 +24,7 @@
 			</div>
 		{{else if .Keyword}}
 			<h3>
-				{{ctx.Locale.Tr "repo.search.results" (.Keyword|Escape) (.RepoLink|Escape) (.RepoName|Escape)}}
+				{{ctx.Locale.Tr "repo.search.results" .Keyword .RepoLink .RepoName}}
 			</h3>
 			{{if .SearchResults}}
 				<div class="flex-text-block gt-fw">
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index e57509abc0..6bbcc9f6ec 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -2,7 +2,7 @@
 	<div class="repo-setting-content">
 		<form class="ui form" action="{{.Link}}" method="post">
 			<h4 class="ui top attached header">
-				{{ctx.Locale.Tr "repo.settings.branch_protection" (.Rule.RuleName|Escape)}}
+				{{ctx.Locale.Tr "repo.settings.branch_protection" .Rule.RuleName}}
 			</h4>
 			<div class="ui attached segment branch-protection">
 				<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_patterns"}}</h5>
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index f3b6be97cf..9fa05b5b5c 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -107,7 +107,7 @@
 		{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" ($title|Escape)}}</p>
+		<p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" $title}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index 7940234ccc..e8a0079c1c 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -62,11 +62,11 @@
 					</a>
 					{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
 					{{if .OriginalAuthor}}
-						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape)}}
+						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}}
 					{{else if gt .Poster.ID 0}}
-						{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape)}}
+						{{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}}
 					{{else}}
-						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape)}}
+						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}}
 					{{end}}
 					{{if .IsPull}}
 						<div class="branches flex-text-inline">
diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl
index 589899f9d3..9cd1712275 100644
--- a/templates/user/auth/activate.tmpl
+++ b/templates/user/auth/activate.tmpl
@@ -15,7 +15,7 @@
 						{{else if .ResendLimited}}
 							<p class="center">{{ctx.Locale.Tr "auth.resent_limit_prompt"}}</p>
 						{{else}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.SignedUser.Email|Escape) .ActiveCodeLives}}</p>
+							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" .SignedUser.Email .ActiveCodeLives}}</p>
 						{{end}}
 					{{else}}
 						{{if .NeedsPassword}}
@@ -29,7 +29,7 @@
 							</div>
 							<input id="code" name="code" type="hidden" value="{{.Code}}">
 						{{else if .IsSendRegisterMail}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.Email|Escape) .ActiveCodeLives}}</p>
+							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" .Email .ActiveCodeLives}}</p>
 						{{else if .IsCodeInvalid}}
 							<p>{{ctx.Locale.Tr "auth.invalid_code"}}</p>
 						{{else if .IsPasswordInvalid}}
@@ -37,7 +37,7 @@
 						{{else if .ManualActivationOnly}}
 							<p class="center">{{ctx.Locale.Tr "auth.manual_activation_only"}}</p>
 						{{else}}
-							<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" (.SignedUser.Name|Escape) (.SignedUser.Email|Escape)}}</p>
+							<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" .SignedUser.Name .SignedUser.Email}}</p>
 							<div class="divider"></div>
 							<div class="text right">
 								<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl
index 03621ea87f..21a630ec5e 100644
--- a/templates/user/auth/forgot_passwd.tmpl
+++ b/templates/user/auth/forgot_passwd.tmpl
@@ -10,7 +10,7 @@
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
 					{{if .IsResetSent}}
-						<p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" (Escape .Email) .ResetPwdCodeLives}}</p>
+						<p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" .Email .ResetPwdCodeLives}}</p>
 					{{else if .IsResetRequest}}
 						<div class="required inline field {{if .Err_Email}}error{{end}}">
 							<label for="email">{{ctx.Locale.Tr "email"}}</label>
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index bb619a5f18..6dec610e93 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -12,71 +12,71 @@
 						{{.ShortActUserName ctx}}
 					{{end}}
 					{{if .GetOpType.InActions "create_repo"}}
-						{{ctx.Locale.Tr "action.create_repo" ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.create_repo" (.GetRepoLink ctx) (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "rename_repo"}}
-						{{ctx.Locale.Tr "action.rename_repo" (.GetContent|Escape) ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.rename_repo" .GetContent (.GetRepoLink ctx) (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "commit_repo"}}
 						{{if .Content}}
-							{{ctx.Locale.Tr "action.commit_repo" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape)}}
+							{{ctx.Locale.Tr "action.commit_repo" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 						{{else}}
-							{{ctx.Locale.Tr "action.create_branch" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape)}}
+							{{ctx.Locale.Tr "action.create_branch" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 						{{end}}
 					{{else if .GetOpType.InActions "create_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.create_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.create_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "create_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.create_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.create_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "transfer_repo"}}
-						{{ctx.Locale.Tr "action.transfer_repo" .GetContent ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.transfer_repo" .GetContent (.GetRepoLink ctx) (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "push_tag"}}
-						{{ctx.Locale.Tr "action.push_tag" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.push_tag" (.GetRepoLink ctx) (.GetRefLink ctx) .GetTag (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "comment_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.comment_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.comment_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "merge_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.merge_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.merge_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "close_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.close_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.close_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "reopen_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reopen_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.reopen_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "close_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.close_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.close_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "reopen_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reopen_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.reopen_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "delete_tag"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.delete_tag" ((.GetRepoLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.delete_tag" (.GetRepoLink ctx) .GetTag (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "delete_branch"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.delete_branch" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.delete_branch" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "mirror_sync_push"}}
-						{{ctx.Locale.Tr "action.mirror_sync_push" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.mirror_sync_push" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "mirror_sync_create"}}
-						{{ctx.Locale.Tr "action.mirror_sync_create" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.mirror_sync_create" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "mirror_sync_delete"}}
-						{{ctx.Locale.Tr "action.mirror_sync_delete" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.mirror_sync_delete" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "approve_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.approve_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.approve_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "reject_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reject_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.reject_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "comment_pull"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.comment_pull" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape)}}
+						{{ctx.Locale.Tr "action.comment_pull" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "publish_release"}}
 						{{$linkText := .Content | RenderEmoji $.Context}}
-						{{ctx.Locale.Tr "action.publish_release" ((.GetRepoLink ctx)|Escape) ((printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag)|Escape) ((.ShortRepoPath ctx)|Escape) $linkText}}
+						{{ctx.Locale.Tr "action.publish_release" (.GetRepoLink ctx) (printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag) (.ShortRepoPath ctx) $linkText}}
 					{{else if .GetOpType.InActions "review_dismissed"}}
 						{{$index := index .GetIssueInfos 0}}
 						{{$reviewer := index .GetIssueInfos 1}}
-						{{ctx.Locale.Tr "action.review_dismissed" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) $reviewer}}
+						{{ctx.Locale.Tr "action.review_dismissed" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx) $reviewer}}
 					{{end}}
 					{{TimeSince .GetCreate ctx.Locale}}
 				</div>

From 756b952c52f1efbb137df36d5b97b370c8a45565 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Sun, 25 Feb 2024 15:31:15 +0100
Subject: [PATCH 172/679] enforce maxlength in frontend (#29389)

Set maxlength attribute in frontend

to long file-name

![image](https://github.com/go-gitea/gitea/assets/72873130/15111614-55ab-4583-acb2-15c25997601d)

![image](https://github.com/go-gitea/gitea/assets/72873130/4105ddd8-4973-4da8-b3ab-4cfae1b45554)
(same for branch-name and commit-summary)
---
 templates/repo/editor/commit_form.tmpl | 4 ++--
 templates/repo/editor/edit.tmpl        | 2 +-
 templates/repo/editor/patch.tmpl       | 2 +-
 templates/repo/editor/upload.tmpl      | 2 +-
 web_src/js/utils.js                    | 7 ++++---
 5 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index f0f0f47826..94429452dd 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -9,7 +9,7 @@
 			{{ctx.Locale.Tr "repo.editor.commit_changes"}}
 		{{- end}}</h3>
 		<div class="field">
-			<input name="commit_summary" placeholder="{{if .PageIsDelete}}{{ctx.Locale.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{ctx.Locale.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.add_tmpl"}}{{else if .PageIsPatch}}{{ctx.Locale.Tr "repo.editor.patch"}}{{else}}{{ctx.Locale.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus>
+			<input name="commit_summary" maxlength="100" placeholder="{{if .PageIsDelete}}{{ctx.Locale.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{ctx.Locale.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.add_tmpl"}}{{else if .PageIsPatch}}{{ctx.Locale.Tr "repo.editor.patch"}}{{else}}{{ctx.Locale.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus>
 		</div>
 		<div class="field">
 			<textarea name="commit_message" placeholder="{{ctx.Locale.Tr "repo.editor.commit_message_desc"}}" rows="5">{{.commit_message}}</textarea>
@@ -60,7 +60,7 @@
 			<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}gt-hidden{{end}}">
 				<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
 					{{svg "octicon-git-branch"}}
-					<input type="text" name="new_branch_name" value="{{.new_branch_name}}" class="input-contrast gt-mr-2 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
+					<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast gt-mr-2 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
 					<span class="text-muted js-quick-pull-normalization-info"></span>
 				</div>
 			</div>
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index cfc266731b..a6dce81c08 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -15,7 +15,7 @@
 					{{range $i, $v := .TreeNames}}
 						<div class="breadcrumb-divider">/</div>
 						{{if eq $i $l}}
-							<input id="file-name" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
+							<input id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
 							<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
 						{{else}}
 							<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
index 44c30bd5f9..c9a78cc35f 100644
--- a/templates/repo/editor/patch.tmpl
+++ b/templates/repo/editor/patch.tmpl
@@ -15,7 +15,7 @@
 					<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a>
 					<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
 					<input type="hidden" id="tree_path" name="tree_path" value="" required>
-					<input id="file-name" type="hidden" value="diff.patch">
+					<input id="file-name" maxlength="500" type="hidden" value="diff.patch">
 				</div>
 			</div>
 			<div class="field">
diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl
index d362a5602a..0a7c49dae3 100644
--- a/templates/repo/editor/upload.tmpl
+++ b/templates/repo/editor/upload.tmpl
@@ -13,7 +13,7 @@
 					{{range $i, $v := .TreeNames}}
 						<div class="breadcrumb-divider">/</div>
 						{{if eq $i $l}}
-							<input type="text" id="file-name" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
+							<input type="text" id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
 							<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
 						{{else}}
 							<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index 3a2694335f..ce0fb66343 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -2,13 +2,14 @@ import {encode, decode} from 'uint8-to-base64';
 
 // transform /path/to/file.ext to file.ext
 export function basename(path = '') {
-  return path ? path.replace(/^.*\//, '') : '';
+  const lastSlashIndex = path.lastIndexOf('/');
+  return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1);
 }
 
 // transform /path/to/file.ext to .ext
 export function extname(path = '') {
-  const [_, ext] = /.+(\.[^.]+)$/.exec(path) || [];
-  return ext || '';
+  const lastPointIndex = path.lastIndexOf('.');
+  return lastPointIndex < 0 ? '' : path.substring(lastPointIndex);
 }
 
 // test whether a variable is an object

From 1c6858543ca976933004c21b3056a7301e1729d6 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sun, 25 Feb 2024 16:10:55 +0100
Subject: [PATCH 173/679] Integrate alpine `noarch` packages into other
 architectures index (#29137)

Fixes #26691
Revert #24972

The alpine package manager expects `noarch` packages in the index of
other architectures too.

---------

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 modules/packages/alpine/metadata.go           |   2 +
 routers/api/packages/alpine/alpine.go         |  33 +++-
 services/packages/alpine/repository.go        |  56 +++++-
 tests/integration/api_packages_alpine_test.go | 163 ++++++++++++------
 4 files changed, 190 insertions(+), 64 deletions(-)

diff --git a/modules/packages/alpine/metadata.go b/modules/packages/alpine/metadata.go
index 582c42610d..c492811744 100644
--- a/modules/packages/alpine/metadata.go
+++ b/modules/packages/alpine/metadata.go
@@ -34,6 +34,8 @@ const (
 
 	RepositoryPackage = "_alpine"
 	RepositoryVersion = "_repository"
+
+	NoArch = "noarch"
 )
 
 // https://wiki.alpinelinux.org/wiki/Apk_spec
diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go
index bb14c5163a..3fd8288c01 100644
--- a/routers/api/packages/alpine/alpine.go
+++ b/routers/api/packages/alpine/alpine.go
@@ -72,7 +72,7 @@ func GetRepositoryFile(ctx *context.Context) {
 		ctx,
 		pv,
 		&packages_service.PackageFileInfo{
-			Filename:     alpine_service.IndexFilename,
+			Filename:     alpine_service.IndexArchiveFilename,
 			CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
 		},
 	)
@@ -182,19 +182,38 @@ func UploadPackageFile(ctx *context.Context) {
 }
 
 func DownloadPackageFile(ctx *context.Context) {
-	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
+	branch := ctx.Params("branch")
+	repository := ctx.Params("repository")
+	architecture := ctx.Params("architecture")
+
+	opts := &packages_model.PackageFileSearchOptions{
 		OwnerID:      ctx.Package.Owner.ID,
 		PackageType:  packages_model.TypeAlpine,
 		Query:        ctx.Params("filename"),
-		CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
-	})
+		CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
+	}
+	pfs, _, err := packages_model.SearchFiles(ctx, opts)
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
 		return
 	}
-	if len(pfs) != 1 {
-		apiError(ctx, http.StatusNotFound, nil)
-		return
+	if len(pfs) == 0 {
+		// Try again with architecture 'noarch'
+		if architecture == alpine_module.NoArch {
+			apiError(ctx, http.StatusNotFound, nil)
+			return
+		}
+
+		opts.CompositeKey = fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch)
+		if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+
+		if len(pfs) == 0 {
+			apiError(ctx, http.StatusNotFound, nil)
+			return
+		}
 	}
 
 	s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go
index 104548b421..664ab34559 100644
--- a/services/packages/alpine/repository.go
+++ b/services/packages/alpine/repository.go
@@ -23,6 +23,7 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	alpine_model "code.gitea.io/gitea/models/packages/alpine"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/json"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
@@ -30,7 +31,10 @@ import (
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
-const IndexFilename = "APKINDEX.tar.gz"
+const (
+	IndexFilename        = "APKINDEX"
+	IndexArchiveFilename = IndexFilename + ".tar.gz"
+)
 
 // GetOrCreateRepositoryVersion gets or creates the internal repository package
 // The Alpine registry needs multiple index files which are stored in this package.
@@ -120,7 +124,22 @@ func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, re
 		return err
 	}
 
-	return buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture)
+	architectures := container.SetOf(architecture)
+	if architecture == alpine_module.NoArch {
+		// Update all other architectures too when updating the noarch index
+		additionalArchitectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
+		if err != nil {
+			return err
+		}
+		architectures.AddMultiple(additionalArchitectures...)
+	}
+
+	for architecture := range architectures {
+		if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
 type packageData struct {
@@ -133,8 +152,7 @@ type packageData struct {
 
 type packageCache = map[*packages_model.PackageFile]*packageData
 
-// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
-func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
+func searchPackageFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) ([]*packages_model.PackageFile, error) {
 	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
 		OwnerID:     ownerID,
 		PackageType: packages_model.TypeAlpine,
@@ -145,13 +163,30 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 			alpine_module.PropertyArchitecture: architecture,
 		},
 	})
+	if err != nil {
+		return nil, err
+	}
+	return pfs, nil
+}
+
+// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
+func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
+	pfs, err := searchPackageFiles(ctx, ownerID, branch, repository, architecture)
 	if err != nil {
 		return err
 	}
+	if architecture != alpine_module.NoArch {
+		// Add all noarch packages too
+		noarchFiles, err := searchPackageFiles(ctx, ownerID, branch, repository, alpine_module.NoArch)
+		if err != nil {
+			return err
+		}
+		pfs = append(pfs, noarchFiles...)
+	}
 
 	// Delete the package indices if there are no packages
 	if len(pfs) == 0 {
-		pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
+		pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
 		if err != nil && !errors.Is(err, util.ErrNotExist) {
 			return err
 		} else if pf == nil {
@@ -206,7 +241,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 		fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum)
 		fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name)
 		fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version)
-		fmt.Fprintf(&buf, "A:%s\n", pd.FileMetadata.Architecture)
+		fmt.Fprintf(&buf, "A:%s\n", architecture)
 		if pd.VersionMetadata.Description != "" {
 			fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description)
 		}
@@ -244,7 +279,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 
 	h := sha1.New()
 
-	if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), "APKINDEX", buf.Bytes(), true); err != nil {
+	if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), IndexFilename, buf.Bytes(), true); err != nil {
 		return err
 	}
 
@@ -299,13 +334,18 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 		repoVersion,
 		&packages_service.PackageFileCreationInfo{
 			PackageFileInfo: packages_service.PackageFileInfo{
-				Filename:     IndexFilename,
+				Filename:     IndexArchiveFilename,
 				CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
 			},
 			Creator:           user_model.NewGhostUser(),
 			Data:              signedIndexContent,
 			IsLead:            false,
 			OverwriteExisting: true,
+			Properties: map[string]string{
+				alpine_module.PropertyBranch:       branch,
+				alpine_module.PropertyRepository:   repository,
+				alpine_module.PropertyArchitecture: architecture,
+			},
 		},
 	)
 	return err
diff --git a/tests/integration/api_packages_alpine_test.go b/tests/integration/api_packages_alpine_test.go
index 3cc7178e02..228f497127 100644
--- a/tests/integration/api_packages_alpine_test.go
+++ b/tests/integration/api_packages_alpine_test.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
+	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -59,7 +60,34 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA`
 	content, err := base64.StdEncoding.DecodeString(base64AlpinePackageContent)
 	assert.NoError(t, err)
 
-	branches := []string{"v3.16", "v3.17", "v3.18"}
+	base64AlpinePackageNoArchContent := `H4sIAAAAAAACA9ML9nT30wsKdtQrLU4t0jUzTUo1NDVP0ysqTtQrKE1ioAYwAAIzExMwDQTotCGI
+bWhiampuYmRiaGrMYGBoZGZkxKBgwEAHUFpcklikoMAwQkHLB7eoE40P9n5jvx32t7Dy9rq7x19k
+66cJPV38t/h+vWe2jdXy+/PzPT0YTF5z39i4cPFptcLa1C1lD0z/XvrNp6In/7nP4PPCF2pZu8uV
+z74QXLxpY1XWJuVFysqVf+PdizccFbD6ZL/QPGXd1Ri1fec2XBNuYfK/rFa6wF/h3dK/W12f8mxP
+04iP3aCy+vPx7h9S+5M1LLkWr5M/4ezGt3bDW/FjBp/S9hiKP72s/XrJ0vWtO0zr5wa+D/X8XluW
+d7BLP7XS3YUhd8WbPPF/NW3691ONJbXsRb69O7BIMZC96uTri+utC/fbie5J+n7zhCxD4Aep/qet
+QnlCZyN8MhNdVNlNl7965R1nExrrGvfI/YQZFx8Dg+d9122hZsYd/24WL/L69OWrDAN/y//nS7im
+XEive3v7QeTe433TPj/X71+9yHiV6+E9k++3TL8V0Xoq9panhNt23fLgau/pTOvmKx6bV/pS26+Y
+5UP4viyuklYeu4/BZl6rLINe1L/uWuUXcH5z7pa2b9+/rp/v/8dFgc1PL3bO3/iVcrI//J/LMU2X
+Nzu1IaMmWXnGp7CmyQIR39d0Nai9/+tdPbfjvmsNH88Tu7uVrvNuJE0wjxfePXGv/KHNXD+mnG0t
+yTPu+Na0b5WR9O4t0yMd9T5k6ui7hOyU/jL/4dOn6neLwhdrZIZfcl1ectnGvUTurWDo1vY5Gw9k
+PTQLVgcA61F+7gAEAAAfiwgAAAAAAAID7VVNa9wwEPXZv2Ig53hHlizbCzkVkobQJtDkB4wl2SvW
+lhdbTpP++oyXQGEPLYU2paTvIs3X05PQSNnmjp4+OrJumjfZ3c3V9efL2+T3AhlaqePIOB0Rc50I
+VRSlypUoZIJCKJQJPCVvgGWONLGU5H1CCDDRD+4CU57S6zT5j3eCP9Tyv9T/GsuT/scyLxPAt+z/
+aRzjj/J+Fv9HcQZXLriJorPQPAM1i+8tyEzkGZ5PmJ7BMvvQQUt7tx4BPPJH4ccAIpN5Jjj+hSJc
+ugZAghDbArco4eH+A+SYq/Sw7wINDi6g89HReRhpMrvVzTzsFZlaV2Hbutmw4zVhmXo2djEe5u1m
+c6zNzDikR3mW1a61JepaC0SZHsjsqTsyPoR9GL+GdPbf1iSFtU5Xyu/c4+Q7H04lMfvgI3vT3hsX
+5rX40/U9b5CWOA78Mhrq+2ewLjrDp7VNWQbtaF6ZXVWZIhdV09RWOIvU6BqNboSxLSEpkrpQq80x
+W1Nla6NavuqtrJQ0sv17D+4L2oD1lwAIAAAfiwgAAAAAAAID7dM/SgNBFAbw6cSAnYXlXsDNm50/
+u1METBeIkEBMK87uzKKEJbB/IN7CxhN4AI/gNcRD6BWciI0WSiBGxO/XvA9mile8L+5P7WrkrfN1
+049dV1XXbNso0FK+zeDzJC4SxqVSqUwkV4IR51KkLFqxHeia1tZhFfY/cR4V7VXlB9QL0b5HnUXD
+6fj4bDI5ncXFpS8WTVfFs9GQD5wVxgrvlde5zMmJRKm89KVRmnhmyJYuo5RMj8Ef8EOV36j/6/yx
+/5qnxKJ1J8MZJifskD2Zu+fzxfggmT+83F4c3dw/7u1vtf/1ctl+9e+7dwAAAAAAAAAAAAAAAAAA
+AACAX/AKARNTyAAoAAA=`
+	noarchContent, err := base64.StdEncoding.DecodeString(base64AlpinePackageNoArchContent)
+	assert.NoError(t, err)
+
+	branches := []string{"v3.16", "v3.17"}
 	repositories := []string{"main", "testing"}
 
 	rootURL := fmt.Sprintf("/api/packages/%s/alpine", user.Name)
@@ -139,63 +167,71 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA`
 					})
 				})
 
-				t.Run("Index", func(t *testing.T) {
-					defer tests.PrintCurrentTest(t)()
+				readIndexContent := func(r io.Reader) (string, error) {
+					br := bufio.NewReader(r)
 
-					url := fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)
+					gzr, err := gzip.NewReader(br)
+					if err != nil {
+						return "", err
+					}
 
-					req := NewRequest(t, "GET", url)
-					resp := MakeRequest(t, req, http.StatusOK)
-
-					assert.Condition(t, func() bool {
-						br := bufio.NewReader(resp.Body)
-
-						gzr, err := gzip.NewReader(br)
-						assert.NoError(t, err)
+					for {
+						gzr.Multistream(false)
 
+						tr := tar.NewReader(gzr)
 						for {
-							gzr.Multistream(false)
-
-							tr := tar.NewReader(gzr)
-							for {
-								hd, err := tr.Next()
-								if err == io.EOF {
-									break
-								}
-								assert.NoError(t, err)
-
-								if hd.Name == "APKINDEX" {
-									buf, err := io.ReadAll(tr)
-									assert.NoError(t, err)
-
-									s := string(buf)
-
-									assert.Contains(t, s, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n")
-									assert.Contains(t, s, "P:"+packageName+"\n")
-									assert.Contains(t, s, "V:"+packageVersion+"\n")
-									assert.Contains(t, s, "A:x86_64\n")
-									assert.Contains(t, s, "T:Gitea Test Package\n")
-									assert.Contains(t, s, "U:https://gitea.io/\n")
-									assert.Contains(t, s, "L:MIT\n")
-									assert.Contains(t, s, "S:1353\n")
-									assert.Contains(t, s, "I:4096\n")
-									assert.Contains(t, s, "o:gitea-test\n")
-									assert.Contains(t, s, "m:KN4CK3R <kn4ck3r@gitea.io>\n")
-									assert.Contains(t, s, "t:1679498030\n")
-
-									return true
-								}
-							}
-
-							err = gzr.Reset(br)
+							hd, err := tr.Next()
 							if err == io.EOF {
 								break
 							}
-							assert.NoError(t, err)
+							if err != nil {
+								return "", err
+							}
+
+							if hd.Name == alpine_service.IndexFilename {
+								buf, err := io.ReadAll(tr)
+								if err != nil {
+									return "", err
+								}
+
+								return string(buf), nil
+							}
 						}
 
-						return false
-					})
+						err = gzr.Reset(br)
+						if err == io.EOF {
+							break
+						}
+						if err != nil {
+							return "", err
+						}
+					}
+
+					return "", io.EOF
+				}
+
+				t.Run("Index", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository))
+					resp := MakeRequest(t, req, http.StatusOK)
+
+					content, err := readIndexContent(resp.Body)
+					assert.NoError(t, err)
+
+					assert.Contains(t, content, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n")
+					assert.Contains(t, content, "P:"+packageName+"\n")
+					assert.Contains(t, content, "V:"+packageVersion+"\n")
+					assert.Contains(t, content, "A:x86_64\n")
+					assert.NotContains(t, content, "A:noarch\n")
+					assert.Contains(t, content, "T:Gitea Test Package\n")
+					assert.Contains(t, content, "U:https://gitea.io/\n")
+					assert.Contains(t, content, "L:MIT\n")
+					assert.Contains(t, content, "S:1353\n")
+					assert.Contains(t, content, "I:4096\n")
+					assert.Contains(t, content, "o:gitea-test\n")
+					assert.Contains(t, content, "m:KN4CK3R <kn4ck3r@gitea.io>\n")
+					assert.Contains(t, content, "t:1679498030\n")
 				})
 
 				t.Run("Download", func(t *testing.T) {
@@ -204,6 +240,35 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA`
 					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion))
 					MakeRequest(t, req, http.StatusOK)
 				})
+
+				t.Run("NoArch", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s/%s", rootURL, branch, repository), bytes.NewReader(noarchContent)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusCreated)
+
+					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository))
+					resp := MakeRequest(t, req, http.StatusOK)
+
+					content, err := readIndexContent(resp.Body)
+					assert.NoError(t, err)
+
+					assert.Contains(t, content, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n")
+					assert.Contains(t, content, "A:x86_64\n")
+					assert.Contains(t, content, "C:Q1kbH5WoIPFccQYyATanaKXd2cJcc=\n")
+					assert.NotContains(t, content, "A:noarch\n")
+
+					// noarch package should be available with every architecture requested
+					for _, arch := range []string{alpine_module.NoArch, "x86_64", "my_arch"} {
+						req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s/gitea-noarch-1.4-r0.apk", rootURL, branch, repository, arch))
+						MakeRequest(t, req, http.StatusOK)
+					}
+
+					req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/noarch/gitea-noarch-1.4-r0.apk", rootURL, branch, repository)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusNoContent)
+				})
 			})
 		}
 	}

From 4ccf5ab330c9ce8959aa6734c2e6fee282619ba5 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sun, 25 Feb 2024 16:42:36 +0100
Subject: [PATCH 174/679] Add missing space (#29393)

---
 templates/repo/diff/options_dropdown.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl
index 3bcb877cc6..b7c46dd846 100644
--- a/templates/repo/diff/options_dropdown.tmpl
+++ b/templates/repo/diff/options_dropdown.tmpl
@@ -13,7 +13,7 @@
 			<a class="item" href="{{$.RepoLink}}/commit/{{PathEscape .Commit.ID.String}}.diff" download="{{ShortSha .Commit.ID.String}}.diff">{{ctx.Locale.Tr "repo.diff.download_diff"}}</a>
 		{{end}}
 		<a id="expand-files-btn" class="item">{{ctx.Locale.Tr "repo.pulls.expand_files"}}</a>
-		<a id="collapse-files-btn"class="item">{{ctx.Locale.Tr "repo.pulls.collapse_files"}}</a>
+		<a id="collapse-files-btn" class="item">{{ctx.Locale.Tr "repo.pulls.collapse_files"}}</a>
 		{{if .Issue.Index}}
 			{{if .ShowOutdatedComments}}
 				<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=false">

From f4b92578b4601bc6e9b631b9a5a5f3766c27b0cb Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 25 Feb 2024 17:46:46 +0100
Subject: [PATCH 175/679] Add tailwindcss (#29357)

This will get tailwindcss working on a basic level. It provides only the
utility classes, e.g. no tailwind base which we don't need because we
have our own CSS reset. Without the base, we also do not have their CSS
variables so a small amount of features do not work and I removed the
generated classes for them.

***Note for future developers: This currently uses a `tw-` prefix, so we
use it like `tw-p-3`.***

<details>
<summary>Currently added CSS, all false-positives</summary>

```
.\!visible{

    visibility: visible !important
}

.visible{

    visibility: visible
}

.invisible{

    visibility: hidden
}

.collapse{

    visibility: collapse
}

.static{

    position: static
}

.\!fixed{

    position: fixed !important
}

.absolute{

    position: absolute
}

.relative{

    position: relative
}

.sticky{

    position: sticky
}

.left-10{

    left: 2.5rem
}

.isolate{

    isolation: isolate
}

.float-right{

    float: right
}

.float-left{

    float: left
}

.mr-2{

    margin-right: 0.5rem
}

.mr-3{

    margin-right: 0.75rem
}

.\!block{

    display: block !important
}

.block{

    display: block
}

.inline-block{

    display: inline-block
}

.inline{

    display: inline
}

.flex{

    display: flex
}

.inline-flex{

    display: inline-flex
}

.\!table{

    display: table !important
}

.inline-table{

    display: inline-table
}

.table-caption{

    display: table-caption
}

.table-cell{

    display: table-cell
}

.table-column{

    display: table-column
}

.table-column-group{

    display: table-column-group
}

.table-footer-group{

    display: table-footer-group
}

.table-header-group{

    display: table-header-group
}

.table-row-group{

    display: table-row-group
}

.table-row{

    display: table-row
}

.flow-root{

    display: flow-root
}

.inline-grid{

    display: inline-grid
}

.contents{

    display: contents
}

.list-item{

    display: list-item
}

.\!hidden{

    display: none !important
}

.hidden{

    display: none
}

.flex-shrink{

    flex-shrink: 1
}

.shrink{

    flex-shrink: 1
}

.flex-grow{

    flex-grow: 1
}

.grow{

    flex-grow: 1
}

.border-collapse{

    border-collapse: collapse
}

.select-all{

    user-select: all
}

.resize{

    resize: both
}

.flex-wrap{

    flex-wrap: wrap
}

.overflow-visible{

    overflow: visible
}

.rounded{

    border-radius: 0.25rem
}

.border{

    border-width: 1px
}

.text-justify{

    text-align: justify
}

.uppercase{

    text-transform: uppercase
}

.lowercase{

    text-transform: lowercase
}

.capitalize{

    text-transform: capitalize
}

.italic{

    font-style: italic
}

.text-red{

    color: var(--color-red)
}

.text-shadow{

    color: var(--color-shadow)
}

.underline{

    text-decoration-line: underline
}

.overline{

    text-decoration-line: overline
}

.line-through{

    text-decoration-line: line-through
}

.outline{

    outline-style: solid
}

.ease-in{

    transition-timing-function: cubic-bezier(0.4, 0, 1, 1)
}

.ease-in-out{

    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)
}

.ease-out{

    transition-timing-function: cubic-bezier(0, 0, 0.2, 1)
}
```

</details>

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 .stylelintrc.yaml               |   2 +-
 Makefile                        |   2 +-
 package-lock.json               | 514 ++++++++++++++++++++++++++++----
 package.json                    |   4 +
 tailwind.config.js              |  39 +++
 templates/devtest/gitea-ui.tmpl |   6 +
 web_src/css/base.css            |   8 +
 web_src/css/index.css           |   2 +
 webpack.config.js               |  11 +
 9 files changed, 530 insertions(+), 58 deletions(-)
 create mode 100644 tailwind.config.js

diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
index a44294ee76..7dd0a566f2 100644
--- a/.stylelintrc.yaml
+++ b/.stylelintrc.yaml
@@ -98,7 +98,7 @@ rules:
   at-rule-allowed-list: null
   at-rule-disallowed-list: null
   at-rule-empty-line-before: null
-  at-rule-no-unknown: true
+  at-rule-no-unknown: [true, {ignoreAtRules: [tailwind]}]
   at-rule-no-vendor-prefix: true
   at-rule-property-required-list: null
   block-no-empty: true
diff --git a/Makefile b/Makefile
index 4ef02c6c54..0e9e792053 100644
--- a/Makefile
+++ b/Makefile
@@ -119,7 +119,7 @@ GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/m
 FOMANTIC_WORK_DIR := web_src/fomantic
 
 WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
-WEBPACK_CONFIGS := webpack.config.js
+WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
 WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
 WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/img/webpack
 
diff --git a/package-lock.json b/package-lock.json
index f1f8cc4705..8f641edb5b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
         "chartjs-plugin-zoom": "2.0.1",
         "clippie": "4.0.6",
         "css-loader": "6.10.0",
+        "css-variables-parser": "1.0.1",
         "dayjs": "1.11.10",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
@@ -41,9 +42,12 @@
         "monaco-editor": "0.46.0",
         "monaco-editor-webpack-plugin": "7.1.0",
         "pdfobject": "2.3.0",
+        "postcss": "8.4.35",
+        "postcss-loader": "8.1.0",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
         "swagger-ui-dist": "5.11.6",
+        "tailwindcss": "3.4.1",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
         "tippy.js": "6.3.7",
@@ -105,6 +109,17 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/@asyncapi/specs": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.3.1.tgz",
@@ -118,7 +133,6 @@
       "version": "7.23.5",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
       "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
-      "dev": true,
       "dependencies": {
         "@babel/highlight": "^7.23.4",
         "chalk": "^2.4.2"
@@ -131,7 +145,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -143,7 +156,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -157,7 +169,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "dependencies": {
         "color-name": "1.1.3"
       }
@@ -165,14 +176,12 @@
     "node_modules/@babel/code-frame/node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
     },
     "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -181,7 +190,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -190,7 +198,6 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
       "dependencies": {
         "has-flag": "^3.0.0"
       },
@@ -202,7 +209,6 @@
       "version": "7.22.20",
       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
       "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
-      "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
@@ -211,7 +217,6 @@
       "version": "7.23.4",
       "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
       "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
-      "dev": true,
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
@@ -225,7 +230,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -237,7 +241,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -251,7 +254,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "dependencies": {
         "color-name": "1.1.3"
       }
@@ -259,14 +261,12 @@
     "node_modules/@babel/highlight/node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
     },
     "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -275,7 +275,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -283,14 +282,12 @@
     "node_modules/@babel/highlight/node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
     },
     "node_modules/@babel/highlight/node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
       "dependencies": {
         "has-flag": "^3.0.0"
       },
@@ -1111,7 +1108,6 @@
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
       "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
-      "dev": true,
       "dependencies": {
         "string-width": "^5.1.2",
         "string-width-cjs": "npm:string-width@^4.2.0",
@@ -1128,7 +1124,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
       "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-      "dev": true,
       "engines": {
         "node": ">=12"
       },
@@ -1140,7 +1135,6 @@
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
       "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-      "dev": true,
       "engines": {
         "node": ">=12"
       },
@@ -1151,14 +1145,12 @@
     "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
       "version": "9.2.2",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-      "dev": true
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
     },
     "node_modules/@isaacs/cliui/node_modules/string-width": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
       "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-      "dev": true,
       "dependencies": {
         "eastasianwidth": "^0.2.0",
         "emoji-regex": "^9.2.2",
@@ -1175,7 +1167,6 @@
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
       "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^6.0.1"
       },
@@ -1190,7 +1181,6 @@
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
       "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^6.1.0",
         "string-width": "^5.0.1",
@@ -1381,7 +1371,6 @@
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
       "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
-      "dev": true,
       "optional": true,
       "engines": {
         "node": ">=14"
@@ -3086,6 +3075,28 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+    },
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -3400,6 +3411,14 @@
         "node": "*"
       }
     },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/boolbase": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3534,11 +3553,18 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
     },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/caniuse-lite": {
       "version": "1.0.30001587",
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz",
@@ -3646,6 +3672,40 @@
         "node": "*"
       }
     },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/chrome-trace-event": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@@ -3857,7 +3917,6 @@
       "version": "9.0.0",
       "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
       "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
-      "dev": true,
       "dependencies": {
         "env-paths": "^2.2.1",
         "import-fresh": "^3.3.0",
@@ -3975,6 +4034,35 @@
         "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
       }
     },
+    "node_modules/css-variables-parser": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/css-variables-parser/-/css-variables-parser-1.0.1.tgz",
+      "integrity": "sha512-GWaqrwGtAWVr/yjjE17iyvbcy+W3voe0vko1/xLCwFeYd3kTLstzUdVH+g5TTXejrtlsb1FS4L9rP6PmeTa8wQ==",
+      "dependencies": {
+        "postcss": "^7.0.36"
+      }
+    },
+    "node_modules/css-variables-parser/node_modules/picocolors": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
+      "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
+    },
+    "node_modules/css-variables-parser/node_modules/postcss": {
+      "version": "7.0.39",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
+      "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
+      "dependencies": {
+        "picocolors": "^0.2.1",
+        "source-map": "^0.6.1"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      }
+    },
     "node_modules/css-what": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
@@ -4661,6 +4749,11 @@
         "node": ">=6"
       }
     },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+    },
     "node_modules/diff": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
@@ -4690,6 +4783,11 @@
         "node": ">=8"
       }
     },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+    },
     "node_modules/doctrine": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -4774,8 +4872,7 @@
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
-      "dev": true
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
     },
     "node_modules/easymde": {
       "version": "2.18.0",
@@ -4839,7 +4936,6 @@
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
       "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
@@ -4859,7 +4955,6 @@
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
       "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
       "dependencies": {
         "is-arrayish": "^0.2.1"
       }
@@ -6154,7 +6249,6 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
       "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
-      "dev": true,
       "dependencies": {
         "cross-spawn": "^7.0.0",
         "signal-exit": "^4.0.1"
@@ -6203,7 +6297,6 @@
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
       "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "dev": true,
       "hasInstallScript": true,
       "optional": true,
       "os": [
@@ -6390,7 +6483,6 @@
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
       "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-      "dev": true,
       "dependencies": {
         "is-glob": "^4.0.3"
       },
@@ -6815,7 +6907,6 @@
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
       "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dev": true,
       "dependencies": {
         "parent-module": "^1.0.0",
         "resolve-from": "^4.0.0"
@@ -6935,8 +7026,7 @@
     "node_modules/is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "dev": true
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
     },
     "node_modules/is-async-function": {
       "version": "2.0.0",
@@ -6965,6 +7055,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/is-boolean-object": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@@ -7377,7 +7478,6 @@
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
       "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
-      "dev": true,
       "dependencies": {
         "@isaacs/cliui": "^8.0.2"
       },
@@ -7418,6 +7518,14 @@
         "url": "https://github.com/chalk/supports-color?sponsor=1"
       }
     },
+    "node_modules/jiti": {
+      "version": "1.21.0",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
+      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
     "node_modules/jquery": {
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@@ -7779,11 +7887,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/lilconfig": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/lines-and-columns": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
-      "dev": true
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
     },
     "node_modules/linkify-it": {
       "version": "5.0.0",
@@ -8707,7 +8822,6 @@
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
       "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
-      "dev": true,
       "engines": {
         "node": ">=16 || 14 >=14.17"
       }
@@ -8759,6 +8873,16 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
     "node_modules/nanoid": {
       "version": "3.3.7",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -8902,7 +9026,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -8969,6 +9092,14 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/object-inspect": {
       "version": "1.13.1",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -9148,7 +9279,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
       "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dev": true,
       "dependencies": {
         "callsites": "^3.0.0"
       },
@@ -9160,7 +9290,6 @@
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
       "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "dev": true,
       "dependencies": {
         "@babel/code-frame": "^7.0.0",
         "error-ex": "^1.3.1",
@@ -9230,7 +9359,6 @@
       "version": "1.10.1",
       "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
       "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
-      "dev": true,
       "dependencies": {
         "lru-cache": "^9.1.1 || ^10.0.0",
         "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -9246,7 +9374,6 @@
       "version": "10.2.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
       "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
-      "dev": true,
       "engines": {
         "node": "14 || >=16.14"
       }
@@ -9296,6 +9423,22 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/pkg-dir": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -9462,6 +9605,70 @@
         "node": "^12 || >=14"
       }
     },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-loader": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz",
+      "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==",
+      "dependencies": {
+        "cosmiconfig": "^9.0.0",
+        "jiti": "^1.20.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "@rspack/core": "0.x || 1.x",
+        "postcss": "^7.0.0 || ^8.0.1",
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@rspack/core": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/postcss-modules-extract-imports": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
@@ -9517,6 +9724,24 @@
         "postcss": "^8.1.0"
       }
     },
+    "node_modules/postcss-nested": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
+      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.11"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
     "node_modules/postcss-resolve-nested-selector": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
@@ -9754,6 +9979,14 @@
       "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
       "dev": true
     },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
     "node_modules/read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -9856,6 +10089,17 @@
         "node": ">=8"
       }
     },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
     "node_modules/rechoir": {
       "version": "0.8.0",
       "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@@ -10037,7 +10281,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -10398,7 +10641,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
       "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-      "dev": true,
       "engines": {
         "node": ">=14"
       },
@@ -10609,7 +10851,6 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -10680,7 +10921,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -10999,6 +11239,56 @@
         "node": ">= 8"
       }
     },
+    "node_modules/sucrase": {
+      "version": "3.35.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "^10.3.10",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/sucrase/node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/sucrase/node_modules/glob": {
+      "version": "10.3.10",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^2.3.5",
+        "minimatch": "^9.0.1",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+        "path-scurry": "^1.10.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/superstruct": {
       "version": "0.10.13",
       "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.13.tgz",
@@ -11144,6 +11434,87 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/tailwindcss": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
+      "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.5.3",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.0",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.19.1",
+        "lilconfig": "^2.1.0",
+        "micromatch": "^4.0.5",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.0.0",
+        "postcss": "^8.4.23",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.1",
+        "postcss-nested": "^6.0.1",
+        "postcss-selector-parser": "^6.0.11",
+        "resolve": "^1.22.2",
+        "sucrase": "^3.32.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tailwindcss/node_modules/postcss-load-config": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "lilconfig": "^3.0.0",
+        "yaml": "^2.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
+      "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
     "node_modules/tapable": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -11258,6 +11629,25 @@
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
       "dev": true
     },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/throttle-debounce": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
@@ -11380,6 +11770,11 @@
         "node": ">=6.10"
       }
     },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+    },
     "node_modules/tsconfig-paths": {
       "version": "3.15.0",
       "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -12431,7 +12826,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -12569,6 +12963,14 @@
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
+    "node_modules/yaml": {
+      "version": "2.3.4",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
+      "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/yargs": {
       "version": "17.3.1",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
diff --git a/package.json b/package.json
index fdea78ca29..3f0f9103cf 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
     "chartjs-plugin-zoom": "2.0.1",
     "clippie": "4.0.6",
     "css-loader": "6.10.0",
+    "css-variables-parser": "1.0.1",
     "dayjs": "1.11.10",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
@@ -40,9 +41,12 @@
     "monaco-editor": "0.46.0",
     "monaco-editor-webpack-plugin": "7.1.0",
     "pdfobject": "2.3.0",
+    "postcss": "8.4.35",
+    "postcss-loader": "8.1.0",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
     "swagger-ui-dist": "5.11.6",
+    "tailwindcss": "3.4.1",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
     "tippy.js": "6.3.7",
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000000..8c474c33a8
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,39 @@
+import {readFileSync} from 'node:fs';
+import {env} from 'node:process';
+import {parse} from 'css-variables-parser';
+
+const isProduction = env.NODE_ENV !== 'development';
+
+export default {
+  prefix: 'tw-',
+  content: [
+    isProduction && '!./templates/devtest/**/*',
+    isProduction && '!./web_src/js/standalone/devtest.js',
+    './templates/**/*.tmpl',
+    './web_src/**/*.{js,vue}',
+  ].filter(Boolean),
+  blocklist: [
+    // classes that don't work without CSS variables from "@tailwind base" which we don't use
+    'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter',
+    'backdrop-filter',
+    // unneeded classes
+    '[-a-zA-Z:0-9_.]',
+  ],
+  theme: {
+    colors: {
+      // make `tw-bg-red` etc work with our CSS variables
+      ...Object.fromEntries(
+        Object.keys(parse([
+          readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'),
+          readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'),
+        ].join('\n'), {})).filter((prop) => prop.startsWith('color-')).map((prop) => {
+          const color = prop.substring(6);
+          return [color, `var(--color-${color})`];
+        })
+      ),
+      inherit: 'inherit',
+      current: 'currentcolor',
+      transparent: 'transparent',
+    },
+  },
+};
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 73293ddf48..ccf188609c 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -275,6 +275,12 @@
 		<div>ps: no JS code attached, so just a layout</div>
 		{{template "shared/combomarkdowneditor" .}}
 	</div>
+
+	<h1>Tailwind CSS Demo</h1>
+	<div>
+		<button class="{{if true}}tw-bg-red{{end}} tw-p-5 tw-border tw-rounded hover:tw-bg-blue active:tw-bg-yellow">Button</button>
+	</div>
+
 	<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
 </div>
 {{template "base/footer" .}}
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 76ecfc9bf5..280808a5ce 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -29,6 +29,14 @@
   --fonts-regular: var(--fonts-override, var(--fonts-proportional)), "Noto Sans", "Liberation Sans", sans-serif, var(--fonts-emoji);
 }
 
+*, ::before, ::after {
+  /* these are needed for tailwind borders to work because we do not load tailwind's base
+     https://github.com/tailwindlabs/tailwindcss/blob/master/src/css/preflight.css */
+  border-width: 0;
+  border-style: solid;
+  border-color: currentcolor;
+}
+
 textarea {
   font-family: var(--fonts-regular);
 }
diff --git a/web_src/css/index.css b/web_src/css/index.css
index f893531b78..ab925a4aa0 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -59,4 +59,6 @@
 @import "./explore.css";
 @import "./review.css";
 @import "./actions.css";
+
+@tailwind utilities;
 @import "./helpers.css";
diff --git a/webpack.config.js b/webpack.config.js
index 82d76d9e8d..3973d85344 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -11,6 +11,8 @@ import webpack from 'webpack';
 import {fileURLToPath} from 'node:url';
 import {readFileSync} from 'node:fs';
 import {env} from 'node:process';
+import tailwindcss from 'tailwindcss';
+import tailwindConfig from './tailwind.config.js';
 
 const {EsbuildPlugin} = EsBuildLoader;
 const {SourceMapDevToolPlugin, DefinePlugin} = webpack;
@@ -145,6 +147,15 @@ export default {
               import: {filter: filterCssImport},
             },
           },
+          {
+            loader: 'postcss-loader',
+            options: {
+              postcssOptions: {
+                map: false, // https://github.com/postcss/postcss/issues/1914
+                plugins: [tailwindcss(tailwindConfig)],
+              },
+            },
+          }
         ],
       },
       {

From ed3892d8430652c2bc8e2af21844d14769825e8a Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 25 Feb 2024 18:53:44 +0200
Subject: [PATCH 176/679] Remove jQuery AJAX from the archive download links
 (#29380)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the repo archive download links dropdown functionality and it
works as before

# Demo using `fetch` instead of jQuery AJAX

![action](https://github.com/go-gitea/gitea/assets/20454870/db791249-bca1-4d22-ac5e-623f68023e15)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-common.js | 58 ++++++++++++++----------------
 1 file changed, 27 insertions(+), 31 deletions(-)

diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js
index 3573e4d50b..a8221bbea8 100644
--- a/web_src/js/features/repo-common.js
+++ b/web_src/js/features/repo-common.js
@@ -1,38 +1,34 @@
 import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
 
-const {csrfToken} = window.config;
-
-function getArchive($target, url, first) {
-  $.ajax({
-    url,
-    type: 'POST',
-    data: {
-      _csrf: csrfToken,
-    },
-    complete(xhr) {
-      if (xhr.status === 200) {
-        if (!xhr.responseJSON) {
-          // XXX Shouldn't happen?
-          $target.closest('.dropdown').children('i').removeClass('loading');
-          return;
-        }
-
-        if (!xhr.responseJSON.complete) {
-          $target.closest('.dropdown').children('i').addClass('loading');
-          // Wait for only three quarters of a second initially, in case it's
-          // quickly archived.
-          setTimeout(() => {
-            getArchive($target, url, false);
-          }, first ? 750 : 2000);
-        } else {
-          // We don't need to continue checking.
-          $target.closest('.dropdown').children('i').removeClass('loading');
-          window.location.href = url;
-        }
+async function getArchive($target, url, first) {
+  try {
+    const response = await POST(url);
+    if (response.status === 200) {
+      const data = await response.json();
+      if (!data) {
+        // XXX Shouldn't happen?
+        $target.closest('.dropdown').children('i').removeClass('loading');
+        return;
       }
-    },
-  });
+
+      if (!data.complete) {
+        $target.closest('.dropdown').children('i').addClass('loading');
+        // Wait for only three quarters of a second initially, in case it's
+        // quickly archived.
+        setTimeout(() => {
+          getArchive($target, url, false);
+        }, first ? 750 : 2000);
+      } else {
+        // We don't need to continue checking.
+        $target.closest('.dropdown').children('i').removeClass('loading');
+        window.location.href = url;
+      }
+    }
+  } catch {
+    $target.closest('.dropdown').children('i').removeClass('loading');
+  }
 }
 
 export function initRepoArchiveLinks() {

From 49e482674700e184aa84806acfb7edaae0554291 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 26 Feb 2024 05:55:00 +0800
Subject: [PATCH 177/679] Refactor "user/active" related logic (#29390)

And add more tests. Remove a lot of fragile "if" blocks.

The old logic is kept as-is.
---
 routers/web/auth/auth.go                 | 127 ++++++++++++-----------
 templates/user/auth/activate.tmpl        |  48 +++------
 templates/user/auth/activate_prompt.tmpl |  15 +++
 tests/integration/signup_test.go         |  50 ++++++++-
 4 files changed, 146 insertions(+), 94 deletions(-)
 create mode 100644 templates/user/auth/activate_prompt.tmpl

diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 3de1f3373d..a30ee0ce54 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -7,6 +7,7 @@ package auth
 import (
 	"errors"
 	"fmt"
+	"html/template"
 	"net/http"
 	"strings"
 
@@ -37,12 +38,10 @@ import (
 )
 
 const (
-	// tplSignIn template for sign in page
-	tplSignIn base.TplName = "user/auth/signin"
-	// tplSignUp template path for sign up page
-	tplSignUp base.TplName = "user/auth/signup"
-	// TplActivate template path for activate user
-	TplActivate base.TplName = "user/auth/activate"
+	tplSignIn         base.TplName = "user/auth/signin"          // for sign in page
+	tplSignUp         base.TplName = "user/auth/signup"          // for sign up page
+	TplActivate       base.TplName = "user/auth/activate"        // for activate user
+	TplActivatePrompt base.TplName = "user/auth/activate_prompt" // for showing a message for user activation
 )
 
 // autoSignIn reads cookie and try to auto-login.
@@ -613,72 +612,83 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
 		}
 	}
 
-	// Send confirmation email
-	if !u.IsActive && u.ID > 1 {
-		if setting.Service.RegisterManualConfirm {
-			ctx.Data["ManualActivationOnly"] = true
-			ctx.HTML(http.StatusOK, TplActivate)
-			return false
-		}
+	// for active user or the first (admin) user, we don't need to send confirmation email
+	if u.IsActive || u.ID == 1 {
+		return true
+	}
 
-		mailer.SendActivateAccountMail(ctx.Locale, u)
-
-		ctx.Data["IsSendRegisterMail"] = true
-		ctx.Data["Email"] = u.Email
-		ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
-		ctx.HTML(http.StatusOK, TplActivate)
-
-		if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
-			log.Error("Set cache(MailResendLimit) fail: %v", err)
-		}
+	if setting.Service.RegisterManualConfirm {
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.manual_activation_only"))
 		return false
 	}
 
-	return true
+	sendActivateEmail(ctx, u)
+	return false
+}
+
+func renderActivationPromptMessage(ctx *context.Context, msg template.HTML) {
+	ctx.Data["ActivationPromptMessage"] = msg
+	ctx.HTML(http.StatusOK, TplActivatePrompt)
+}
+
+func sendActivateEmail(ctx *context.Context, u *user_model.User) {
+	if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+		return
+	}
+
+	if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+		log.Error("Set cache(MailResendLimit) fail: %v", err)
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+		return
+	}
+
+	mailer.SendActivateAccountMail(ctx.Locale, u)
+
+	activeCodeLives := timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
+	msgHTML := ctx.Locale.Tr("auth.confirmation_mail_sent_prompt", u.Email, activeCodeLives)
+	renderActivationPromptMessage(ctx, msgHTML)
+}
+
+func renderActivationVerifyPassword(ctx *context.Context, code string) {
+	ctx.Data["ActivationCode"] = code
+	ctx.Data["NeedVerifyLocalPassword"] = true
+	ctx.HTML(http.StatusOK, TplActivate)
 }
 
 // Activate render activate user page
 func Activate(ctx *context.Context) {
 	code := ctx.FormString("code")
 
-	if len(code) == 0 {
-		ctx.Data["IsActivatePage"] = true
-		if ctx.Doer == nil || ctx.Doer.IsActive {
-			ctx.NotFound("invalid user", nil)
+	if code == "" {
+		if ctx.Doer == nil {
+			ctx.Redirect(setting.AppSubURL + "/user/login")
+			return
+		} else if ctx.Doer.IsActive {
+			ctx.Redirect(setting.AppSubURL + "/")
 			return
 		}
-		// Resend confirmation email.
-		if setting.Service.RegisterEmailConfirm {
-			if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) {
-				ctx.Data["ResendLimited"] = true
-			} else {
-				ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
-				mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
 
-				if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
-					log.Error("Set cache(MailResendLimit) fail: %v", err)
-				}
-			}
-		} else {
-			ctx.Data["ServiceNotEnabled"] = true
+		if setting.MailService == nil || !setting.Service.RegisterEmailConfirm {
+			renderActivationPromptMessage(ctx, ctx.Tr("auth.disable_register_mail"))
+			return
 		}
-		ctx.HTML(http.StatusOK, TplActivate)
+
+		// Resend confirmation email.
+		sendActivateEmail(ctx, ctx.Doer)
 		return
 	}
 
+	// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
 	user := user_model.VerifyUserActiveCode(ctx, code)
-	// if code is wrong
-	if user == nil {
-		ctx.Data["IsCodeInvalid"] = true
-		ctx.HTML(http.StatusOK, TplActivate)
+	if user == nil { // if code is wrong
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
 		return
 	}
 
 	// if account is local account, verify password
 	if user.LoginSource == 0 {
-		ctx.Data["Code"] = code
-		ctx.Data["NeedsPassword"] = true
-		ctx.HTML(http.StatusOK, TplActivate)
+		renderActivationVerifyPassword(ctx, code)
 		return
 	}
 
@@ -688,31 +698,28 @@ func Activate(ctx *context.Context) {
 // ActivatePost handles account activation with password check
 func ActivatePost(ctx *context.Context) {
 	code := ctx.FormString("code")
-	if len(code) == 0 {
+	if code == "" || (ctx.Doer != nil && ctx.Doer.IsActive) {
 		ctx.Redirect(setting.AppSubURL + "/user/activate")
 		return
 	}
 
+	// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
 	user := user_model.VerifyUserActiveCode(ctx, code)
-	// if code is wrong
-	if user == nil {
-		ctx.Data["IsCodeInvalid"] = true
-		ctx.HTML(http.StatusOK, TplActivate)
+	if user == nil { // if code is wrong
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
 		return
 	}
 
 	// if account is local account, verify password
 	if user.LoginSource == 0 {
 		password := ctx.FormString("password")
-		if len(password) == 0 {
-			ctx.Data["Code"] = code
-			ctx.Data["NeedsPassword"] = true
-			ctx.HTML(http.StatusOK, TplActivate)
+		if password == "" {
+			renderActivationVerifyPassword(ctx, code)
 			return
 		}
 		if !user.ValidatePassword(password) {
-			ctx.Data["IsPasswordInvalid"] = true
-			ctx.HTML(http.StatusOK, TplActivate)
+			ctx.Flash.Error(ctx.Locale.Tr("auth.invalid_password"), true)
+			renderActivationVerifyPassword(ctx, code)
 			return
 		}
 	}
diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl
index 9cd1712275..51dc1eb6a6 100644
--- a/templates/user/auth/activate.tmpl
+++ b/templates/user/auth/activate.tmpl
@@ -9,40 +9,22 @@
 				</h2>
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
-					{{if .IsActivatePage}}
-						{{if .ServiceNotEnabled}}
-							<p class="center">{{ctx.Locale.Tr "auth.disable_register_mail"}}</p>
-						{{else if .ResendLimited}}
-							<p class="center">{{ctx.Locale.Tr "auth.resent_limit_prompt"}}</p>
-						{{else}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" .SignedUser.Email .ActiveCodeLives}}</p>
-						{{end}}
+					{{if .NeedVerifyLocalPassword}}
+						<div class="required inline field">
+							<label for="verify-password">{{ctx.Locale.Tr "password"}}</label>
+							<input id="verify-password" name="password" type="password" autocomplete="off" required>
+						</div>
+						<div class="inline field">
+							<label></label>
+							<button class="ui primary button">{{ctx.Locale.Tr "install.confirm_password"}}</button>
+						</div>
+						<input name="code" type="hidden" value="{{.ActivationCode}}">
 					{{else}}
-						{{if .NeedsPassword}}
-							<div class="required inline field">
-								<label for="password">{{ctx.Locale.Tr "password"}}</label>
-								<input id="password" name="password" type="password" autocomplete="off" required>
-							</div>
-							<div class="inline field">
-								<label></label>
-								<button class="ui primary button">{{ctx.Locale.Tr "install.confirm_password"}}</button>
-							</div>
-							<input id="code" name="code" type="hidden" value="{{.Code}}">
-						{{else if .IsSendRegisterMail}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" .Email .ActiveCodeLives}}</p>
-						{{else if .IsCodeInvalid}}
-							<p>{{ctx.Locale.Tr "auth.invalid_code"}}</p>
-						{{else if .IsPasswordInvalid}}
-							<p>{{ctx.Locale.Tr "auth.invalid_password"}}</p>
-						{{else if .ManualActivationOnly}}
-							<p class="center">{{ctx.Locale.Tr "auth.manual_activation_only"}}</p>
-						{{else}}
-							<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" .SignedUser.Name .SignedUser.Email}}</p>
-							<div class="divider"></div>
-							<div class="text right">
-								<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
-							</div>
-						{{end}}
+						<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" .SignedUser.Name .SignedUser.Email}}</p>
+						<div class="divider"></div>
+						<div class="text right">
+							<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
+						</div>
 					{{end}}
 				</div>
 			</form>
diff --git a/templates/user/auth/activate_prompt.tmpl b/templates/user/auth/activate_prompt.tmpl
new file mode 100644
index 0000000000..237244df8c
--- /dev/null
+++ b/templates/user/auth/activate_prompt.tmpl
@@ -0,0 +1,15 @@
+{{template "base/head" .}}
+<div role="main" aria-label="{{.Title}}" class="page-content user activate">
+	<div class="ui middle very relaxed page grid">
+		<div class="column">
+			<h2 class="ui top attached header">
+				{{ctx.Locale.Tr "auth.active_your_account"}}
+			</h2>
+			<div class="ui attached segment">
+				{{template "base/alert" .}}
+				<p>{{.ActivationPromptMessage}}</p>
+			</div>
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go
index 859f873f85..fbf586f696 100644
--- a/tests/integration/signup_test.go
+++ b/tests/integration/signup_test.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/tests"
 
@@ -58,7 +59,7 @@ func TestSignupAsRestricted(t *testing.T) {
 	assert.True(t, user2.IsRestricted)
 }
 
-func TestSignupEmail(t *testing.T) {
+func TestSignupEmailValidation(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	setting.Service.EnableCaptcha = false
@@ -91,3 +92,50 @@ func TestSignupEmail(t *testing.T) {
 		}
 	}
 }
+
+func TestSignupEmailActive(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
+
+	// try to sign up and send the activation email
+	req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
+		"user_name": "test-user-1",
+		"email":     "email-1@example.com",
+		"password":  "password1",
+		"retype":    "password1",
+	})
+	resp := MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`)
+
+	// access "user/active" means trying to re-send the activation email
+	session := loginUserWithPassword(t, "test-user-1", "password1")
+	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK)
+	assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently")
+
+	// access "user/active" with a valid activation code, then get the "verify password" page
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	activationCode := user.GenerateEmailActivateCode(user.Email)
+	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
+
+	// try to use a wrong password, it should fail
+	req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{
+		"code":     activationCode,
+		"password": "password-wrong",
+	})
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `Your password does not match`)
+	assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	assert.False(t, user.IsActive)
+
+	// then use a correct password, the user should be activated
+	req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{
+		"code":     activationCode,
+		"password": "password1",
+	})
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, "/", test.RedirectURL(resp))
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	assert.True(t, user.IsActive)
+}

From f13f93261ea7e5199a13f2c347ad2d45dbe75d4b Mon Sep 17 00:00:00 2001
From: kralo <kralo@users.noreply.github.com>
Date: Mon, 26 Feb 2024 00:35:52 +0100
Subject: [PATCH 178/679] Improve Documentation for Restoration from backup
 (#29321)

Comment the default path for repos and suggest using doctor for when
things are stuck
---
 docs/content/administration/backup-and-restore.en-us.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/docs/content/administration/backup-and-restore.en-us.md b/docs/content/administration/backup-and-restore.en-us.md
index d46efecf99..451ef5c944 100644
--- a/docs/content/administration/backup-and-restore.en-us.md
+++ b/docs/content/administration/backup-and-restore.en-us.md
@@ -92,7 +92,7 @@ cd gitea-dump-1610949662
 mv app.ini /etc/gitea/conf/app.ini
 mv data/* /var/lib/gitea/data/
 mv log/* /var/lib/gitea/log/
-mv repos/* /var/lib/gitea/gitea-repositories/
+mv repos/* /var/lib/gitea/data/gitea-repositories/
 chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea
 
 # mysql
@@ -111,6 +111,8 @@ With Gitea running, and from the directory Gitea's binary is located, execute: `
 
 This ensures that application and configuration file paths in repository Git Hooks are consistent and applicable to the current installation. If these paths are not updated, repository `push` actions will fail.
 
+If you still have issues, consider running `./gitea doctor check` to inspect possible errors (or run with `--fix`).
+
 ### Using Docker (`restore`)
 
 There is also no support for a recovery command in a Docker-based gitea instance. The restore process contains the same steps as described in the previous section but with different paths.

From f38888bc7834899777bda1a271e166d3836524cf Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 26 Feb 2024 00:24:51 +0000
Subject: [PATCH 179/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_fr-FR.ini | 37 +++++++++++++++++++++++++++++++--
 options/locale/locale_lv-LV.ini |  8 +++++++
 2 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 628bc2a777..7fd79446cc 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -123,6 +123,7 @@ pin=Épingler
 unpin=Désépingler
 
 artifacts=Artefacts
+confirm_delete_artifact=Êtes-vous sûr de vouloir supprimer l‘artefact « %s » ?
 
 archived=Archivé
 
@@ -361,6 +362,7 @@ disable_register_prompt=Les inscriptions sont désactivées. Veuillez contacter
 disable_register_mail=La confirmation par courriel à l’inscription est désactivée.
 manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation.
 remember_me=Mémoriser cet appareil
+remember_me.compromised=Le jeton de connexion n’est plus valide, ce qui peut indiquer un compte compromis. Veuillez inspecter les activités inhabituelles de votre compte.
 forgot_password_title=Mot de passe oublié
 forgot_password=Mot de passe oublié ?
 sign_up_now=Pas de compte ? Inscrivez-vous maintenant.
@@ -587,6 +589,7 @@ org_still_own_packages=Cette organisation possède encore un ou plusieurs paquet
 
 target_branch_not_exist=La branche cible n'existe pas.
 
+admin_cannot_delete_self=Vous ne pouvez pas vous supprimer vous-même lorsque vous êtes admin. Veuillez d’abord supprimer vos privilèges d’administrateur.
 
 [user]
 change_avatar=Changer votre avatar…
@@ -794,7 +797,7 @@ valid_until_date=Valable jusqu'au %s
 valid_forever=Valide pour toujours
 last_used=Dernière utilisation le
 no_activity=Aucune activité récente
-can_read_info=Lue(s)
+can_read_info=Lecture
 can_write_info=Écriture
 key_state_desc=Cette clé a été utilisée au cours des 7 derniers jours
 token_state_desc=Ce jeton a été utilisé au cours des 7 derniers jours
@@ -827,7 +830,7 @@ permissions_public_only=Publique uniquement
 permissions_access_all=Tout (public, privé et limité)
 select_permissions=Sélectionner les autorisations
 permission_no_access=Aucun accès
-permission_read=Lue(s)
+permission_read=Lecture
 permission_write=Lecture et écriture
 access_token_desc=Les autorisations des jetons sélectionnées se limitent aux <a %s>routes API</a> correspondantes. Lisez la <a %s>documentation</a> pour plus d’informations.
 at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton.
@@ -984,6 +987,7 @@ mirror_prune=Purger
 mirror_prune_desc=Supprimer les références externes obsolètes
 mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s)
 mirror_interval_invalid=L'intervalle de synchronisation est invalide.
+mirror_sync=synchronisé
 mirror_sync_on_commit=Synchroniser quand les révisions sont soumis
 mirror_address=Cloner depuis une URL
 mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation.
@@ -1034,6 +1038,7 @@ desc.public=Publique
 desc.template=Modèle
 desc.internal=Interne
 desc.archived=Archivé
+desc.sha256=SHA256
 
 template.items=Élément du modèle
 template.git_content=Contenu Git (branche par défaut)
@@ -1184,6 +1189,8 @@ audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « au
 stored_lfs=Stocké avec Git LFS
 symbolic_link=Lien symbolique
 executable_file=Fichiers exécutables
+vendored=Externe
+generated=Générée
 commit_graph=Graphe des révisions
 commit_graph.select=Sélectionner les branches
 commit_graph.hide_pr_refs=Masquer les demandes d'ajout
@@ -1765,6 +1772,7 @@ pulls.merge_pull_request=Créer une révision de fusion
 pulls.rebase_merge_pull_request=Rebaser puis avancer rapidement
 pulls.rebase_merge_commit_pull_request=Rebaser puis créer une révision de fusion
 pulls.squash_merge_pull_request=Créer une révision de concaténation
+pulls.fast_forward_only_merge_pull_request=Avance rapide uniquement
 pulls.merge_manually=Fusionner manuellement
 pulls.merge_commit_id=L'ID de la révision de fusion
 pulls.require_signed_wont_sign=La branche nécessite des révisions signées mais cette fusion ne sera pas signée
@@ -1901,6 +1909,7 @@ wiki.page_name_desc=Entrez un nom pour cette page Wiki. Certains noms spéciaux
 wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial.
 
 activity=Activité
+activity.navbar.contributors=Contributeurs
 activity.period.filter_label=Période :
 activity.period.daily=1 jour
 activity.period.halfweekly=3 jours
@@ -1966,7 +1975,10 @@ activity.git_stats_and_deletions=et
 activity.git_stats_deletion_1=%d suppression
 activity.git_stats_deletion_n=%d suppressions
 
+contributors.contribution_type.filter_label=Type de contribution :
 contributors.contribution_type.commits=Révisions
+contributors.contribution_type.additions=Ajouts
+contributors.contribution_type.deletions=Suppressions
 
 search=Chercher
 search.search_repo=Rechercher dans le dépôt
@@ -2314,6 +2326,8 @@ settings.protect_approvals_whitelist_users=Évaluateurs autorisés :
 settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés :
 settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées
 settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande d’ajout, les approbations existantes sont révoquées.
+settings.ignore_stale_approvals=Ignorer les approbations obsolètes
+settings.ignore_stale_approvals_desc=Ignorer les approbations d’anciennes révisions (évaluations obsolètes) du décompte des approbations de la demande d’ajout. Non pertinent quand les évaluations obsolètes sont déjà révoquées.
 settings.require_signed_commits=Exiger des révisions signées
 settings.require_signed_commits_desc=Rejeter les soumissions sur cette branche lorsqu'ils ne sont pas signés ou vérifiables.
 settings.protect_branch_name_pattern=Motif de nom de branche protégé
@@ -2369,6 +2383,7 @@ settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt.
 settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir.
 settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé.
 settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé.
+settings.archive.mirrors_unavailable=Les miroirs ne sont pas disponibles lorsque le dépôt est archivé.
 settings.unarchive.button=Réhabiliter
 settings.unarchive.header=Réhabiliter ce dépôt
 settings.unarchive.text=Réhabiliter un dépôt dégèle les actions de révisions et de soumissions, la gestion des tickets et des demandes d'ajouts.
@@ -2568,6 +2583,11 @@ error.csv.unexpected=Impossible de visualiser ce fichier car il contient un cara
 error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.
 
 [graphs]
+component_loading=Chargement de %s…
+component_loading_failed=Impossible de charger %s.
+component_loading_info=Ça prend son temps…
+component_failed_to_load=Une erreur inattendue s’est produite.
+contributors.what=contributions
 
 [org]
 org_name_holder=Nom de l'organisation
@@ -2695,6 +2715,7 @@ teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindr
 
 [admin]
 dashboard=Tableau de bord
+self_check=Autodiagnostique
 identity_access=Identité et accès
 users=Comptes utilisateurs
 organizations=Organisations
@@ -2740,6 +2761,7 @@ dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git
 dashboard.delete_missing_repos.started=Tâche de suppression de tous les dépôts sans fichiers Git démarrée.
 dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés
 dashboard.sync_repo_branches=Synchroniser les branches manquantes depuis Git vers la base de donnée.
+dashboard.sync_repo_tags=Synchroniser les étiquettes git depuis les dépôts vers la base de données
 dashboard.update_mirrors=Actualiser les miroirs
 dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts
 dashboard.check_repo_stats=Voir les statistiques de tous les dépôts
@@ -2794,6 +2816,7 @@ dashboard.stop_endless_tasks=Arrêter les tâches sans fin
 dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés
 dashboard.start_schedule_tasks=Démarrer les tâches planifiées
 dashboard.sync_branch.started=Début de la synchronisation des branches
+dashboard.sync_tag.started=Synchronisation des étiquettes
 dashboard.rebuild_issue_indexer=Reconstruire l’indexeur des tickets
 
 users.user_manage_panel=Gestion du compte utilisateur
@@ -3220,6 +3243,12 @@ notices.desc=Description
 notices.op=Opération
 notices.delete_success=Les informations systèmes ont été supprimées.
 
+self_check.no_problem_found=Aucun problème trouvé pour l’instant.
+self_check.database_collation_mismatch=Exige que la base de données utilise la collation %s.
+self_check.database_collation_case_insensitive=La base de données utilise la collation %s, insensible à la casse. Bien que Gitea soit compatible, il peut y avoir quelques rares cas qui ne fonctionnent pas comme prévu.
+self_check.database_inconsistent_collation_columns=La base de données utilise la collation %s, mais ces colonnes utilisent des collations différentes. Cela peut causer des problèmes imprévus.
+self_check.database_fix_mysql=Pour les utilisateurs de MySQL ou MariaDB, vous pouvez utiliser la commande « gitea doctor convert » dans un terminal ou exécuter une requête du type « ALTER … COLLATE ... » pour résoudre les problèmes de collation.
+self_check.database_fix_mssql=Pour les utilisateurs de MSSQL, vous ne pouvez résoudre le problème qu’en exécutant une requête SQL du type « ALTER … COLLATE … ».
 
 [action]
 create_repo=a créé le dépôt <a href="%s">%s</a>
@@ -3407,6 +3436,7 @@ rpm.distros.suse=sur les distributions basées sur SUSE
 rpm.install=Pour installer le paquet, exécutez la commande suivante :
 rpm.repository=Informations sur le Dépôt
 rpm.repository.architectures=Architectures
+rpm.repository.multiple_groups=Ce paquet est disponible en plusieurs groupes.
 rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante :
 rubygems.install2=ou ajoutez-le au Gemfile :
 rubygems.dependencies.runtime=Dépendances d'exécution
@@ -3539,6 +3569,8 @@ runs.actors_no_select=Tous les acteurs
 runs.status_no_select=Touts les statuts
 runs.no_results=Aucun résultat correspondant.
 runs.no_workflows=Il n'y a pas encore de workflows.
+runs.no_workflows.quick_start=Vous découvrez les Actions Gitea ? Consultez <a target="_blank" rel="noopener noreferrer" href="%s">le didacticiel</a>.
+runs.no_workflows.documentation=Pour plus d’informations sur les actions Gitea, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
 runs.no_runs=Le flux de travail n'a pas encore d'exécution.
 runs.empty_commit_message=(message de révision vide)
 
@@ -3557,6 +3589,7 @@ variables.none=Il n'y a pas encore de variables.
 variables.deletion=Retirer la variable
 variables.deletion.description=La suppression d’une variable est permanente et ne peut être défaite. Continuer ?
 variables.description=Les variables sont passées aux actions et ne peuvent être lues autrement.
+variables.id_not_exist=La variable avec l’ID %d n’existe pas.
 variables.edit=Modifier la variable
 variables.deletion.failed=Impossible de retirer la variable.
 variables.deletion.success=La variable a bien été retirée.
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 96db89d810..3c3513ad48 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -1035,6 +1035,7 @@ desc.public=Publisks
 desc.template=Sagatave
 desc.internal=Iekšējs
 desc.archived=Arhivēts
+desc.sha256=SHA256
 
 template.items=Sagataves ieraksti
 template.git_content=Git saturs (noklusētais atzars)
@@ -2569,6 +2570,10 @@ error.csv.unexpected=Nevar attēlot šo failu, jo tas satur neparedzētu simbolu
 error.csv.invalid_field_count=Nevar attēlot šo failu, jo tas satur nepareizu skaitu ar laukiem %d. līnijā.
 
 [graphs]
+component_loading=Ielādē %s...
+component_loading_failed=Nevarēja ielādēt %s
+component_loading_info=Šis var aizņemt kādu brīdi…
+component_failed_to_load=Atgadījās neparedzēta kļūda.
 
 [org]
 org_name_holder=Organizācijas nosaukums
@@ -2696,6 +2701,7 @@ teams.invite.description=Nospiediet pogu zemāk, lai pievienotos komandai.
 
 [admin]
 dashboard=Infopanelis
+self_check=Pašpārbaude
 identity_access=Identitāte un piekļuve
 users=Lietotāju konti
 organizations=Organizācijas
@@ -3221,6 +3227,7 @@ notices.desc=Apraksts
 notices.op=Op.
 notices.delete_success=Sistēmas paziņojumi ir dzēsti.
 
+self_check.no_problem_found=Pašlaik nav atrasta neviena problēma.
 
 [action]
 create_repo=izveidoja repozitoriju <a href="%s">%s</a>
@@ -3558,6 +3565,7 @@ variables.none=Vēl nav neviena mainīgā.
 variables.deletion=Noņemt mainīgo
 variables.deletion.description=Mainīgā noņemšana ir neatgriezeniska un nav atsaucama. Vai turpināt?
 variables.description=Mainīgie tiks padoti noteiktām darbībām, un citādāk tos nevar nolasīt.
+variables.id_not_exist=Mainīgais ar identifikatoru %d nepastāv.
 variables.edit=Labot mainīgo
 variables.deletion.failed=Neizdevās noņemt mainīgo.
 variables.deletion.success=Mainīgais tika noņemts.

From 65952417a81631ee879d4d29ad798cbb6445fa7e Mon Sep 17 00:00:00 2001
From: qwerty287 <80460567+qwerty287@users.noreply.github.com>
Date: Mon, 26 Feb 2024 03:39:01 +0100
Subject: [PATCH 180/679] Add API to get PR by base/head (#29242)

Closes https://github.com/go-gitea/gitea/issues/16289

Add a new API `/repos/{owner}/{repo}/pulls/{base}/{head}` to get a PR by
its base and head branch.
---
 models/issues/pull.go              | 29 ++++++++++
 routers/api/v1/api.go              |  1 +
 routers/api/v1/repo/pull.go        | 85 ++++++++++++++++++++++++++++++
 templates/swagger/v1_json.tmpl     | 50 ++++++++++++++++++
 tests/integration/api_pull_test.go | 21 ++++++++
 5 files changed, 186 insertions(+)

diff --git a/models/issues/pull.go b/models/issues/pull.go
index 18e6b2776d..7d299eac27 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -652,6 +652,35 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest,
 	return pr, pr.LoadAttributes(ctx)
 }
 
+// GetPullRequestsByBaseHeadInfo returns the pull request by given base and head
+func GetPullRequestByBaseHeadInfo(ctx context.Context, baseID, headID int64, base, head string) (*PullRequest, error) {
+	pr := &PullRequest{}
+	sess := db.GetEngine(ctx).
+		Join("INNER", "issue", "issue.id = pull_request.issue_id").
+		Where("base_repo_id = ? AND base_branch = ? AND head_repo_id = ? AND head_branch = ?", baseID, base, headID, head)
+	has, err := sess.Get(pr)
+	if err != nil {
+		return nil, err
+	}
+	if !has {
+		return nil, ErrPullRequestNotExist{
+			HeadRepoID: headID,
+			BaseRepoID: baseID,
+			HeadBranch: head,
+			BaseBranch: base,
+		}
+	}
+
+	if err = pr.LoadAttributes(ctx); err != nil {
+		return nil, err
+	}
+	if err = pr.LoadIssue(ctx); err != nil {
+		return nil, err
+	}
+
+	return pr, nil
+}
+
 // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
 // By poster id.
 func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) {
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index e7bdef1489..e0c72c7ac4 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1225,6 +1225,7 @@ func Routes() *web.Route {
 							Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
 							Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
 					})
+					m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
 				}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
 				m.Group("/statuses", func() {
 					m.Combo("/{sha}").Get(repo.GetCommitStatuses).
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index eaf406e64d..85f8ec1de5 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -187,6 +187,91 @@ func GetPullRequest(ctx *context.APIContext) {
 	ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
 }
 
+// GetPullRequest returns a single PR based on index
+func GetPullRequestByBaseHead(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/pulls/{base}/{head} repository repoGetPullRequestByBaseHead
+	// ---
+	// summary: Get a pull request by base and head
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: base
+	//   in: path
+	//   description: base of the pull request to get
+	//   type: string
+	//   required: true
+	// - name: head
+	//   in: path
+	//   description: head of the pull request to get
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/PullRequest"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	var headRepoID int64
+	var headBranch string
+	head := ctx.Params("*")
+	if strings.Contains(head, ":") {
+		split := strings.SplitN(head, ":", 2)
+		headBranch = split[1]
+		var owner, name string
+		if strings.Contains(split[0], "/") {
+			split = strings.Split(split[0], "/")
+			owner = split[0]
+			name = split[1]
+		} else {
+			owner = split[0]
+			name = ctx.Repo.Repository.Name
+		}
+		repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name)
+		if err != nil {
+			if repo_model.IsErrRepoNotExist(err) {
+				ctx.NotFound()
+			} else {
+				ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerName", err)
+			}
+			return
+		}
+		headRepoID = repo.ID
+	} else {
+		headRepoID = ctx.Repo.Repository.ID
+		headBranch = head
+	}
+
+	pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.Params(":base"), headBranch)
+	if err != nil {
+		if issues_model.IsErrPullRequestNotExist(err) {
+			ctx.NotFound()
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetPullRequestByBaseHeadInfo", err)
+		}
+		return
+	}
+
+	if err = pr.LoadBaseRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
+		return
+	}
+	if err = pr.LoadHeadRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
+}
+
 // DownloadPullDiffOrPatch render a pull's raw diff or patch
 func DownloadPullDiffOrPatch(ctx *context.APIContext) {
 	// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.{diffType} repository repoDownloadPullDiffOrPatch
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index eaa1448b2b..b2bd1bf174 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -10409,6 +10409,56 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/pulls/{base}/{head}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get a pull request by base and head",
+        "operationId": "repoGetPullRequestByBaseHead",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "base of the pull request to get",
+            "name": "base",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "head of the pull request to get",
+            "name": "head",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/PullRequest"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/pulls/{index}": {
       "get": {
         "produces": [
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
index 33cc826e5e..92931c5699 100644
--- a/tests/integration/api_pull_test.go
+++ b/tests/integration/api_pull_test.go
@@ -61,6 +61,27 @@ func TestAPIViewPulls(t *testing.T) {
 	}
 }
 
+func TestAPIViewPullsByBaseHead(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+	ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
+
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch2", owner.Name, repo.Name).
+		AddTokenAuth(ctx.Token)
+	resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+	pull := &api.PullRequest{}
+	DecodeJSON(t, resp, pull)
+	assert.EqualValues(t, 3, pull.Index)
+	assert.EqualValues(t, 2, pull.ID)
+
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch-not-exist", owner.Name, repo.Name).
+		AddTokenAuth(ctx.Token)
+	ctx.Session.MakeRequest(t, req, http.StatusNotFound)
+}
+
 // TestAPIMergePullWIP ensures that we can't merge a WIP pull request
 func TestAPIMergePullWIP(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()

From 17f170ee3724d8bdf2ddaad4211b12433f78ff0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Br=C3=BCckner?= <code@nik.dev>
Date: Mon, 26 Feb 2024 04:08:21 +0000
Subject: [PATCH 181/679] Include resource state events in Gitlab downloads
 (#29382)

Some specific events on Gitlab issues and merge requests are stored
separately from comments as "resource state events". With this change,
all relevant resource state events are downloaded during issue and merge
request migration, and converted to comments.

This PR also updates the template used to render comments to add support
for migrated comments of these types.

ref: https://docs.gitlab.com/ee/api/resource_state_events.html
---
 services/migrations/gitea_uploader.go         |  6 ++
 services/migrations/gitlab.go                 | 54 ++++++++++++++++++
 .../repo/issue/view_content/comments.tmpl     | 55 +++++--------------
 .../view_content/comments_authorlink.tmpl     | 11 ++++
 4 files changed, 86 insertions(+), 40 deletions(-)
 create mode 100644 templates/repo/issue/view_content/comments_authorlink.tmpl

diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 8bcf483947..1c9824fe3a 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -483,6 +483,10 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 		}
 
 		switch cm.Type {
+		case issues_model.CommentTypeReopen:
+			cm.Content = ""
+		case issues_model.CommentTypeClose:
+			cm.Content = ""
 		case issues_model.CommentTypeAssignees:
 			if assigneeID, ok := comment.Meta["AssigneeID"].(int); ok {
 				cm.AssigneeID = int64(assigneeID)
@@ -503,6 +507,8 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 				cm.NewRef = fmt.Sprint(comment.Meta["NewRef"])
 				cm.Content = ""
 			}
+		case issues_model.CommentTypeMergePull:
+			cm.Content = ""
 		case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge:
 			cm.Content = ""
 		default:
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 5e49ae6d57..bbc44e958a 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -517,6 +517,60 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
 		}
 		page = resp.NextPage
 	}
+
+	page = 1
+	for {
+		var stateEvents []*gitlab.StateEvent
+		var resp *gitlab.Response
+		var err error
+		if context.IsMergeRequest {
+			stateEvents, resp, err = g.client.ResourceStateEvents.ListMergeStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
+				ListOptions: gitlab.ListOptions{
+					Page:    page,
+					PerPage: g.maxPerPage,
+				},
+			}, nil, gitlab.WithContext(g.ctx))
+		} else {
+			stateEvents, resp, err = g.client.ResourceStateEvents.ListIssueStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
+				ListOptions: gitlab.ListOptions{
+					Page:    page,
+					PerPage: g.maxPerPage,
+				},
+			}, nil, gitlab.WithContext(g.ctx))
+		}
+		if err != nil {
+			return nil, false, fmt.Errorf("error while listing state events: %v %w", g.repoID, err)
+		}
+
+		for _, stateEvent := range stateEvents {
+			comment := &base.Comment{
+				IssueIndex: commentable.GetLocalIndex(),
+				Index:      int64(stateEvent.ID),
+				PosterID:   int64(stateEvent.User.ID),
+				PosterName: stateEvent.User.Username,
+				Content:    "",
+				Created:    *stateEvent.CreatedAt,
+			}
+			switch stateEvent.State {
+			case gitlab.ClosedEventType:
+				comment.CommentType = issues_model.CommentTypeClose.String()
+			case gitlab.MergedEventType:
+				comment.CommentType = issues_model.CommentTypeMergePull.String()
+			case gitlab.ReopenedEventType:
+				comment.CommentType = issues_model.CommentTypeReopen.String()
+			default:
+				// Ignore other event types
+				continue
+			}
+			allComments = append(allComments, comment)
+		}
+
+		if resp.NextPage == 0 {
+			break
+		}
+		page = resp.NextPage
+	}
+
 	return allComments, true, nil
 }
 
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index b25a5ad1b4..562e44c791 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -81,9 +81,11 @@
 		{{else if eq .Type 1}}
 			<div class="timeline-item event" id="{{.HashTag}}">
 				<span class="badge gt-bg-green gt-text-white">{{svg "octicon-dot-fill"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{if .Issue.IsPull}}
 						{{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr}}
 					{{else}}
@@ -94,9 +96,11 @@
 		{{else if eq .Type 2}}
 			<div class="timeline-item event" id="{{.HashTag}}">
 				<span class="badge gt-bg-red gt-text-white">{{svg "octicon-circle-slash"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{if .Issue.IsPull}}
 						{{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr}}
 					{{else}}
@@ -107,9 +111,11 @@
 		{{else if eq .Type 28}}
 			<div class="timeline-item event" id="{{.HashTag}}">
 				<span class="badge gt-bg-purple gt-text-white">{{svg "octicon-git-merge"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}}
 					{{if eq $.Issue.PullRequest.Status 3}}
 						{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (HTMLFormat `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "<b>%[1]s</b>" $.BaseTarget) $createdStr}}
@@ -375,18 +381,7 @@
 					{{end}}
 					<span class="badge{{if eq .Review.Type 1}} gt-bg-green gt-text-white{{else if eq .Review.Type 3}} gt-bg-red gt-text-white{{end}}">{{svg (printf "octicon-%s" .Review.Type.Icon)}}</span>
 					<span class="text grey muted-links">
-						{{if .OriginalAuthor}}
-							<span class="text black">
-								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
-								{{.OriginalAuthor}}
-							</span>
-							{{if $.Repository.OriginalURL}}
-							<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
-							{{end}}
-						{{else}}
-							{{template "shared/user/authorlink" .Poster}}
-						{{end}}
-
+						{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 						{{if eq .Review.Type 1}}
 							{{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}}
 						{{else if eq .Review.Type 2}}
@@ -498,17 +493,7 @@
 					{{template "shared/user/avatarlink" dict "user" .Poster}}
 				{{end}}
 				<span class="text grey muted-links">
-					{{if .OriginalAuthor}}
-						<span class="text black">
-							{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
-							{{.OriginalAuthor}}
-						</span>
-						{{if $.Repository.OriginalURL}}
-						<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
-						{{end}}
-					{{else}}
-						{{template "shared/user/authorlink" .Poster}}
-					{{end}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" .OldRef .NewRef $createdStr}}
 				</span>
 			</div>
@@ -675,17 +660,7 @@
 			<div class="timeline-item event" id="{{.HashTag}}">
 				<span class="badge">{{svg "octicon-git-merge" 16}}</span>
 				<span class="text grey muted-links">
-					{{if .OriginalAuthor}}
-						<span class="text black">
-							{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
-							{{.OriginalAuthor}}
-						</span>
-						{{if $.Repository.OriginalURL}}
-						<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
-						{{end}}
-					{{else}}
-						{{template "shared/user/authorlink" .Poster}}
-					{{end}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr}}
 					{{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr}}{{end}}
 				</span>
diff --git a/templates/repo/issue/view_content/comments_authorlink.tmpl b/templates/repo/issue/view_content/comments_authorlink.tmpl
new file mode 100644
index 0000000000..f652a0bec3
--- /dev/null
+++ b/templates/repo/issue/view_content/comments_authorlink.tmpl
@@ -0,0 +1,11 @@
+{{if .comment.OriginalAuthor}}
+	<span class="text black">
+		{{svg (MigrationIcon .ctxData.Repository.GetOriginalURLHostname)}}
+		{{.comment.OriginalAuthor}}
+	</span>
+	{{if .ctxData.Repository.OriginalURL}}
+		<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" .ctxData.Repository.OriginalURL .ctxData.Repository.GetOriginalURLHostname}})</span>
+	{{end}}
+{{else}}
+	{{template "shared/user/authorlink" .comment.Poster}}
+{{end}}

From f8c1efe944c539396402fb014bbfdb67ba199ef2 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 26 Feb 2024 17:10:14 +0900
Subject: [PATCH 182/679] Fix logic error from #28138 (#29417)

There's a miss in #28138:

![image](https://github.com/go-gitea/gitea/assets/18380374/b1e0c5fc-0e6e-44ab-9f6e-34bc8ffbe1cc)


https://github.com/go-gitea/gitea/pull/28138/files#diff-2556e62ad7204a230c91927a3f2115e25a2b688240d0ee1de6d34f0277f37dfeR162

@lunny
Not sure about the impact of this, but it will only effect 1.22, and
maybe we should fix it ASAP.

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 routers/private/hook_post_receive.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 8b954a8130..5ae03ce294 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -159,7 +159,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 		}
 
 		// If we've pushed a branch (and not deleted it)
-		if git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
+		if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
 			// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
 			if repo == nil {
 				repo = loadRepository(ctx, ownerName, repoName)

From f8974c772560e2c957e5e218bfb348d1ee6b9448 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 26 Feb 2024 17:05:22 +0800
Subject: [PATCH 183/679] Fix incorrect tree path value for patch editor
 (#29377)

Regression of #18718. When submitting the form,
EditRepoFileForm.TreePath is marked as "Required", so the value can't be
empty. The value is not used by backend, so use a meaningful dummy value
for it.
---
 templates/repo/editor/patch.tmpl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
index c9a78cc35f..8df8758988 100644
--- a/templates/repo/editor/patch.tmpl
+++ b/templates/repo/editor/patch.tmpl
@@ -14,8 +14,8 @@
 					<div class="breadcrumb-divider">:</div>
 					<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a>
 					<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
-					<input type="hidden" id="tree_path" name="tree_path" value="" required>
-					<input id="file-name" maxlength="500" type="hidden" value="diff.patch">
+					<input type="hidden" name="tree_path" value="__dummy_for_EditRepoFileForm.TreePath(Required)__">
+					<input id="file-name" type="hidden" value="diff.patch">
 				</div>
 			</div>
 			<div class="field">

From 403766cd81697288804fd218d68c458c6aa5b73d Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 26 Feb 2024 18:38:15 +0900
Subject: [PATCH 184/679] Ignore empty repo for CreateRepository in action
 notifier (#29416)

Fix #29415
---
 services/actions/notifier_helper.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index c20335af6f..b248af1d01 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -117,6 +117,9 @@ func notify(ctx context.Context, input *notifyInput) error {
 		log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
 		return nil
 	}
+	if input.Repo.IsEmpty {
+		return nil
+	}
 	if unit_model.TypeActions.UnitGlobalDisabled() {
 		if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
 			log.Error("CleanRepoScheduleTasks: %v", err)

From 324626a11c041208b003ee64e33000b223994662 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Mon, 26 Feb 2024 14:40:41 +0200
Subject: [PATCH 185/679] Fix htmx rendering the login page in frame on session
 logout (#29405)

- Fix #29391

With this change, htmx will not follow the redirect in the AJAX request
but instead redirect the whole browser.

To reproduce the bug fixed by this change without waiting a long time
for the token to expire, you can logout in another tab then look in the
original tab. Just make sure to comment out both instances of
`window.location.href = appSubUrl` in the codebase so you won't be
redirected immediately on logout. This is what I did in the following
gifs.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 modules/context/base.go | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/modules/context/base.go b/modules/context/base.go
index fa05850a16..ddd04f4767 100644
--- a/modules/context/base.go
+++ b/modules/context/base.go
@@ -265,6 +265,14 @@ func (b *Base) Redirect(location string, status ...int) {
 		// So in this case, we should remove the session cookie from the response header
 		removeSessionCookieHeader(b.Resp)
 	}
+	// in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx
+	if b.Req.Header.Get("HX-Request") == "true" {
+		b.Resp.Header().Set("HX-Redirect", location)
+		// we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect
+		// so as to give htmx redirect logic a chance to run
+		b.Status(http.StatusNoContent)
+		return
+	}
 	http.Redirect(b.Resp, b.Req, location, code)
 }
 

From 4f70ebb68494b23c5bce93efbe4c5f80b3aaf444 Mon Sep 17 00:00:00 2001
From: delvh <dev.lh@web.de>
Date: Mon, 26 Feb 2024 23:04:44 +0100
Subject: [PATCH 186/679] Document our issue locking policy (#29433)

---
 CONTRIBUTING.md | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f9b9a421a3..dc90c6905b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,6 +8,7 @@
     - [How to report issues](#how-to-report-issues)
     - [Types of issues](#types-of-issues)
     - [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
+    - [Issue locking](#issue-locking)
   - [Building Gitea](#building-gitea)
   - [Dependencies](#dependencies)
     - [Backend](#backend)
@@ -103,6 +104,13 @@ the goals for the project and tools.
 
 Pull requests should not be the place for architecture discussions.
 
+### Issue locking
+
+Commenting on closed or merged issues/PRs is strongly discouraged.
+Such comments will likely be overlooked as some maintainers may not view notifications on closed issues, thinking that the item is resolved.
+As such, commenting on closed/merged issues/PRs may be disabled prior to the scheduled auto-locking if a discussion starts or if unrelated comments are posted.
+If further discussion is needed, we encourage you to open a new issue instead and we recommend linking to the issue/PR in question for context.
+
 ## Building Gitea
 
 See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).

From eb2fc1818b00b7ca6f8c21bb490a8e8be1e62f9a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 27 Feb 2024 06:31:30 +0800
Subject: [PATCH 187/679] Fix mail template error (#29410)

---
 modules/templates/mailer.go              | 10 ++++++++--
 templates/mail/notify/repo_transfer.tmpl |  2 +-
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go
index 54d857a8f6..04032e3982 100644
--- a/modules/templates/mailer.go
+++ b/modules/templates/mailer.go
@@ -44,11 +44,17 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
 	}
 	if _, err := stpl.New(name).
 		Parse(string(subjectContent)); err != nil {
-		log.Warn("Failed to parse template [%s/subject]: %v", name, err)
+		log.Error("Failed to parse template [%s/subject]: %v", name, err)
+		if !setting.IsProd {
+			log.Fatal("Please fix the mail template error")
+		}
 	}
 	if _, err := btpl.New(name).
 		Parse(string(bodyContent)); err != nil {
-		log.Warn("Failed to parse template [%s/body]: %v", name, err)
+		log.Error("Failed to parse template [%s/body]: %v", name, err)
+		if !setting.IsProd {
+			log.Fatal("Please fix the mail template error")
+		}
 	}
 }
 
diff --git a/templates/mail/notify/repo_transfer.tmpl b/templates/mail/notify/repo_transfer.tmpl
index 597048ddf4..8c8b276484 100644
--- a/templates/mail/notify/repo_transfer.tmpl
+++ b/templates/mail/notify/repo_transfer.tmpl
@@ -5,7 +5,7 @@
 	<title>{{.Subject}}</title>
 </head>
 
-{{$url := HTMLFormat "<a href='%[1]s'>%[2]s</a>" .Link .Repo)}}
+{{$url := HTMLFormat "<a href='%[1]s'>%[2]s</a>" .Link .Repo}}
 <body>
 	<p>{{.Subject}}.
 		{{.locale.Tr "mail.repo.transfer.body" $url}}

From e55926ebfe88d6ee079842967dc7dccc2a9cdbf2 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 27 Feb 2024 04:04:46 +0100
Subject: [PATCH 188/679] Apply tailwindcss rules with `!important` (#29437)

As per discussion in https://github.com/go-gitea/gitea/pull/29423, I
think this is the right way that does not burden developers having to
think about CSS precedence which should be irrelevant with an atomic CSS
framework.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 tailwind.config.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tailwind.config.js b/tailwind.config.js
index 8c474c33a8..7f36822001 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -6,6 +6,7 @@ const isProduction = env.NODE_ENV !== 'development';
 
 export default {
   prefix: 'tw-',
+  important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles
   content: [
     isProduction && '!./templates/devtest/**/*',
     isProduction && '!./web_src/js/standalone/devtest.js',

From 29f149bd9f517225a3c9f1ca3fb0a7b5325af696 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 27 Feb 2024 15:12:22 +0800
Subject: [PATCH 189/679] Move context from modules to services (#29440)

Since `modules/context` has to depend on `models` and many other
packages, it should be moved from `modules/context` to
`services/context` according to design principles. There is no logic
code change on this PR, only move packages.

- Move `code.gitea.io/gitea/modules/context` to
`code.gitea.io/gitea/services/context`
- Move `code.gitea.io/gitea/modules/contexttest` to
`code.gitea.io/gitea/services/contexttest` because of depending on
context
- Move `code.gitea.io/gitea/modules/upload` to
`code.gitea.io/gitea/services/context/upload` because of depending on
context
---
 routers/api/actions/artifacts.go              |  2 +-
 routers/api/packages/alpine/alpine.go         |  2 +-
 routers/api/packages/api.go                   |  7 +++----
 routers/api/packages/cargo/cargo.go           |  2 +-
 routers/api/packages/chef/chef.go             |  2 +-
 routers/api/packages/composer/composer.go     |  2 +-
 routers/api/packages/conan/conan.go           |  2 +-
 routers/api/packages/conan/search.go          |  2 +-
 routers/api/packages/conda/conda.go           |  2 +-
 routers/api/packages/container/container.go   |  2 +-
 routers/api/packages/cran/cran.go             |  2 +-
 routers/api/packages/debian/debian.go         |  2 +-
 routers/api/packages/generic/generic.go       |  2 +-
 routers/api/packages/goproxy/goproxy.go       |  2 +-
 routers/api/packages/helm/helm.go             |  2 +-
 routers/api/packages/helper/helper.go         |  2 +-
 routers/api/packages/maven/maven.go           |  2 +-
 routers/api/packages/npm/npm.go               |  2 +-
 routers/api/packages/nuget/nuget.go           |  2 +-
 routers/api/packages/pub/pub.go               |  2 +-
 routers/api/packages/pypi/pypi.go             |  2 +-
 routers/api/packages/rpm/rpm.go               |  2 +-
 routers/api/packages/rubygems/rubygems.go     |  2 +-
 routers/api/packages/swift/swift.go           |  2 +-
 routers/api/packages/vagrant/vagrant.go       |  2 +-
 routers/api/v1/activitypub/person.go          |  2 +-
 routers/api/v1/activitypub/reqsignature.go    |  2 +-
 routers/api/v1/admin/adopt.go                 |  2 +-
 routers/api/v1/admin/cron.go                  |  2 +-
 routers/api/v1/admin/email.go                 |  2 +-
 routers/api/v1/admin/hooks.go                 |  2 +-
 routers/api/v1/admin/org.go                   |  2 +-
 routers/api/v1/admin/repo.go                  |  2 +-
 routers/api/v1/admin/runners.go               |  2 +-
 routers/api/v1/admin/user.go                  |  2 +-
 routers/api/v1/api.go                         | 19 +++++++++----------
 routers/api/v1/misc/gitignore.go              |  2 +-
 routers/api/v1/misc/label_templates.go        |  2 +-
 routers/api/v1/misc/licenses.go               |  2 +-
 routers/api/v1/misc/markup.go                 |  2 +-
 routers/api/v1/misc/markup_test.go            |  2 +-
 routers/api/v1/misc/nodeinfo.go               |  2 +-
 routers/api/v1/misc/signing.go                |  2 +-
 routers/api/v1/misc/version.go                |  2 +-
 routers/api/v1/notify/notifications.go        |  2 +-
 routers/api/v1/notify/repo.go                 |  2 +-
 routers/api/v1/notify/threads.go              |  2 +-
 routers/api/v1/notify/user.go                 |  2 +-
 routers/api/v1/org/avatar.go                  |  2 +-
 routers/api/v1/org/hook.go                    |  2 +-
 routers/api/v1/org/label.go                   |  2 +-
 routers/api/v1/org/member.go                  |  2 +-
 routers/api/v1/org/org.go                     |  2 +-
 routers/api/v1/org/runners.go                 |  2 +-
 routers/api/v1/org/secrets.go                 |  2 +-
 routers/api/v1/org/team.go                    |  2 +-
 routers/api/v1/packages/package.go            |  2 +-
 routers/api/v1/repo/action.go                 |  2 +-
 routers/api/v1/repo/avatar.go                 |  2 +-
 routers/api/v1/repo/blob.go                   |  2 +-
 routers/api/v1/repo/branch.go                 |  2 +-
 routers/api/v1/repo/collaborators.go          |  2 +-
 routers/api/v1/repo/commits.go                |  2 +-
 routers/api/v1/repo/file.go                   |  2 +-
 routers/api/v1/repo/fork.go                   |  2 +-
 routers/api/v1/repo/git_hook.go               |  2 +-
 routers/api/v1/repo/git_ref.go                |  2 +-
 routers/api/v1/repo/hook.go                   |  2 +-
 routers/api/v1/repo/hook_test.go              |  2 +-
 routers/api/v1/repo/issue.go                  |  2 +-
 routers/api/v1/repo/issue_attachment.go       |  2 +-
 routers/api/v1/repo/issue_comment.go          |  2 +-
 .../api/v1/repo/issue_comment_attachment.go   |  2 +-
 routers/api/v1/repo/issue_dependency.go       |  2 +-
 routers/api/v1/repo/issue_label.go            |  2 +-
 routers/api/v1/repo/issue_pin.go              |  2 +-
 routers/api/v1/repo/issue_reaction.go         |  2 +-
 routers/api/v1/repo/issue_stopwatch.go        |  2 +-
 routers/api/v1/repo/issue_subscription.go     |  2 +-
 routers/api/v1/repo/issue_tracked_time.go     |  2 +-
 routers/api/v1/repo/key.go                    |  2 +-
 routers/api/v1/repo/label.go                  |  2 +-
 routers/api/v1/repo/language.go               |  2 +-
 routers/api/v1/repo/migrate.go                |  2 +-
 routers/api/v1/repo/milestone.go              |  2 +-
 routers/api/v1/repo/mirror.go                 |  2 +-
 routers/api/v1/repo/notes.go                  |  2 +-
 routers/api/v1/repo/patch.go                  |  2 +-
 routers/api/v1/repo/pull.go                   |  2 +-
 routers/api/v1/repo/pull_review.go            |  2 +-
 routers/api/v1/repo/release.go                |  2 +-
 routers/api/v1/repo/release_attachment.go     |  4 ++--
 routers/api/v1/repo/release_tags.go           |  2 +-
 routers/api/v1/repo/repo.go                   |  2 +-
 routers/api/v1/repo/repo_test.go              |  2 +-
 routers/api/v1/repo/runners.go                |  2 +-
 routers/api/v1/repo/star.go                   |  2 +-
 routers/api/v1/repo/status.go                 |  2 +-
 routers/api/v1/repo/subscriber.go             |  2 +-
 routers/api/v1/repo/tag.go                    |  2 +-
 routers/api/v1/repo/teams.go                  |  2 +-
 routers/api/v1/repo/topic.go                  |  2 +-
 routers/api/v1/repo/transfer.go               |  2 +-
 routers/api/v1/repo/tree.go                   |  2 +-
 routers/api/v1/repo/wiki.go                   |  2 +-
 routers/api/v1/settings/settings.go           |  2 +-
 routers/api/v1/shared/runners.go              |  2 +-
 routers/api/v1/user/action.go                 |  2 +-
 routers/api/v1/user/app.go                    |  2 +-
 routers/api/v1/user/avatar.go                 |  2 +-
 routers/api/v1/user/email.go                  |  2 +-
 routers/api/v1/user/follower.go               |  2 +-
 routers/api/v1/user/gpg_key.go                |  2 +-
 routers/api/v1/user/helper.go                 |  2 +-
 routers/api/v1/user/hook.go                   |  2 +-
 routers/api/v1/user/key.go                    |  2 +-
 routers/api/v1/user/repo.go                   |  2 +-
 routers/api/v1/user/runners.go                |  2 +-
 routers/api/v1/user/settings.go               |  2 +-
 routers/api/v1/user/star.go                   |  2 +-
 routers/api/v1/user/user.go                   |  2 +-
 routers/api/v1/user/watch.go                  |  2 +-
 routers/api/v1/utils/git.go                   |  2 +-
 routers/api/v1/utils/hook.go                  |  2 +-
 routers/api/v1/utils/page.go                  |  2 +-
 routers/common/auth.go                        |  2 +-
 routers/common/errpage.go                     |  2 +-
 routers/common/markup.go                      |  2 +-
 routers/common/middleware.go                  |  2 +-
 routers/common/serve.go                       |  2 +-
 routers/install/install.go                    |  2 +-
 routers/private/actions.go                    |  2 +-
 routers/private/default_branch.go             |  2 +-
 routers/private/hook_post_receive.go          |  2 +-
 routers/private/hook_pre_receive.go           |  2 +-
 routers/private/hook_proc_receive.go          |  2 +-
 routers/private/internal.go                   |  2 +-
 routers/private/internal_repo.go              |  2 +-
 routers/private/key.go                        |  2 +-
 routers/private/mail.go                       |  2 +-
 routers/private/manager.go                    |  2 +-
 routers/private/manager_process.go            |  2 +-
 routers/private/manager_unix.go               |  2 +-
 routers/private/manager_windows.go            |  2 +-
 routers/private/restore_repo.go               |  2 +-
 routers/private/serv.go                       |  2 +-
 routers/private/ssh_log.go                    |  2 +-
 routers/web/admin/admin.go                    |  2 +-
 routers/web/admin/applications.go             |  2 +-
 routers/web/admin/auths.go                    |  2 +-
 routers/web/admin/config.go                   |  2 +-
 routers/web/admin/diagnosis.go                |  2 +-
 routers/web/admin/emails.go                   |  2 +-
 routers/web/admin/hooks.go                    |  2 +-
 routers/web/admin/notice.go                   |  2 +-
 routers/web/admin/orgs.go                     |  2 +-
 routers/web/admin/packages.go                 |  2 +-
 routers/web/admin/queue.go                    |  2 +-
 routers/web/admin/repos.go                    |  2 +-
 routers/web/admin/runners.go                  |  2 +-
 routers/web/admin/stacktrace.go               |  2 +-
 routers/web/admin/users.go                    |  2 +-
 routers/web/admin/users_test.go               |  2 +-
 routers/web/auth/2fa.go                       |  2 +-
 routers/web/auth/auth.go                      |  2 +-
 routers/web/auth/linkaccount.go               |  2 +-
 routers/web/auth/oauth.go                     |  2 +-
 routers/web/auth/openid.go                    |  2 +-
 routers/web/auth/password.go                  |  2 +-
 routers/web/auth/webauthn.go                  |  2 +-
 routers/web/devtest/devtest.go                |  2 +-
 routers/web/events/events.go                  |  2 +-
 routers/web/explore/code.go                   |  2 +-
 routers/web/explore/org.go                    |  2 +-
 routers/web/explore/repo.go                   |  2 +-
 routers/web/explore/topic.go                  |  2 +-
 routers/web/explore/user.go                   |  2 +-
 routers/web/feed/branch.go                    |  2 +-
 routers/web/feed/convert.go                   |  2 +-
 routers/web/feed/file.go                      |  2 +-
 routers/web/feed/profile.go                   |  2 +-
 routers/web/feed/release.go                   |  2 +-
 routers/web/feed/render.go                    |  2 +-
 routers/web/feed/repo.go                      |  2 +-
 routers/web/githttp.go                        |  5 ++---
 routers/web/goget.go                          |  2 +-
 routers/web/home.go                           |  2 +-
 routers/web/misc/markup.go                    |  2 +-
 routers/web/misc/swagger.go                   |  2 +-
 routers/web/nodeinfo.go                       |  2 +-
 routers/web/org/home.go                       |  2 +-
 routers/web/org/members.go                    |  2 +-
 routers/web/org/org.go                        |  2 +-
 routers/web/org/org_labels.go                 |  2 +-
 routers/web/org/projects.go                   |  2 +-
 routers/web/org/projects_test.go              |  2 +-
 routers/web/org/setting.go                    |  2 +-
 routers/web/org/setting/runners.go            |  2 +-
 routers/web/org/setting_oauth2.go             |  2 +-
 routers/web/org/setting_packages.go           |  2 +-
 routers/web/org/teams.go                      |  2 +-
 routers/web/passkey.go                        |  2 +-
 routers/web/repo/actions/actions.go           |  2 +-
 routers/web/repo/actions/view.go              |  2 +-
 routers/web/repo/activity.go                  |  2 +-
 routers/web/repo/attachment.go                |  4 ++--
 routers/web/repo/blame.go                     |  2 +-
 routers/web/repo/branch.go                    |  2 +-
 routers/web/repo/cherry_pick.go               |  2 +-
 routers/web/repo/code_frequency.go            |  2 +-
 routers/web/repo/commit.go                    |  2 +-
 routers/web/repo/compare.go                   |  4 ++--
 routers/web/repo/contributors.go              |  2 +-
 routers/web/repo/download.go                  |  2 +-
 routers/web/repo/editor.go                    |  4 ++--
 routers/web/repo/editor_test.go               |  2 +-
 routers/web/repo/find.go                      |  2 +-
 routers/web/repo/githttp.go                   |  2 +-
 routers/web/repo/helper.go                    |  2 +-
 routers/web/repo/issue.go                     |  4 ++--
 routers/web/repo/issue_content_history.go     |  2 +-
 routers/web/repo/issue_dependency.go          |  2 +-
 routers/web/repo/issue_label.go               |  2 +-
 routers/web/repo/issue_label_test.go          |  2 +-
 routers/web/repo/issue_lock.go                |  2 +-
 routers/web/repo/issue_pin.go                 |  2 +-
 routers/web/repo/issue_stopwatch.go           |  2 +-
 routers/web/repo/issue_timetrack.go           |  2 +-
 routers/web/repo/issue_watch.go               |  2 +-
 routers/web/repo/middlewares.go               |  2 +-
 routers/web/repo/migrate.go                   |  2 +-
 routers/web/repo/milestone.go                 |  2 +-
 routers/web/repo/packages.go                  |  2 +-
 routers/web/repo/patch.go                     |  2 +-
 routers/web/repo/projects.go                  |  2 +-
 routers/web/repo/projects_test.go             |  2 +-
 routers/web/repo/pull.go                      |  4 ++--
 routers/web/repo/pull_review.go               |  4 ++--
 routers/web/repo/pull_review_test.go          |  4 ++--
 routers/web/repo/recent_commits.go            |  2 +-
 routers/web/repo/release.go                   |  4 ++--
 routers/web/repo/release_test.go              |  2 +-
 routers/web/repo/render.go                    |  2 +-
 routers/web/repo/repo.go                      |  2 +-
 routers/web/repo/search.go                    |  2 +-
 routers/web/repo/setting/avatar.go            |  2 +-
 routers/web/repo/setting/collaboration.go     |  2 +-
 routers/web/repo/setting/default_branch.go    |  2 +-
 routers/web/repo/setting/deploy_key.go        |  2 +-
 routers/web/repo/setting/git_hooks.go         |  2 +-
 routers/web/repo/setting/lfs.go               |  2 +-
 routers/web/repo/setting/protected_branch.go  |  2 +-
 routers/web/repo/setting/protected_tag.go     |  2 +-
 routers/web/repo/setting/runners.go           |  2 +-
 routers/web/repo/setting/secrets.go           |  2 +-
 routers/web/repo/setting/setting.go           |  2 +-
 routers/web/repo/setting/settings_test.go     |  4 ++--
 routers/web/repo/setting/variables.go         |  2 +-
 routers/web/repo/setting/webhook.go           |  2 +-
 routers/web/repo/topic.go                     |  2 +-
 routers/web/repo/treelist.go                  |  2 +-
 routers/web/repo/view.go                      |  2 +-
 routers/web/repo/wiki.go                      |  2 +-
 routers/web/repo/wiki_test.go                 |  2 +-
 routers/web/shared/actions/runners.go         |  2 +-
 routers/web/shared/actions/variables.go       |  2 +-
 routers/web/shared/packages/packages.go       |  2 +-
 routers/web/shared/secrets/secrets.go         |  2 +-
 routers/web/shared/user/header.go             |  2 +-
 routers/web/swagger_json.go                   |  2 +-
 routers/web/user/avatar.go                    |  2 +-
 routers/web/user/code.go                      |  2 +-
 routers/web/user/home.go                      |  9 ++++-----
 routers/web/user/home_test.go                 |  2 +-
 routers/web/user/notification.go              |  2 +-
 routers/web/user/package.go                   |  2 +-
 routers/web/user/profile.go                   |  2 +-
 routers/web/user/search.go                    |  2 +-
 routers/web/user/setting/account.go           |  2 +-
 routers/web/user/setting/account_test.go      |  2 +-
 routers/web/user/setting/adopt.go             |  2 +-
 routers/web/user/setting/applications.go      |  2 +-
 routers/web/user/setting/keys.go              |  2 +-
 routers/web/user/setting/oauth2.go            |  2 +-
 routers/web/user/setting/oauth2_common.go     |  2 +-
 routers/web/user/setting/packages.go          |  2 +-
 routers/web/user/setting/profile.go           |  2 +-
 routers/web/user/setting/runner.go            |  2 +-
 routers/web/user/setting/security/2fa.go      |  2 +-
 routers/web/user/setting/security/openid.go   |  2 +-
 routers/web/user/setting/security/security.go |  2 +-
 routers/web/user/setting/security/webauthn.go |  2 +-
 routers/web/user/setting/webhooks.go          |  2 +-
 routers/web/user/stop_watch.go                |  2 +-
 routers/web/user/task.go                      |  2 +-
 routers/web/web.go                            |  7 +++----
 routers/web/webfinger.go                      |  2 +-
 services/attachment/attachment.go             |  2 +-
 services/auth/auth.go                         |  2 +-
 services/auth/sspi.go                         |  2 +-
 {modules => services}/context/access_log.go   |  0
 {modules => services}/context/api.go          |  0
 {modules => services}/context/api_org.go      |  0
 {modules => services}/context/api_test.go     |  0
 {modules => services}/context/base.go         |  0
 {modules => services}/context/captcha.go      |  0
 {modules => services}/context/context.go      |  0
 .../context/context_cookie.go                 |  0
 .../context/context_model.go                  |  0
 .../context/context_request.go                |  0
 .../context/context_response.go               |  0
 .../context/context_template.go               |  0
 {modules => services}/context/context_test.go |  0
 {modules => services}/context/csrf.go         |  0
 {modules => services}/context/org.go          |  0
 {modules => services}/context/package.go      |  0
 {modules => services}/context/pagination.go   |  0
 {modules => services}/context/permission.go   |  0
 {modules => services}/context/private.go      |  0
 {modules => services}/context/repo.go         |  0
 {modules => services}/context/response.go     |  0
 .../context}/upload/upload.go                 |  2 +-
 .../context}/upload/upload_test.go            |  0
 services/context/user.go                      | 17 ++++++++---------
 {modules => services}/context/utils.go        |  0
 {modules => services}/context/xsrf.go         |  0
 {modules => services}/context/xsrf_test.go    |  0
 .../contexttest/context_tests.go              |  2 +-
 services/convert/git_commit.go                |  2 +-
 services/forms/admin.go                       |  2 +-
 services/forms/auth_form.go                   |  2 +-
 services/forms/org.go                         |  2 +-
 services/forms/package_form.go                |  2 +-
 services/forms/repo_branch_form.go            |  2 +-
 services/forms/repo_form.go                   |  2 +-
 services/forms/repo_tag_form.go               |  2 +-
 services/forms/runner.go                      |  2 +-
 services/forms/user_form.go                   |  2 +-
 services/forms/user_form_auth_openid.go       |  2 +-
 services/forms/user_form_hidden_comments.go   |  2 +-
 services/lfs/locks.go                         |  2 +-
 services/lfs/server.go                        |  2 +-
 services/mailer/incoming/incoming_handler.go  |  2 +-
 services/markup/processorhelper.go            |  2 +-
 services/markup/processorhelper_test.go       |  4 ++--
 services/pull/pull.go                         |  2 +-
 services/repository/archiver/archiver_test.go |  2 +-
 services/repository/commit.go                 |  2 +-
 services/repository/files/content_test.go     |  2 +-
 services/repository/files/diff_test.go        |  2 +-
 services/repository/files/file_test.go        |  2 +-
 services/repository/files/tree_test.go        |  2 +-
 .../integration/api_repo_file_create_test.go  |  2 +-
 .../integration/api_repo_file_update_test.go  |  2 +-
 .../integration/api_repo_files_change_test.go |  2 +-
 tests/integration/editor_test.go              |  2 +-
 tests/integration/git_test.go                 |  2 +-
 tests/integration/integration_test.go         |  2 +-
 tests/integration/mirror_push_test.go         |  2 +-
 tests/integration/repofiles_change_test.go    |  2 +-
 360 files changed, 369 insertions(+), 375 deletions(-)
 rename {modules => services}/context/access_log.go (100%)
 rename {modules => services}/context/api.go (100%)
 rename {modules => services}/context/api_org.go (100%)
 rename {modules => services}/context/api_test.go (100%)
 rename {modules => services}/context/base.go (100%)
 rename {modules => services}/context/captcha.go (100%)
 rename {modules => services}/context/context.go (100%)
 rename {modules => services}/context/context_cookie.go (100%)
 rename {modules => services}/context/context_model.go (100%)
 rename {modules => services}/context/context_request.go (100%)
 rename {modules => services}/context/context_response.go (100%)
 rename {modules => services}/context/context_template.go (100%)
 rename {modules => services}/context/context_test.go (100%)
 rename {modules => services}/context/csrf.go (100%)
 rename {modules => services}/context/org.go (100%)
 rename {modules => services}/context/package.go (100%)
 rename {modules => services}/context/pagination.go (100%)
 rename {modules => services}/context/permission.go (100%)
 rename {modules => services}/context/private.go (100%)
 rename {modules => services}/context/repo.go (100%)
 rename {modules => services}/context/response.go (100%)
 rename {modules => services/context}/upload/upload.go (98%)
 rename {modules => services/context}/upload/upload_test.go (100%)
 rename {modules => services}/context/utils.go (100%)
 rename {modules => services}/context/xsrf.go (100%)
 rename {modules => services}/context/xsrf_test.go (100%)
 rename {modules => services}/contexttest/context_tests.go (99%)

diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
index 9fbd3f045d..d530e9cee5 100644
--- a/routers/api/actions/artifacts.go
+++ b/routers/api/actions/artifacts.go
@@ -71,7 +71,6 @@ import (
 
 	"code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -80,6 +79,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	web_types "code.gitea.io/gitea/modules/web/types"
 	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
 )
 
 const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go
index 3fd8288c01..dae9c3dfcb 100644
--- a/routers/api/packages/alpine/alpine.go
+++ b/routers/api/packages/alpine/alpine.go
@@ -14,12 +14,12 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 )
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index d990ebb56a..5e3cbac8f9 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -10,7 +10,6 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/perm"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
@@ -36,7 +35,7 @@ import (
 	"code.gitea.io/gitea/routers/api/packages/swift"
 	"code.gitea.io/gitea/routers/api/packages/vagrant"
 	"code.gitea.io/gitea/services/auth"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
@@ -642,7 +641,7 @@ func CommonRoutes() *web.Route {
 				})
 			})
 		}, reqPackageAccess(perm.AccessModeRead))
-	}, context_service.UserAssignmentWeb(), context.PackageAssignment())
+	}, context.UserAssignmentWeb(), context.PackageAssignment())
 
 	return r
 }
@@ -812,7 +811,7 @@ func ContainerRoutes() *web.Route {
 
 			ctx.Status(http.StatusNotFound)
 		})
-	}, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
+	}, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
 
 	return r
 }
diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go
index 8f1e965c9a..d01a13d78f 100644
--- a/routers/api/packages/cargo/cargo.go
+++ b/routers/api/packages/cargo/cargo.go
@@ -12,7 +12,6 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	cargo_module "code.gitea.io/gitea/modules/packages/cargo"
@@ -20,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	packages_service "code.gitea.io/gitea/services/packages"
 	cargo_service "code.gitea.io/gitea/services/packages/cargo"
diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go
index f1e9ae12d8..720fce0a2a 100644
--- a/routers/api/packages/chef/chef.go
+++ b/routers/api/packages/chef/chef.go
@@ -15,12 +15,12 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go
index 0551093cd1..346408d261 100644
--- a/routers/api/packages/composer/composer.go
+++ b/routers/api/packages/composer/composer.go
@@ -14,12 +14,12 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	composer_module "code.gitea.io/gitea/modules/packages/composer"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	packages_service "code.gitea.io/gitea/services/packages"
 
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
index 4bf13222dc..c45e085a4d 100644
--- a/routers/api/packages/conan/conan.go
+++ b/routers/api/packages/conan/conan.go
@@ -15,13 +15,13 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	conan_model "code.gitea.io/gitea/models/packages/conan"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	conan_module "code.gitea.io/gitea/modules/packages/conan"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	notify_service "code.gitea.io/gitea/services/notify"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go
index 2bcf9df162..7370c702cd 100644
--- a/routers/api/packages/conan/search.go
+++ b/routers/api/packages/conan/search.go
@@ -9,9 +9,9 @@ import (
 
 	conan_model "code.gitea.io/gitea/models/packages/conan"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	conan_module "code.gitea.io/gitea/modules/packages/conan"
+	"code.gitea.io/gitea/services/context"
 )
 
 // SearchResult contains the found recipe names
diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go
index 0bee7baa96..30c80fc15e 100644
--- a/routers/api/packages/conda/conda.go
+++ b/routers/api/packages/conda/conda.go
@@ -12,13 +12,13 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	conda_model "code.gitea.io/gitea/models/packages/conda"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	conda_module "code.gitea.io/gitea/modules/packages/conda"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/dsnet/compress/bzip2"
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 8621242da4..e519766142 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -17,7 +17,6 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	container_model "code.gitea.io/gitea/models/packages/container"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -25,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 	container_service "code.gitea.io/gitea/services/packages/container"
 
diff --git a/routers/api/packages/cran/cran.go b/routers/api/packages/cran/cran.go
index ae43df7c9a..2cec75294f 100644
--- a/routers/api/packages/cran/cran.go
+++ b/routers/api/packages/cran/cran.go
@@ -13,11 +13,11 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	cran_model "code.gitea.io/gitea/models/packages/cran"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	cran_module "code.gitea.io/gitea/modules/packages/cran"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go
index 379137e87e..241de3ac5d 100644
--- a/routers/api/packages/debian/debian.go
+++ b/routers/api/packages/debian/debian.go
@@ -13,11 +13,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	debian_module "code.gitea.io/gitea/modules/packages/debian"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	notify_service "code.gitea.io/gitea/services/notify"
 	packages_service "code.gitea.io/gitea/services/packages"
 	debian_service "code.gitea.io/gitea/services/packages/debian"
diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go
index 30854335c0..b65870a8d0 100644
--- a/routers/api/packages/generic/generic.go
+++ b/routers/api/packages/generic/generic.go
@@ -10,10 +10,10 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go
index 18e0074ab4..9eb515d9a1 100644
--- a/routers/api/packages/goproxy/goproxy.go
+++ b/routers/api/packages/goproxy/goproxy.go
@@ -12,11 +12,11 @@ import (
 	"time"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
index a8daa69dc3..e7a346d9ca 100644
--- a/routers/api/packages/helm/helm.go
+++ b/routers/api/packages/helm/helm.go
@@ -13,7 +13,6 @@ import (
 	"time"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -21,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"gopkg.in/yaml.v3"
diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go
index aadb10376c..cdb64109ad 100644
--- a/routers/api/packages/helper/helper.go
+++ b/routers/api/packages/helper/helper.go
@@ -10,9 +10,9 @@ import (
 	"net/url"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // LogAndProcessError logs an error and calls a custom callback with the processed error message.
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 5106395eb1..27f0578db7 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -20,12 +20,12 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	maven_module "code.gitea.io/gitea/modules/packages/maven"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index 170edfbe11..72b4305928 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -17,12 +17,12 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	npm_module "code.gitea.io/gitea/modules/packages/npm"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/hashicorp/go-version"
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index 769c4c1824..a0273aad5a 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -17,13 +17,13 @@ import (
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
 	nuget_model "code.gitea.io/gitea/models/packages/nuget"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	nuget_module "code.gitea.io/gitea/modules/packages/nuget"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go
index 1f605c6c9f..f87df52a29 100644
--- a/routers/api/packages/pub/pub.go
+++ b/routers/api/packages/pub/pub.go
@@ -14,7 +14,6 @@ import (
 	"time"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -22,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 5718b1203b..7824db1823 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -12,12 +12,12 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	pypi_module "code.gitea.io/gitea/modules/packages/pypi"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
index 5d06680552..4de361c214 100644
--- a/routers/api/packages/rpm/rpm.go
+++ b/routers/api/packages/rpm/rpm.go
@@ -13,13 +13,13 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	notify_service "code.gitea.io/gitea/services/notify"
 	packages_service "code.gitea.io/gitea/services/packages"
 	rpm_service "code.gitea.io/gitea/services/packages/rpm"
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
index 01fd4dad66..5d05b6d524 100644
--- a/routers/api/packages/rubygems/rubygems.go
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -13,11 +13,11 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 6ad289e51e..1fc8baeaac 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -13,7 +13,6 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -21,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/hashicorp/go-version"
diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go
index af9cd08a62..98a81da368 100644
--- a/routers/api/packages/vagrant/vagrant.go
+++ b/routers/api/packages/vagrant/vagrant.go
@@ -12,11 +12,11 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/hashicorp/go-version"
diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go
index cad5032d10..995a148f0b 100644
--- a/routers/api/v1/activitypub/person.go
+++ b/routers/api/v1/activitypub/person.go
@@ -9,9 +9,9 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/activitypub"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 
 	ap "github.com/go-ap/activitypub"
 	"github.com/go-ap/jsonld"
diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go
index 3f60ed7776..59ebc74b89 100644
--- a/routers/api/v1/activitypub/reqsignature.go
+++ b/routers/api/v1/activitypub/reqsignature.go
@@ -13,9 +13,9 @@ import (
 	"net/url"
 
 	"code.gitea.io/gitea/modules/activitypub"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/setting"
+	gitea_context "code.gitea.io/gitea/services/context"
 
 	ap "github.com/go-ap/activitypub"
 	"github.com/go-fed/httpsig"
diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go
index bf030eb222..a4708fe032 100644
--- a/routers/api/v1/admin/adopt.go
+++ b/routers/api/v1/admin/adopt.go
@@ -8,9 +8,9 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go
index cc8c6c9e23..e1ca6048c9 100644
--- a/routers/api/v1/admin/cron.go
+++ b/routers/api/v1/admin/cron.go
@@ -6,11 +6,11 @@ package admin
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/cron"
 )
 
diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go
index 5914215bc2..ba963e9f69 100644
--- a/routers/api/v1/admin/email.go
+++ b/routers/api/v1/admin/email.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go
index 8a095a7def..2217d002a0 100644
--- a/routers/api/v1/admin/hooks.go
+++ b/routers/api/v1/admin/hooks.go
@@ -8,12 +8,12 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go
index bf68942a9c..a5c299bbf0 100644
--- a/routers/api/v1/admin/org.go
+++ b/routers/api/v1/admin/org.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go
index a4895f260b..c119d5390a 100644
--- a/routers/api/v1/admin/repo.go
+++ b/routers/api/v1/admin/repo.go
@@ -4,10 +4,10 @@
 package admin
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/repo"
+	"code.gitea.io/gitea/services/context"
 )
 
 // CreateRepo api for creating a repository
diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go
index c0d9364435..329242d9f6 100644
--- a/routers/api/v1/admin/runners.go
+++ b/routers/api/v1/admin/runners.go
@@ -4,8 +4,8 @@
 package admin
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 2ce7651a09..64315108b0 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -15,7 +15,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
@@ -25,6 +24,7 @@ import (
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/mailer"
 	user_service "code.gitea.io/gitea/services/user"
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index e0c72c7ac4..0913571c27 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -79,7 +79,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
@@ -95,7 +94,7 @@ import (
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/auth"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
@@ -855,11 +854,11 @@ func Routes() *web.Route {
 				m.Group("/user/{username}", func() {
 					m.Get("", activitypub.Person)
 					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
-				}, context_service.UserAssignmentAPI())
+				}, context.UserAssignmentAPI())
 				m.Group("/user-id/{user-id}", func() {
 					m.Get("", activitypub.Person)
 					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
-				}, context_service.UserIDAssignmentAPI())
+				}, context.UserIDAssignmentAPI())
 			}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
 		}
 
@@ -915,7 +914,7 @@ func Routes() *web.Route {
 				}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
 
 				m.Get("/activities/feeds", user.ListUserActivityFeeds)
-			}, context_service.UserAssignmentAPI(), individualPermsChecker)
+			}, context.UserAssignmentAPI(), individualPermsChecker)
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
 
 		// Users (requires user scope)
@@ -933,7 +932,7 @@ func Routes() *web.Route {
 				m.Get("/starred", user.GetStarredRepos)
 
 				m.Get("/subscriptions", user.GetWatchedRepos)
-			}, context_service.UserAssignmentAPI())
+			}, context.UserAssignmentAPI())
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 
 		// Users (requires user scope)
@@ -968,7 +967,7 @@ func Routes() *web.Route {
 					m.Get("", user.CheckMyFollowing)
 					m.Put("", user.Follow)
 					m.Delete("", user.Unfollow)
-				}, context_service.UserAssignmentAPI())
+				}, context.UserAssignmentAPI())
 			})
 
 			// (admin:public_key scope)
@@ -1415,14 +1414,14 @@ func Routes() *web.Route {
 				m.Get("/files", reqToken(), packages.ListPackageFiles)
 			})
 			m.Get("/", reqToken(), packages.ListPackages)
-		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
 
 		// Organizations
 		m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
 		m.Group("/users/{username}/orgs", func() {
 			m.Get("", reqToken(), org.ListUserOrgs)
 			m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
-		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
 		m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
 		m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
 		m.Group("/orgs/{org}", func() {
@@ -1520,7 +1519,7 @@ func Routes() *web.Route {
 					m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
 					m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
 					m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
-				}, context_service.UserAssignmentAPI())
+				}, context.UserAssignmentAPI())
 			})
 			m.Group("/emails", func() {
 				m.Get("", admin.GetAllEmails)
diff --git a/routers/api/v1/misc/gitignore.go b/routers/api/v1/misc/gitignore.go
index 7c7fe4b125..dffd771752 100644
--- a/routers/api/v1/misc/gitignore.go
+++ b/routers/api/v1/misc/gitignore.go
@@ -6,11 +6,11 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/options"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Shows a list of all Gitignore templates
diff --git a/routers/api/v1/misc/label_templates.go b/routers/api/v1/misc/label_templates.go
index 0e0ca39fc5..cc11f37626 100644
--- a/routers/api/v1/misc/label_templates.go
+++ b/routers/api/v1/misc/label_templates.go
@@ -6,9 +6,9 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/misc/licenses.go b/routers/api/v1/misc/licenses.go
index 65f63468cf..2a980f5084 100644
--- a/routers/api/v1/misc/licenses.go
+++ b/routers/api/v1/misc/licenses.go
@@ -8,12 +8,12 @@ import (
 	"net/http"
 	"net/url"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/options"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Returns a list of all License templates
diff --git a/routers/api/v1/misc/markup.go b/routers/api/v1/misc/markup.go
index 7b24b353b6..9699c79368 100644
--- a/routers/api/v1/misc/markup.go
+++ b/routers/api/v1/misc/markup.go
@@ -6,12 +6,12 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Markup render markup document to HTML
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index ec8f8f47b7..f499501c2f 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -10,11 +10,11 @@ import (
 	"strings"
 	"testing"
 
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go
index cc754f64a2..3bd80de5c1 100644
--- a/routers/api/v1/misc/nodeinfo.go
+++ b/routers/api/v1/misc/nodeinfo.go
@@ -9,9 +9,9 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go
index 2ca9813e15..24a46c1e70 100644
--- a/routers/api/v1/misc/signing.go
+++ b/routers/api/v1/misc/signing.go
@@ -7,8 +7,8 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 )
 
 // SigningKey returns the public key of the default signing key if it exists
diff --git a/routers/api/v1/misc/version.go b/routers/api/v1/misc/version.go
index 83fa35219a..e3b43a0e6b 100644
--- a/routers/api/v1/misc/version.go
+++ b/routers/api/v1/misc/version.go
@@ -6,9 +6,9 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Version shows the version of the Gitea server
diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go
index c87da9399f..46b3c7f5e7 100644
--- a/routers/api/v1/notify/notifications.go
+++ b/routers/api/v1/notify/notifications.go
@@ -9,9 +9,9 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 )
 
 // NewAvailable check if unread notifications exist
diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go
index 55ca6ad1fd..8d97e8a3f8 100644
--- a/routers/api/v1/notify/repo.go
+++ b/routers/api/v1/notify/repo.go
@@ -10,9 +10,9 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go
index 919e52952d..8e12d359cb 100644
--- a/routers/api/v1/notify/threads.go
+++ b/routers/api/v1/notify/threads.go
@@ -10,7 +10,7 @@ import (
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go
index 4abdfb2e92..879f484cce 100644
--- a/routers/api/v1/notify/user.go
+++ b/routers/api/v1/notify/user.go
@@ -9,8 +9,8 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go
index 7b621a50c3..e34c68dfc9 100644
--- a/routers/api/v1/org/avatar.go
+++ b/routers/api/v1/org/avatar.go
@@ -7,9 +7,9 @@ import (
 	"encoding/base64"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go
index 3c3f058b5d..c1dc0519ea 100644
--- a/routers/api/v1/org/hook.go
+++ b/routers/api/v1/org/hook.go
@@ -6,10 +6,10 @@ package org
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go
index 5a03059ded..b5ec54ccf4 100644
--- a/routers/api/v1/org/label.go
+++ b/routers/api/v1/org/label.go
@@ -9,11 +9,11 @@ import (
 	"strings"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go
index 422b7cecfe..fb66d4c3f5 100644
--- a/routers/api/v1/org/member.go
+++ b/routers/api/v1/org/member.go
@@ -9,11 +9,11 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/organization"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index 255e28c706..e848d95181 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -12,12 +12,12 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/org"
 	user_service "code.gitea.io/gitea/services/user"
diff --git a/routers/api/v1/org/runners.go b/routers/api/v1/org/runners.go
index 05bce8daef..2a52bd8778 100644
--- a/routers/api/v1/org/runners.go
+++ b/routers/api/v1/org/runners.go
@@ -4,8 +4,8 @@
 package org
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/org/secrets.go b/routers/api/v1/org/secrets.go
index ddc74d865b..abb6bb26c4 100644
--- a/routers/api/v1/org/secrets.go
+++ b/routers/api/v1/org/secrets.go
@@ -9,11 +9,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	secret_model "code.gitea.io/gitea/models/secret"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index f129c66230..b62a386fd7 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -15,12 +15,12 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index a79ba315be..3be31b13ae 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -7,10 +7,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index 039cdadac9..e0af276c71 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -7,10 +7,10 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go
index 1b661955f0..698337ffd2 100644
--- a/routers/api/v1/repo/avatar.go
+++ b/routers/api/v1/repo/avatar.go
@@ -7,9 +7,9 @@ import (
 	"encoding/base64"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go
index 26605bba03..3b116666ea 100644
--- a/routers/api/v1/repo/blob.go
+++ b/routers/api/v1/repo/blob.go
@@ -6,7 +6,7 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
 
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index 2cdbcd25a2..5e6b6a8658 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -14,7 +14,6 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/optional"
@@ -22,6 +21,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index a222e50a5e..7d48d71516 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -13,11 +13,11 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go
index d01cf6b8bc..d06a3b4e49 100644
--- a/routers/api/v1/repo/commits.go
+++ b/routers/api/v1/repo/commits.go
@@ -12,11 +12,11 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 317213c946..907a5b568e 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -19,7 +19,6 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/httpcache"
@@ -30,6 +29,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 	archiver_service "code.gitea.io/gitea/services/repository/archiver"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 69433bf4cc..212cc7a93b 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -14,11 +14,11 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go
index 7e471e263b..26ae84d08d 100644
--- a/routers/api/v1/repo/git_hook.go
+++ b/routers/api/v1/repo/git_hook.go
@@ -6,10 +6,10 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go
index 34d2dcfcc8..0fa58425b8 100644
--- a/routers/api/v1/repo/git_ref.go
+++ b/routers/api/v1/repo/git_ref.go
@@ -7,10 +7,10 @@ import (
 	"net/http"
 	"net/url"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetGitAllRefs get ref or an list all the refs of a repository
diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go
index 8859e3ae23..ffd2313591 100644
--- a/routers/api/v1/repo/hook.go
+++ b/routers/api/v1/repo/hook.go
@@ -11,13 +11,13 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go
index 94a71e20ad..37cf61c1ed 100644
--- a/routers/api/v1/repo/hook_test.go
+++ b/routers/api/v1/repo/hook_test.go
@@ -9,7 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/contexttest"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 0f76a4b4ff..efc1a08a05 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -18,7 +18,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
@@ -26,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 	notify_service "code.gitea.io/gitea/services/notify"
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
index 11d19b21ff..d62e23aa02 100644
--- a/routers/api/v1/repo/issue_attachment.go
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -8,12 +8,12 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 2b7a8f7ba1..763419b7a2 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -14,11 +14,11 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index 21e2f4dabd..e7436db798 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -8,12 +8,12 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go
index 62d1057cdf..a42920d4fd 100644
--- a/routers/api/v1/repo/issue_dependency.go
+++ b/routers/api/v1/repo/issue_dependency.go
@@ -11,10 +11,10 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index c2f530956e..7d9f85d2aa 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -8,9 +8,9 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go
index 61f88de34e..ff1135862b 100644
--- a/routers/api/v1/repo/issue_pin.go
+++ b/routers/api/v1/repo/issue_pin.go
@@ -7,8 +7,8 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
index c886bd71b7..799c687812 100644
--- a/routers/api/v1/repo/issue_reaction.go
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -8,10 +8,10 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go
index 52bf8b5c7b..d9054e8f77 100644
--- a/routers/api/v1/repo/issue_stopwatch.go
+++ b/routers/api/v1/repo/issue_stopwatch.go
@@ -8,8 +8,8 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go
index ece880c03e..a535172462 100644
--- a/routers/api/v1/repo/issue_subscription.go
+++ b/routers/api/v1/repo/issue_subscription.go
@@ -9,9 +9,9 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index cf03e72aa0..c640515881 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -12,10 +12,10 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go
index af48c40885..88444a2625 100644
--- a/routers/api/v1/repo/key.go
+++ b/routers/api/v1/repo/key.go
@@ -15,12 +15,12 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go
index 420d3ab5b4..b6eb51fd20 100644
--- a/routers/api/v1/repo/label.go
+++ b/routers/api/v1/repo/label.go
@@ -9,11 +9,11 @@ import (
 	"strconv"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/language.go b/routers/api/v1/repo/language.go
index 12f1761ad0..f1d5bbe45f 100644
--- a/routers/api/v1/repo/language.go
+++ b/routers/api/v1/repo/language.go
@@ -9,8 +9,8 @@ import (
 	"strconv"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 type languageResponse []*repo_model.LanguageStat
diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go
index 839fbfe8a1..2caaa130e8 100644
--- a/routers/api/v1/repo/migrate.go
+++ b/routers/api/v1/repo/migrate.go
@@ -17,7 +17,6 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
@@ -26,6 +25,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index 9c2ed16d93..d4c828fe8b 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -11,12 +11,12 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go
index 26e0be301c..864644e1ef 100644
--- a/routers/api/v1/repo/mirror.go
+++ b/routers/api/v1/repo/mirror.go
@@ -13,12 +13,12 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go
index e7e00dae41..a4a1d4eab7 100644
--- a/routers/api/v1/repo/notes.go
+++ b/routers/api/v1/repo/notes.go
@@ -7,9 +7,9 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
index 9b5635d245..0e0601b7d9 100644
--- a/routers/api/v1/repo/patch.go
+++ b/routers/api/v1/repo/patch.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models"
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/repository/files"
 )
 
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 85f8ec1de5..8f9848f71d 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -21,7 +21,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
@@ -32,6 +31,7 @@ import (
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/automerge"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/gitdiff"
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 6338651aae..5128102e61 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -12,11 +12,11 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go
index a41c5ba7d8..a47fc1cc59 100644
--- a/routers/api/v1/repo/release.go
+++ b/routers/api/v1/repo/release.go
@@ -11,10 +11,10 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	release_service "code.gitea.io/gitea/services/release"
 )
diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index c36bf12e6d..a29bce66a4 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -7,13 +7,13 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go
index 9f2098df06..fec91164a2 100644
--- a/routers/api/v1/repo/release_tags.go
+++ b/routers/api/v1/repo/release_tags.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	releaseservice "code.gitea.io/gitea/services/release"
 )
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 40de8853d8..8685d88913 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -20,7 +20,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/label"
@@ -32,6 +31,7 @@ import (
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/issue"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
index 08ba7fabac..8d6ca9e3b5 100644
--- a/routers/api/v1/repo/repo_test.go
+++ b/routers/api/v1/repo/repo_test.go
@@ -9,9 +9,9 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/api/v1/repo/runners.go b/routers/api/v1/repo/runners.go
index 0a2bbf8117..fe133b311d 100644
--- a/routers/api/v1/repo/runners.go
+++ b/routers/api/v1/repo/runners.go
@@ -4,8 +4,8 @@
 package repo
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetRegistrationToken returns the token to register repo runners
diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go
index 05227e33a0..99676de119 100644
--- a/routers/api/v1/repo/star.go
+++ b/routers/api/v1/repo/star.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go
index b4edf0608c..53711bedeb 100644
--- a/routers/api/v1/repo/status.go
+++ b/routers/api/v1/repo/status.go
@@ -9,10 +9,10 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
diff --git a/routers/api/v1/repo/subscriber.go b/routers/api/v1/repo/subscriber.go
index 05509fc443..8584182857 100644
--- a/routers/api/v1/repo/subscriber.go
+++ b/routers/api/v1/repo/subscriber.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go
index 2f19f95e66..a6908f3615 100644
--- a/routers/api/v1/repo/tag.go
+++ b/routers/api/v1/repo/tag.go
@@ -10,10 +10,10 @@ import (
 
 	"code.gitea.io/gitea/models"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	releaseservice "code.gitea.io/gitea/services/release"
 )
diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go
index 1bacc71211..0ecf3a39d8 100644
--- a/routers/api/v1/repo/teams.go
+++ b/routers/api/v1/repo/teams.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/organization"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go
index d662b9b583..1d8e675bde 100644
--- a/routers/api/v1/repo/topic.go
+++ b/routers/api/v1/repo/topic.go
@@ -8,11 +8,11 @@ import (
 	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go
index c0a40ce062..4f05c0df51 100644
--- a/routers/api/v1/repo/transfer.go
+++ b/routers/api/v1/repo/transfer.go
@@ -13,10 +13,10 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go
index f63100b6ea..353a996d5b 100644
--- a/routers/api/v1/repo/tree.go
+++ b/routers/api/v1/repo/tree.go
@@ -6,7 +6,7 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
 
diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index 4f27500496..f18ea087c4 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -10,13 +10,13 @@ import (
 	"net/url"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	notify_service "code.gitea.io/gitea/services/notify"
 	wiki_service "code.gitea.io/gitea/services/wiki"
diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go
index 02bda1309d..0ee81b96d5 100644
--- a/routers/api/v1/settings/settings.go
+++ b/routers/api/v1/settings/settings.go
@@ -6,9 +6,9 @@ package settings
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetGeneralUISettings returns instance's global settings for ui
diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go
index a342bd4b63..c850ad7866 100644
--- a/routers/api/v1/shared/runners.go
+++ b/routers/api/v1/shared/runners.go
@@ -8,8 +8,8 @@ import (
 	"net/http"
 
 	actions_model "code.gitea.io/gitea/models/actions"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // RegistrationToken is response related to registeration token
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index cbe332a779..babb8c0cf7 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -7,10 +7,10 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go
index f045fb4d5d..88e314ed31 100644
--- a/routers/api/v1/user/app.go
+++ b/routers/api/v1/user/app.go
@@ -13,10 +13,10 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go
index 1c1bb6181a..f912296228 100644
--- a/routers/api/v1/user/avatar.go
+++ b/routers/api/v1/user/avatar.go
@@ -7,9 +7,9 @@ import (
 	"encoding/base64"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go
index 3dcea9083c..33aa851a80 100644
--- a/routers/api/v1/user/email.go
+++ b/routers/api/v1/user/email.go
@@ -8,9 +8,9 @@ import (
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	user_service "code.gitea.io/gitea/services/user"
 )
diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go
index 5815ed4f0b..398c6b2567 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -8,9 +8,9 @@ import (
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index 234da5dfdc..b8438cd2aa 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -10,10 +10,10 @@ import (
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
index 392b266ebd..8b5c64e291 100644
--- a/routers/api/v1/user/helper.go
+++ b/routers/api/v1/user/helper.go
@@ -7,7 +7,7 @@ import (
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetUserByParamsName get user by name
diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go
index e87385e4a2..9d9ca5bf01 100644
--- a/routers/api/v1/user/hook.go
+++ b/routers/api/v1/user/hook.go
@@ -6,10 +6,10 @@ package user
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index dd185aa7d6..ada6759f8e 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -11,13 +11,13 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/repo"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go
index b8b2d265bf..81f8e0f3fe 100644
--- a/routers/api/v1/user/repo.go
+++ b/routers/api/v1/user/repo.go
@@ -11,9 +11,9 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go
index 51556ae0fb..899218473e 100644
--- a/routers/api/v1/user/runners.go
+++ b/routers/api/v1/user/runners.go
@@ -4,8 +4,8 @@
 package user
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go
index 062df1ca43..d0a8daaa85 100644
--- a/routers/api/v1/user/settings.go
+++ b/routers/api/v1/user/settings.go
@@ -6,10 +6,10 @@ package user
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	user_service "code.gitea.io/gitea/services/user"
 )
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index 2659789ddd..e624884db3 100644
--- a/routers/api/v1/user/star.go
+++ b/routers/api/v1/user/star.go
@@ -12,9 +12,9 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index fb8f67d072..09147cd2ae 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -9,8 +9,8 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go
index 7f531eafaa..706f4cc66b 100644
--- a/routers/api/v1/user/watch.go
+++ b/routers/api/v1/user/watch.go
@@ -11,9 +11,9 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go
index 5e80190017..4e25137817 100644
--- a/routers/api/v1/utils/git.go
+++ b/routers/api/v1/utils/git.go
@@ -8,10 +8,10 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ResolveRefOrSha resolve ref to sha if exist
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 28b21ab8db..f1abd49a7d 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -12,12 +12,12 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
diff --git a/routers/api/v1/utils/page.go b/routers/api/v1/utils/page.go
index 6910b82931..024ba7b8d9 100644
--- a/routers/api/v1/utils/page.go
+++ b/routers/api/v1/utils/page.go
@@ -5,7 +5,7 @@ package utils
 
 import (
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/common/auth.go b/routers/common/auth.go
index 8904785d51..115d65ed10 100644
--- a/routers/common/auth.go
+++ b/routers/common/auth.go
@@ -5,9 +5,9 @@ package common
 
 import (
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
 	auth_service "code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/context"
 )
 
 type AuthResult struct {
diff --git a/routers/common/errpage.go b/routers/common/errpage.go
index 923421a29c..402ca44c12 100644
--- a/routers/common/errpage.go
+++ b/routers/common/errpage.go
@@ -9,13 +9,13 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/modules/web/routing"
+	"code.gitea.io/gitea/services/context"
 )
 
 const tplStatus500 base.TplName = "status/500"
diff --git a/routers/common/markup.go b/routers/common/markup.go
index a1c2c37ac0..7819ee7227 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -9,11 +9,11 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 
 	"mvdan.cc/xurls/v2"
 )
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
index 8a39dda179..1ee4c629ad 100644
--- a/routers/common/middleware.go
+++ b/routers/common/middleware.go
@@ -9,11 +9,11 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/cache"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/modules/web/routing"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/session"
 	"github.com/chi-middleware/proxy"
diff --git a/routers/common/serve.go b/routers/common/serve.go
index 8a7f8b3332..446908db75 100644
--- a/routers/common/serve.go
+++ b/routers/common/serve.go
@@ -7,11 +7,11 @@ import (
 	"io"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ServeBlob download a git.Blob
diff --git a/routers/install/install.go b/routers/install/install.go
index decf74cecb..9c6a8849b6 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -22,7 +22,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password/hash"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
@@ -36,6 +35,7 @@ import (
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/common"
 	auth_service "code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"gitea.com/go-chi/session"
diff --git a/routers/private/actions.go b/routers/private/actions.go
index 886f23b1c2..53c2412308 100644
--- a/routers/private/actions.go
+++ b/routers/private/actions.go
@@ -12,11 +12,11 @@ import (
 	actions_model "code.gitea.io/gitea/models/actions"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GenerateActionsRunnerToken generates a new runner token for a given scope
diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go
index a23e101e9d..2e323129ef 100644
--- a/routers/private/default_branch.go
+++ b/routers/private/default_branch.go
@@ -8,9 +8,9 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/private"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 // SetDefaultBranch updates the default branch
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 5ae03ce294..4eafe3923d 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -10,7 +10,6 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
@@ -18,6 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	gitea_context "code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index ad52f35084..32ec3003e2 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -16,11 +16,11 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/web"
+	gitea_context "code.gitea.io/gitea/services/context"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
 
diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go
index 5805202bb5..cee3bbdd12 100644
--- a/routers/private/hook_proc_receive.go
+++ b/routers/private/hook_proc_receive.go
@@ -7,12 +7,12 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/agit"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 // HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
diff --git a/routers/private/internal.go b/routers/private/internal.go
index 407edebeed..ede310113c 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -8,11 +8,11 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 	chi_middleware "github.com/go-chi/chi/v5/middleware"
diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go
index 615239d479..e8ee8ba8ac 100644
--- a/routers/private/internal_repo.go
+++ b/routers/private/internal_repo.go
@@ -9,10 +9,10 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 // This file contains common functions relating to setting the Repository for the internal routes
diff --git a/routers/private/key.go b/routers/private/key.go
index 0096480d6a..5b8f238a83 100644
--- a/routers/private/key.go
+++ b/routers/private/key.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/services/context"
 )
 
 // UpdatePublicKeyInRepo update public key and deploy key updates
diff --git a/routers/private/mail.go b/routers/private/mail.go
index e5e162c880..c19ee67896 100644
--- a/routers/private/mail.go
+++ b/routers/private/mail.go
@@ -11,11 +11,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/mailer"
 )
 
diff --git a/routers/private/manager.go b/routers/private/manager.go
index 397e6fac7b..a6aa03e4ec 100644
--- a/routers/private/manager.go
+++ b/routers/private/manager.go
@@ -8,7 +8,6 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/graceful/releasereopen"
 	"code.gitea.io/gitea/modules/log"
@@ -17,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ReloadTemplates reloads all the templates
diff --git a/routers/private/manager_process.go b/routers/private/manager_process.go
index 68e4a21805..9a0298a37c 100644
--- a/routers/private/manager_process.go
+++ b/routers/private/manager_process.go
@@ -11,10 +11,10 @@ import (
 	"runtime"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	process_module "code.gitea.io/gitea/modules/process"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Processes prints out the processes
diff --git a/routers/private/manager_unix.go b/routers/private/manager_unix.go
index 09ced33b8d..0c63ebc918 100644
--- a/routers/private/manager_unix.go
+++ b/routers/private/manager_unix.go
@@ -8,8 +8,8 @@ package private
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Restart causes the server to perform a graceful restart
diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go
index bd3c3c30d0..f1b9365f52 100644
--- a/routers/private/manager_windows.go
+++ b/routers/private/manager_windows.go
@@ -8,9 +8,9 @@ package private
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/private"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Restart is not implemented for Windows based servers as they can't fork
diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go
index 7efc22a3d9..4e95d3071d 100644
--- a/routers/private/restore_repo.go
+++ b/routers/private/restore_repo.go
@@ -7,9 +7,9 @@ import (
 	"io"
 	"net/http"
 
-	myCtx "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/private"
+	myCtx "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/migrations"
 )
 
diff --git a/routers/private/serv.go b/routers/private/serv.go
index 3812ccb52b..85368a0aed 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -14,11 +14,11 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 	wiki_service "code.gitea.io/gitea/services/wiki"
 )
diff --git a/routers/private/ssh_log.go b/routers/private/ssh_log.go
index eacfa18f05..5bec632ead 100644
--- a/routers/private/ssh_log.go
+++ b/routers/private/ssh_log.go
@@ -6,11 +6,11 @@ package private
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 )
 
 // SSHLog hook to response ssh log
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index 9fbd429f74..f3f10fd1b8 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -14,13 +14,13 @@ import (
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/updatechecker"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/cron"
 	"code.gitea.io/gitea/services/forms"
 	release_service "code.gitea.io/gitea/services/release"
diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go
index b6f7bcd2a5..8583398074 100644
--- a/routers/web/admin/applications.go
+++ b/routers/web/admin/applications.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 var (
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 7fdd18dfae..ba487d1045 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -16,7 +16,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/auth/pam"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -27,6 +26,7 @@ import (
 	pam_service "code.gitea.io/gitea/services/auth/source/pam"
 	"code.gitea.io/gitea/services/auth/source/smtp"
 	"code.gitea.io/gitea/services/auth/source/sspi"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"xorm.io/xorm/convert"
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index 47f9201504..2f5f17e201 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -12,13 +12,13 @@ import (
 
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/setting/config"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/mailer"
 
 	"gitea.com/go-chi/session"
diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go
index 2d550125d5..020554a35a 100644
--- a/routers/web/admin/diagnosis.go
+++ b/routers/web/admin/diagnosis.go
@@ -9,8 +9,8 @@ import (
 	"runtime/pprof"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httplib"
+	"code.gitea.io/gitea/services/context"
 )
 
 func MonitorDiagnosis(ctx *context.Context) {
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
index 59f80035d8..4296d70aee 100644
--- a/routers/web/admin/emails.go
+++ b/routers/web/admin/emails.go
@@ -11,10 +11,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index cd8cc29cdf..8d4c66fdb2 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -8,9 +8,9 @@ import (
 
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
index e1cb578d05..36303cbc06 100644
--- a/routers/web/admin/notice.go
+++ b/routers/web/admin/notice.go
@@ -11,9 +11,9 @@ import (
 	"code.gitea.io/gitea/models/db"
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 00131c9e2f..c5454db71e 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -8,10 +8,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/web/explore"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index 35ce215be4..7c16b69a85 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -11,9 +11,9 @@ import (
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 	packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
 )
diff --git a/routers/web/admin/queue.go b/routers/web/admin/queue.go
index 18a8d7d3e6..d8c50730b1 100644
--- a/routers/web/admin/queue.go
+++ b/routers/web/admin/queue.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 	"strconv"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/queue"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 func Queues(ctx *context.Context) {
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index 45c280ef73..ddf4440167 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -12,11 +12,11 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/explore"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/admin/runners.go b/routers/web/admin/runners.go
index eaa268b4f1..d73290a8db 100644
--- a/routers/web/admin/runners.go
+++ b/routers/web/admin/runners.go
@@ -4,8 +4,8 @@
 package admin
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go
index b603fb59a2..d6def94bb4 100644
--- a/routers/web/admin/stacktrace.go
+++ b/routers/web/admin/stacktrace.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 	"runtime"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Stacktrace show admin monitor goroutines page
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index adb9799c01..cbca26eba8 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -19,7 +19,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
@@ -27,6 +26,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/explore"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 	user_service "code.gitea.io/gitea/services/user"
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
index 560ee70ea0..f6f9237858 100644
--- a/routers/web/admin/users_test.go
+++ b/routers/web/admin/users_test.go
@@ -8,10 +8,10 @@ import (
 
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go
index dc0062ebaa..f93177bf96 100644
--- a/routers/web/auth/2fa.go
+++ b/routers/web/auth/2fa.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 )
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index a30ee0ce54..fee6a89a99 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -16,7 +16,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
@@ -29,6 +28,7 @@ import (
 	"code.gitea.io/gitea/routers/utils"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 1d94e52fe3..f744a57a43 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -12,13 +12,13 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 33a4ae9192..d5ca7397f0 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -22,7 +22,6 @@ import (
 	auth_module "code.gitea.io/gitea/modules/auth"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
@@ -34,6 +33,7 @@ import (
 	auth_service "code.gitea.io/gitea/services/auth"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	user_service "code.gitea.io/gitea/services/user"
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index 29ef772b1c..2143b8096a 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -11,12 +11,12 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/openid"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 1f2d133282..c9e0386041 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -12,7 +12,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
@@ -20,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 	user_service "code.gitea.io/gitea/services/user"
diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go
index 95c8d262a5..1079f44a08 100644
--- a/routers/web/auth/webauthn.go
+++ b/routers/web/auth/webauthn.go
@@ -11,9 +11,9 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	wa "code.gitea.io/gitea/modules/auth/webauthn"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 
 	"github.com/go-webauthn/webauthn/protocol"
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 525ca9be53..dd20663f94 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -10,8 +10,8 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/context"
 )
 
 // List all devtest templates, they will be used for e2e tests for the UI components
diff --git a/routers/web/events/events.go b/routers/web/events/events.go
index 1a5a162c1a..52f20e07dc 100644
--- a/routers/web/events/events.go
+++ b/routers/web/events/events.go
@@ -7,11 +7,11 @@ import (
 	"net/http"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/routers/web/auth"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Events listens for events
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index d81884ec62..2cde8b655e 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -8,9 +8,9 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index dc1318beef..4a468482ae 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -6,9 +6,9 @@ package explore
 import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Organizations render explore organizations page
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index 0446edebe6..d5a46f6883 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index bb1be310de..95fecfe2b8 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -8,8 +8,8 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index 09d31f95ef..b67fac2fc1 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -10,12 +10,12 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index f13038ff9b..80ce2ad198 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -9,7 +9,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 1e040ed819..298fe0bb39 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -14,12 +14,12 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
index 56a9c54ddc..1ab768ff27 100644
--- a/routers/web/feed/file.go
+++ b/routers/web/feed/file.go
@@ -9,9 +9,9 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 3feca68d61..2b70aad17b 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -7,9 +7,9 @@ import (
 	"time"
 
 	activities_model "code.gitea.io/gitea/models/activities"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go
index 558c03dba7..273f47e3b4 100644
--- a/routers/web/feed/release.go
+++ b/routers/web/feed/release.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go
index 8931dae8cc..a41808c24a 100644
--- a/routers/web/feed/render.go
+++ b/routers/web/feed/render.go
@@ -4,7 +4,7 @@
 package feed
 
 import (
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 // RenderBranchFeed render format for branch or file
diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go
index 51c24510c7..bfcc3a37d6 100644
--- a/routers/web/feed/repo.go
+++ b/routers/web/feed/repo.go
@@ -8,7 +8,7 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/githttp.go b/routers/web/githttp.go
index ab74e9a333..5f1dedce76 100644
--- a/routers/web/githttp.go
+++ b/routers/web/githttp.go
@@ -6,11 +6,10 @@ package web
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/repo"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 func requireSignIn(ctx *context.Context) {
@@ -39,5 +38,5 @@ func gitHTTPRouters(m *web.Route) {
 		m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
 		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
 		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
-	}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
+	}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
 }
diff --git a/routers/web/goget.go b/routers/web/goget.go
index c5b8b6cbc0..8d5612ebfe 100644
--- a/routers/web/goget.go
+++ b/routers/web/goget.go
@@ -12,9 +12,9 @@ import (
 	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 func goGet(ctx *context.Context) {
diff --git a/routers/web/home.go b/routers/web/home.go
index 2321b00efe..555f94c983 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -12,7 +12,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
@@ -21,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/web/auth"
 	"code.gitea.io/gitea/routers/web/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index c91da9a7f1..2dbbd6fc09 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -5,10 +5,10 @@
 package misc
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Markup render markup document to HTML
diff --git a/routers/web/misc/swagger.go b/routers/web/misc/swagger.go
index 72c09a3780..5fddfa8885 100644
--- a/routers/web/misc/swagger.go
+++ b/routers/web/misc/swagger.go
@@ -7,7 +7,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 // tplSwagger swagger page template
diff --git a/routers/web/nodeinfo.go b/routers/web/nodeinfo.go
index 01b71e7086..f1cc7bf530 100644
--- a/routers/web/nodeinfo.go
+++ b/routers/web/nodeinfo.go
@@ -7,8 +7,8 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 type nodeInfoLinks struct {
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 36f543dc45..4a7378689a 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -12,7 +12,6 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
@@ -20,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 15a615c706..9a3d60e122 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
index 1e4544730e..f94dd16eae 100644
--- a/routers/web/org/org.go
+++ b/routers/web/org/org.go
@@ -12,10 +12,10 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index f78bd00274..02eae8052e 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -8,10 +8,10 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index f062127d24..338558fa23 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -17,12 +17,12 @@ import (
 	attachment_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
index 8053ab4cf9..f4ccfe1c06 100644
--- a/routers/web/org/projects_test.go
+++ b/routers/web/org/projects_test.go
@@ -7,8 +7,8 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/routers/web/org"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 47d0063f76..494ada4323 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -14,7 +14,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
@@ -22,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/web/org/setting/runners.go b/routers/web/org/setting/runners.go
index c3c771036a..fe05709237 100644
--- a/routers/web/org/setting/runners.go
+++ b/routers/web/org/setting/runners.go
@@ -4,7 +4,7 @@
 package setting
 
 import (
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go
index ca4fe09f38..7f855795d3 100644
--- a/routers/web/org/setting_oauth2.go
+++ b/routers/web/org/setting_oauth2.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go
index 796829d34e..af9836e42c 100644
--- a/routers/web/org/setting_packages.go
+++ b/routers/web/org/setting_packages.go
@@ -8,10 +8,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared "code.gitea.io/gitea/routers/web/shared/packages"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 71fe99c97c..fd7486cacd 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -20,11 +20,11 @@ import (
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	org_service "code.gitea.io/gitea/services/org"
diff --git a/routers/web/passkey.go b/routers/web/passkey.go
index 95874dfc48..0d10a69dfe 100644
--- a/routers/web/passkey.go
+++ b/routers/web/passkey.go
@@ -6,8 +6,8 @@ package web
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 type passkeyEndpointsType struct {
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 19aca26711..e784912377 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -15,11 +15,11 @@ import (
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/repo"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 
 	"github.com/nektos/act/pkg/model"
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 49387362b3..2f26e710cd 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -21,12 +21,12 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
-	context_module "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	actions_service "code.gitea.io/gitea/services/actions"
+	context_module "code.gitea.io/gitea/services/context"
 
 	"xorm.io/builder"
 )
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index af99c4ed98..6f6641cc65 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -10,7 +10,7 @@ import (
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 8c322b45e5..f0c5622aec 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -9,15 +9,15 @@ import (
 
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 7602b30d2b..b088b8387e 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -13,13 +13,13 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
 
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index c543160f42..05f06a3ceb 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -16,7 +16,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
@@ -24,6 +23,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
index 8de54d569f..088f8d889d 100644
--- a/routers/web/repo/cherry_pick.go
+++ b/routers/web/repo/cherry_pick.go
@@ -12,11 +12,11 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/repository/files"
 )
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
index 48ade655b7..c76f492da0 100644
--- a/routers/web/repo/code_frequency.go
+++ b/routers/web/repo/code_frequency.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	contributors_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 32fa973ef6..16da917d22 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -19,7 +19,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitgraph"
 	"code.gitea.io/gitea/modules/gitrepo"
@@ -27,6 +26,7 @@ import (
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/gitdiff"
 	git_service "code.gitea.io/gitea/services/repository"
 )
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 535487d5fd..b0570f97c3 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -25,7 +25,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	csv_module "code.gitea.io/gitea/modules/csv"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
@@ -35,8 +34,9 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/typesniffer"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/gitdiff"
 )
 
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
index bcfef7580a..5fda17469e 100644
--- a/routers/web/repo/contributors.go
+++ b/routers/web/repo/contributors.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	contributors_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index a9e2e2b2fa..c4a8baecca 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -9,7 +9,6 @@ import (
 	"time"
 
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/lfs"
@@ -17,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 28644fbe3d..8f3d9612ec 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -16,17 +16,17 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/typesniffer"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index c28c3ef1d6..313fcfe33a 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -7,9 +7,9 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go
index daefe59c8f..07b3722798 100644
--- a/routers/web/repo/find.go
+++ b/routers/web/repo/find.go
@@ -7,7 +7,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index 27c7f4961d..8fb6d93068 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -24,13 +24,13 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 
 	"github.com/go-chi/cors"
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index a98abe566f..5e1e116018 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -8,8 +8,8 @@ import (
 	"sort"
 
 	"code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/services/context"
 )
 
 func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 46d48c4638..65e74a2d90 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -31,7 +31,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
@@ -44,11 +43,12 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/templates/vars"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	issue_service "code.gitea.io/gitea/services/issue"
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 0939af487c..fce0eccc7b 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -11,11 +11,11 @@ import (
 
 	"code.gitea.io/gitea/models/avatars"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/sergi/go-diff/diffmatchpatch"
 )
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index 022ec3ae3e..e3b85ee638 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -8,8 +8,8 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // AddDependency adds new dependencies
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index dd3e2803b4..9dedaefa4b 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -10,12 +10,12 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 742f12114d..93fc72300b 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -10,10 +10,10 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index f83109d9b3..1d5fc8a5f3 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -5,8 +5,8 @@ package repo
 
 import (
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go
index 9f334129f9..365c812681 100644
--- a/routers/web/repo/issue_pin.go
+++ b/routers/web/repo/issue_pin.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // IssuePinOrUnpin pin or unpin a Issue
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index ab9fe3e69d..70d42b27c0 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -9,8 +9,8 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
+	"code.gitea.io/gitea/services/context"
 )
 
 // IssueStopwatch creates or stops a stopwatch for the given issue.
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index c9bf861b84..241e434049 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -9,9 +9,9 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 1f51ceba5e..8b033f3b17 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -9,8 +9,8 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
index d70a53030e..420931c5fb 100644
--- a/routers/web/repo/middlewares.go
+++ b/routers/web/repo/middlewares.go
@@ -9,9 +9,9 @@ import (
 
 	system_model "code.gitea.io/gitea/models/system"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index b70901d5f2..97b0c425ea 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -15,13 +15,13 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
 	"code.gitea.io/gitea/services/task"
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 400748b963..49ac94aaf1 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -12,13 +12,13 @@ import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/issue"
 
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index ac9e64d774..6ed5909dcf 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
index 00bd45aaec..0dee02dd9c 100644
--- a/routers/web/repo/patch.go
+++ b/routers/web/repo/patch.go
@@ -10,10 +10,10 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/repository/files"
 )
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index cc0127e7e1..1f9ee727c3 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -17,13 +17,13 @@ import (
 	attachment_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
index 6698d47028..479f8c55a2 100644
--- a/routers/web/repo/projects_test.go
+++ b/routers/web/repo/projects_test.go
@@ -7,7 +7,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index af626dad30..b1521a2112 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -27,7 +27,6 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
@@ -36,12 +35,13 @@ import (
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/automerge"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/gitdiff"
 	notify_service "code.gitea.io/gitea/services/notify"
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 92665af7e7..64212291e1 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -11,12 +11,12 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	pull_model "code.gitea.io/gitea/models/pull"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
index 8fc9cecaf3..5f035f1eb0 100644
--- a/routers/web/repo/pull_review_test.go
+++ b/routers/web/repo/pull_review_test.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/pull"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
index 3507cb8752..c158fb30b6 100644
--- a/routers/web/repo/recent_commits.go
+++ b/routers/web/repo/recent_commits.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	contributors_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index b920ffb6dd..f9ab956d4c 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -17,16 +17,16 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/feed"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	releaseservice "code.gitea.io/gitea/services/release"
 )
diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go
index c4a2c1904e..7ebea4c3fb 100644
--- a/routers/web/repo/release_test.go
+++ b/routers/web/repo/release_test.go
@@ -10,8 +10,8 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index 7eb5a42aa4..10fa21c60e 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -10,11 +10,11 @@ import (
 	"path"
 
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // RenderFile renders a file by repos path
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 323413d976..0fad8752e3 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -21,7 +21,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/cache"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
@@ -31,6 +30,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 3c0fa4bc00..c53d8fd918 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const tplSearch base.TplName = "repo/search"
diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go
index 44468d2666..504f57cfc2 100644
--- a/routers/web/repo/setting/avatar.go
+++ b/routers/web/repo/setting/avatar.go
@@ -8,11 +8,11 @@ import (
 	"fmt"
 	"io"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go
index c5c2a88c49..6bfd485566 100644
--- a/routers/web/repo/setting/collaboration.go
+++ b/routers/web/repo/setting/collaboration.go
@@ -13,10 +13,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/mailer"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go
index c8a576e576..881d148afc 100644
--- a/routers/web/repo/setting/default_branch.go
+++ b/routers/web/repo/setting/default_branch.go
@@ -7,10 +7,10 @@ import (
 	"net/http"
 
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers/web/repo"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go
index 3d4420006c..abc3eb4af1 100644
--- a/routers/web/repo/setting/deploy_key.go
+++ b/routers/web/repo/setting/deploy_key.go
@@ -8,11 +8,11 @@ import (
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go
index 551327d44b..217a01c90c 100644
--- a/routers/web/repo/setting/git_hooks.go
+++ b/routers/web/repo/setting/git_hooks.go
@@ -6,8 +6,8 @@ package setting
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GitHooks hooks of a repository
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 76a90a4ac5..32049cf0a4 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -18,7 +18,6 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/git/pipeline"
 	"code.gitea.io/gitea/modules/lfs"
@@ -28,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index 85068f0ab2..b30dc3b061 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -15,9 +15,9 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/repo"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	pull_service "code.gitea.io/gitea/services/pull"
 	"code.gitea.io/gitea/services/repository"
diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go
index 46addb3f0a..2c25b650b9 100644
--- a/routers/web/repo/setting/protected_tag.go
+++ b/routers/web/repo/setting/protected_tag.go
@@ -13,9 +13,9 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go
index 8d4112c157..a47d3b45e2 100644
--- a/routers/web/repo/setting/runners.go
+++ b/routers/web/repo/setting/runners.go
@@ -11,10 +11,10 @@ import (
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	actions_shared "code.gitea.io/gitea/routers/web/shared/actions"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go
index cf427b2c44..d4d56bfc57 100644
--- a/routers/web/repo/setting/secrets.go
+++ b/routers/web/repo/setting/secrets.go
@@ -8,10 +8,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared "code.gitea.io/gitea/routers/web/shared/secrets"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 3b11638a92..b13c4c2ddb 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -18,7 +18,6 @@ import (
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/indexer/stats"
@@ -31,6 +30,7 @@ import (
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
 	mirror_service "code.gitea.io/gitea/services/mirror"
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index 066d2ef2a9..09586cc68d 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -14,10 +14,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 	repo_service "code.gitea.io/gitea/services/repository"
 
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
index 428aa0bd5c..45b6c0f39a 100644
--- a/routers/web/repo/setting/variables.go
+++ b/routers/web/repo/setting/variables.go
@@ -8,10 +8,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared "code.gitea.io/gitea/routers/web/shared/actions"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index c12d7e82a6..bba4d4df51 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -18,7 +18,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
@@ -26,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	webhook_service "code.gitea.io/gitea/services/webhook"
diff --git a/routers/web/repo/topic.go b/routers/web/repo/topic.go
index d0e706c5bd..d81a695df9 100644
--- a/routers/web/repo/topic.go
+++ b/routers/web/repo/topic.go
@@ -8,8 +8,8 @@ import (
 	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // TopicsPost response for creating repository
diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go
index c364e7090f..d11af4669f 100644
--- a/routers/web/repo/treelist.go
+++ b/routers/web/repo/treelist.go
@@ -7,8 +7,8 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/go-enry/go-enry/v2"
 )
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 48a35dd060..e89739e2fb 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -36,7 +36,6 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/lfs"
@@ -49,6 +48,7 @@ import (
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
+	"code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
 	files_service "code.gitea.io/gitea/services/repository/files"
 
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 49e95faaba..91cf727e2c 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -18,7 +18,6 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
@@ -29,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	notify_service "code.gitea.io/gitea/services/notify"
 	wiki_service "code.gitea.io/gitea/services/wiki"
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index d3decdae2d..49c83cfef5 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -11,10 +11,10 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 	wiki_service "code.gitea.io/gitea/services/wiki"
 
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index ae9a376724..34b7969442 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -8,10 +8,10 @@ import (
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index 07a0575207..0f705399c9 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -10,9 +10,9 @@ import (
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index 30c25374d1..1454396f04 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -12,10 +12,10 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	cargo_service "code.gitea.io/gitea/services/packages/cargo"
 	container_service "code.gitea.io/gitea/services/packages/container"
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index c805da734a..73505ec372 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -6,10 +6,10 @@ package secrets
 import (
 	"code.gitea.io/gitea/models/db"
 	secret_model "code.gitea.io/gitea/models/secret"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/shared/actions"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 99b701b439..eb108268ae 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -13,12 +13,12 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // prepareContextForCommonProfile store some common data into context data for user's profile related pages (including the nav menu)
diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go
index 42e9dbe967..fc39b504a9 100644
--- a/routers/web/swagger_json.go
+++ b/routers/web/swagger_json.go
@@ -4,7 +4,7 @@
 package web
 
 import (
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 // SwaggerV1Json render swagger v1 json
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index 772cc38bea..04f510161d 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -9,8 +9,8 @@ import (
 
 	"code.gitea.io/gitea/models/avatars"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httpcache"
+	"code.gitea.io/gitea/services/context"
 )
 
 func cacheableRedirect(ctx *context.Context, location string) {
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index ee514a7cfe..eb711b76eb 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -8,10 +8,10 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index b7abbcbc00..78548e6df7 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -24,7 +24,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
@@ -32,7 +31,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
 
@@ -714,7 +713,7 @@ func UsernameSubRoute(ctx *context.Context) {
 	username := ctx.Params("username")
 	reloadParam := func(suffix string) (success bool) {
 		ctx.SetParams("username", strings.TrimSuffix(username, suffix))
-		context_service.UserAssignmentWeb()(ctx)
+		context.UserAssignmentWeb()(ctx)
 		// check view permissions
 		if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
 			ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
@@ -741,7 +740,7 @@ func UsernameSubRoute(ctx *context.Context) {
 			return
 		}
 		if reloadParam(".rss") {
-			context_service.UserAssignmentWeb()(ctx)
+			context.UserAssignmentWeb()(ctx)
 			feed.ShowUserFeedRSS(ctx)
 		}
 	case strings.HasSuffix(username, ".atom"):
@@ -753,7 +752,7 @@ func UsernameSubRoute(ctx *context.Context) {
 			feed.ShowUserFeedAtom(ctx)
 		}
 	default:
-		context_service.UserAssignmentWeb()(ctx)
+		context.UserAssignmentWeb()(ctx)
 		if !ctx.Written() {
 			ctx.Data["EnableFeed"] = setting.Other.EnableFeed
 			OwnerProfile(ctx)
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index a32b015cd1..3f5fd26689 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -10,8 +10,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 26f77cfc3a..05034f8efa 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -16,11 +16,11 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 708af3e43c..d03b28309f 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -15,7 +15,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 	debian_module "code.gitea.io/gitea/modules/packages/debian"
@@ -25,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	packages_helper "code.gitea.io/gitea/routers/api/packages/helper"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 4d0ad06cba..e7890b7c12 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -15,7 +15,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
@@ -25,6 +24,7 @@ import (
 	"code.gitea.io/gitea/routers/web/feed"
 	"code.gitea.io/gitea/routers/web/org"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index 4d090a3784..fb7729bbe1 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 659c3e29c1..23d3ca3161 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -13,13 +13,13 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 	"code.gitea.io/gitea/services/user"
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
index 6742c382e9..9fdc5e4d53 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -8,9 +8,9 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
index decb35c1e1..171c1933d4 100644
--- a/routers/web/user/setting/adopt.go
+++ b/routers/web/user/setting/adopt.go
@@ -8,9 +8,9 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index a7e31fd505..e3822ca988 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -10,9 +10,9 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 16410d06ff..0a12777e5e 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -10,10 +10,10 @@ import (
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go
index 93142c21fc..1f485e06c8 100644
--- a/routers/web/user/setting/oauth2.go
+++ b/routers/web/user/setting/oauth2.go
@@ -5,8 +5,8 @@ package setting
 
 import (
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go
index fecaa4b873..85d1e820a5 100644
--- a/routers/web/user/setting/oauth2_common.go
+++ b/routers/web/user/setting/oauth2_common.go
@@ -9,10 +9,10 @@ import (
 
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go
index 34d18f999e..4132659495 100644
--- a/routers/web/user/setting/packages.go
+++ b/routers/web/user/setting/packages.go
@@ -9,11 +9,11 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	shared "code.gitea.io/gitea/routers/web/shared/packages"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 24a807d518..49eb050dcb 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -20,7 +20,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
@@ -29,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	user_service "code.gitea.io/gitea/services/user"
 )
diff --git a/routers/web/user/setting/runner.go b/routers/web/user/setting/runner.go
index 451fd0ca97..2bb10cceb9 100644
--- a/routers/web/user/setting/runner.go
+++ b/routers/web/user/setting/runner.go
@@ -4,8 +4,8 @@
 package setting
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index 7858b634ce..cd09102369 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -13,10 +13,10 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/pquerna/otp"
diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go
index 9a207e149d..8f788e1735 100644
--- a/routers/web/user/setting/security/openid.go
+++ b/routers/web/user/setting/security/openid.go
@@ -8,10 +8,10 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/openid"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index 3647d606ee..30611dd9f1 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -12,10 +12,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go
index ce103528c5..e382c8b9af 100644
--- a/routers/web/user/setting/security/webauthn.go
+++ b/routers/web/user/setting/security/webauthn.go
@@ -11,10 +11,10 @@ import (
 
 	"code.gitea.io/gitea/models/auth"
 	wa "code.gitea.io/gitea/modules/auth/webauthn"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/go-webauthn/webauthn/protocol"
diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go
index 679b72e501..4423b62781 100644
--- a/routers/web/user/setting/webhooks.go
+++ b/routers/web/user/setting/webhooks.go
@@ -9,8 +9,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go
index 86f66e64a6..38f74ea455 100644
--- a/routers/web/user/stop_watch.go
+++ b/routers/web/user/stop_watch.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index bec68c5f20..8476767e9e 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -8,8 +8,8 @@ import (
 	"strconv"
 
 	admin_model "code.gitea.io/gitea/models/admin"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/services/context"
 )
 
 // TaskStatus returns task's status
diff --git a/routers/web/web.go b/routers/web/web.go
index b1fa5cf355..452998703a 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -12,7 +12,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/metrics"
 	"code.gitea.io/gitea/modules/public"
@@ -42,7 +41,7 @@ import (
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
 	"code.gitea.io/gitea/routers/web/user/setting/security"
 	auth_service "code.gitea.io/gitea/services/auth"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/lfs"
 
@@ -790,7 +789,7 @@ func registerRoutes(m *web.Route) {
 		m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment)
 	}, ignSignIn)
 
-	m.Post("/{username}", reqSignIn, context_service.UserAssignmentWeb(), user.Action)
+	m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.Action)
 
 	reqRepoAdmin := context.RequireRepoAdmin()
 	reqRepoCodeWriter := context.RequireRepoWriter(unit.TypeCode)
@@ -1019,7 +1018,7 @@ func registerRoutes(m *web.Route) {
 		m.Group("", func() {
 			m.Get("/code", user.CodeSearch)
 		}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker)
-	}, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
+	}, ignSignIn, context.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
 
 	m.Group("/{username}/{reponame}", func() {
 		m.Group("/settings", func() {
diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go
index faa35b8d2f..a87c426b3b 100644
--- a/routers/web/webfinger.go
+++ b/routers/web/webfinger.go
@@ -10,9 +10,9 @@ import (
 	"strings"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index 967332fd98..eab3d0b142 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -12,8 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/storage"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context/upload"
 
 	"github.com/google/uuid"
 )
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 7c07dc438e..a2523a2452 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -12,12 +12,12 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/webauthn"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/session"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web/middleware"
+	gitea_context "code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
diff --git a/services/auth/sspi.go b/services/auth/sspi.go
index 8c0fc77a96..9108a0a668 100644
--- a/services/auth/sspi.go
+++ b/services/auth/sspi.go
@@ -14,13 +14,13 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/services/auth/source/sspi"
+	gitea_context "code.gitea.io/gitea/services/context"
 
 	gouuid "github.com/google/uuid"
 )
diff --git a/modules/context/access_log.go b/services/context/access_log.go
similarity index 100%
rename from modules/context/access_log.go
rename to services/context/access_log.go
diff --git a/modules/context/api.go b/services/context/api.go
similarity index 100%
rename from modules/context/api.go
rename to services/context/api.go
diff --git a/modules/context/api_org.go b/services/context/api_org.go
similarity index 100%
rename from modules/context/api_org.go
rename to services/context/api_org.go
diff --git a/modules/context/api_test.go b/services/context/api_test.go
similarity index 100%
rename from modules/context/api_test.go
rename to services/context/api_test.go
diff --git a/modules/context/base.go b/services/context/base.go
similarity index 100%
rename from modules/context/base.go
rename to services/context/base.go
diff --git a/modules/context/captcha.go b/services/context/captcha.go
similarity index 100%
rename from modules/context/captcha.go
rename to services/context/captcha.go
diff --git a/modules/context/context.go b/services/context/context.go
similarity index 100%
rename from modules/context/context.go
rename to services/context/context.go
diff --git a/modules/context/context_cookie.go b/services/context/context_cookie.go
similarity index 100%
rename from modules/context/context_cookie.go
rename to services/context/context_cookie.go
diff --git a/modules/context/context_model.go b/services/context/context_model.go
similarity index 100%
rename from modules/context/context_model.go
rename to services/context/context_model.go
diff --git a/modules/context/context_request.go b/services/context/context_request.go
similarity index 100%
rename from modules/context/context_request.go
rename to services/context/context_request.go
diff --git a/modules/context/context_response.go b/services/context/context_response.go
similarity index 100%
rename from modules/context/context_response.go
rename to services/context/context_response.go
diff --git a/modules/context/context_template.go b/services/context/context_template.go
similarity index 100%
rename from modules/context/context_template.go
rename to services/context/context_template.go
diff --git a/modules/context/context_test.go b/services/context/context_test.go
similarity index 100%
rename from modules/context/context_test.go
rename to services/context/context_test.go
diff --git a/modules/context/csrf.go b/services/context/csrf.go
similarity index 100%
rename from modules/context/csrf.go
rename to services/context/csrf.go
diff --git a/modules/context/org.go b/services/context/org.go
similarity index 100%
rename from modules/context/org.go
rename to services/context/org.go
diff --git a/modules/context/package.go b/services/context/package.go
similarity index 100%
rename from modules/context/package.go
rename to services/context/package.go
diff --git a/modules/context/pagination.go b/services/context/pagination.go
similarity index 100%
rename from modules/context/pagination.go
rename to services/context/pagination.go
diff --git a/modules/context/permission.go b/services/context/permission.go
similarity index 100%
rename from modules/context/permission.go
rename to services/context/permission.go
diff --git a/modules/context/private.go b/services/context/private.go
similarity index 100%
rename from modules/context/private.go
rename to services/context/private.go
diff --git a/modules/context/repo.go b/services/context/repo.go
similarity index 100%
rename from modules/context/repo.go
rename to services/context/repo.go
diff --git a/modules/context/response.go b/services/context/response.go
similarity index 100%
rename from modules/context/response.go
rename to services/context/response.go
diff --git a/modules/upload/upload.go b/services/context/upload/upload.go
similarity index 98%
rename from modules/upload/upload.go
rename to services/context/upload/upload.go
index cd10715864..77a7eb9377 100644
--- a/modules/upload/upload.go
+++ b/services/context/upload/upload.go
@@ -11,9 +11,9 @@ import (
 	"regexp"
 	"strings"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ErrFileTypeForbidden not allowed file type error
diff --git a/modules/upload/upload_test.go b/services/context/upload/upload_test.go
similarity index 100%
rename from modules/upload/upload_test.go
rename to services/context/upload/upload_test.go
diff --git a/services/context/user.go b/services/context/user.go
index 8b2faf3369..4c9cd2928b 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -9,12 +9,11 @@ import (
 	"strings"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 )
 
 // UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
-func UserAssignmentWeb() func(ctx *context.Context) {
-	return func(ctx *context.Context) {
+func UserAssignmentWeb() func(ctx *Context) {
+	return func(ctx *Context) {
 		errorFn := func(status int, title string, obj any) {
 			err, ok := obj.(error)
 			if !ok {
@@ -32,8 +31,8 @@ func UserAssignmentWeb() func(ctx *context.Context) {
 }
 
 // UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes
-func UserIDAssignmentAPI() func(ctx *context.APIContext) {
-	return func(ctx *context.APIContext) {
+func UserIDAssignmentAPI() func(ctx *APIContext) {
+	return func(ctx *APIContext) {
 		userID := ctx.ParamsInt64(":user-id")
 
 		if ctx.IsSigned && ctx.Doer.ID == userID {
@@ -53,13 +52,13 @@ func UserIDAssignmentAPI() func(ctx *context.APIContext) {
 }
 
 // UserAssignmentAPI returns a middleware to handle context-user assignment for api routes
-func UserAssignmentAPI() func(ctx *context.APIContext) {
-	return func(ctx *context.APIContext) {
+func UserAssignmentAPI() func(ctx *APIContext) {
+	return func(ctx *APIContext) {
 		ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, ctx.Error)
 	}
 }
 
-func userAssignment(ctx *context.Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) {
+func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) {
 	username := ctx.Params(":username")
 
 	if doer != nil && doer.LowerName == strings.ToLower(username) {
@@ -70,7 +69,7 @@ func userAssignment(ctx *context.Base, doer *user_model.User, errCb func(int, st
 		if err != nil {
 			if user_model.IsErrUserNotExist(err) {
 				if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
-					context.RedirectToUser(ctx, username, redirectUserID)
+					RedirectToUser(ctx, username, redirectUserID)
 				} else if user_model.IsErrUserRedirectNotExist(err) {
 					errCb(http.StatusNotFound, "GetUserByName", err)
 				} else {
diff --git a/modules/context/utils.go b/services/context/utils.go
similarity index 100%
rename from modules/context/utils.go
rename to services/context/utils.go
diff --git a/modules/context/xsrf.go b/services/context/xsrf.go
similarity index 100%
rename from modules/context/xsrf.go
rename to services/context/xsrf.go
diff --git a/modules/context/xsrf_test.go b/services/context/xsrf_test.go
similarity index 100%
rename from modules/context/xsrf_test.go
rename to services/context/xsrf_test.go
diff --git a/modules/contexttest/context_tests.go b/services/contexttest/context_tests.go
similarity index 99%
rename from modules/contexttest/context_tests.go
rename to services/contexttest/context_tests.go
index c9bacf259f..8a7dd69a0f 100644
--- a/modules/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -17,11 +17,11 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/go-chi/chi/v5"
 	"github.com/stretchr/testify/assert"
diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go
index ed08691c8b..e0efcddbcb 100644
--- a/services/convert/git_commit.go
+++ b/services/convert/git_commit.go
@@ -10,11 +10,11 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	ctx "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	ctx "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/gitdiff"
 )
 
diff --git a/services/forms/admin.go b/services/forms/admin.go
index 4b3cacc606..f112013060 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -6,9 +6,9 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 25acbbb99e..c9f3182b3a 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/org.go b/services/forms/org.go
index 6e2d787516..3677fcf429 100644
--- a/services/forms/org.go
+++ b/services/forms/org.go
@@ -7,9 +7,9 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index 2f08dfe9f4..cc940d42d3 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go
index 5deb0ae463..42e6c85c37 100644
--- a/services/forms/repo_branch_form.go
+++ b/services/forms/repo_branch_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 98b8d610d0..e40bcf4eea 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -12,10 +12,10 @@ import (
 	"code.gitea.io/gitea/models"
 	issues_model "code.gitea.io/gitea/models/issues"
 	project_model "code.gitea.io/gitea/models/project"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/webhook"
 
 	"gitea.com/go-chi/binding"
diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go
index 4dd99f9e32..0135684737 100644
--- a/services/forms/repo_tag_form.go
+++ b/services/forms/repo_tag_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/runner.go b/services/forms/runner.go
index 6d16cfce49..6abfc66fc2 100644
--- a/services/forms/runner.go
+++ b/services/forms/runner.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index cbab274238..186aa4a878 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -10,11 +10,11 @@ import (
 	"strings"
 
 	auth_model "code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go
index d8137a8d13..ca1c77e320 100644
--- a/services/forms/user_form_auth_openid.go
+++ b/services/forms/user_form_auth_openid.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go
index 03e629a553..c21fddf478 100644
--- a/services/forms/user_form_hidden_comments.go
+++ b/services/forms/user_form_hidden_comments.go
@@ -7,8 +7,8 @@ import (
 	"math/big"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 type hiddenCommentTypeGroupsType map[string][]issues_model.CommentType
diff --git a/services/lfs/locks.go b/services/lfs/locks.go
index 08d7432656..2a362b1c0d 100644
--- a/services/lfs/locks.go
+++ b/services/lfs/locks.go
@@ -11,12 +11,12 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	lfs_module "code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 56714120ad..706be0d080 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -26,12 +26,12 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	lfs_module "code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/golang-jwt/jwt/v5"
 )
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
index 5ce2cd5fd5..dc0b539822 100644
--- a/services/mailer/incoming/incoming_handler.go
+++ b/services/mailer/incoming/incoming_handler.go
@@ -14,9 +14,9 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	attachment_service "code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context/upload"
 	issue_service "code.gitea.io/gitea/services/issue"
 	incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
 	"code.gitea.io/gitea/services/mailer/token"
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 3551f85c46..a4378678a0 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -7,8 +7,8 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 func ProcessorHelper() *markup.ProcessorHelper {
diff --git a/services/markup/processorhelper_test.go b/services/markup/processorhelper_test.go
index ef8f562245..170edae0e0 100644
--- a/services/markup/processorhelper_test.go
+++ b/services/markup/processorhelper_test.go
@@ -12,8 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/contexttest"
+	gitea_context "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/services/pull/pull.go b/services/pull/pull.go
index e1ea4357fc..42363f886d 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -21,7 +21,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/graceful"
@@ -31,6 +30,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sync"
 	"code.gitea.io/gitea/modules/util"
+	gitea_context "code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index 5deec259da..ec6e9dfac3 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -10,7 +10,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
+	"code.gitea.io/gitea/services/contexttest"
 
 	_ "code.gitea.io/gitea/models/actions"
 
diff --git a/services/repository/commit.go b/services/repository/commit.go
index 2497910a83..e8c0262ef4 100644
--- a/services/repository/commit.go
+++ b/services/repository/commit.go
@@ -7,8 +7,8 @@ import (
 	"context"
 	"fmt"
 
-	gitea_ctx "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
+	gitea_ctx "code.gitea.io/gitea/services/context"
 )
 
 type ContainedLinks struct { // TODO: better name?
diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go
index d50847789a..4811f9d327 100644
--- a/services/repository/files/content_test.go
+++ b/services/repository/files/content_test.go
@@ -7,9 +7,9 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/gitrepo"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 
 	_ "code.gitea.io/gitea/models/actions"
 
diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go
index 91c878e505..63aff9b0e3 100644
--- a/services/repository/files/diff_test.go
+++ b/services/repository/files/diff_test.go
@@ -8,8 +8,8 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/gitdiff"
 
 	"github.com/stretchr/testify/assert"
diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go
index 675ddbddb3..a5b3aad91e 100644
--- a/services/repository/files/file_test.go
+++ b/services/repository/files/file_test.go
@@ -7,10 +7,10 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go
index 528ef500df..508f20090d 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -7,8 +7,8 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go
index 0d192a1fe8..41ad7211ff 100644
--- a/tests/integration/api_repo_file_create_test.go
+++ b/tests/integration/api_repo_file_create_test.go
@@ -17,10 +17,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go
index 195a1090c7..ac28e0c0a2 100644
--- a/tests/integration/api_repo_file_update_test.go
+++ b/tests/integration/api_repo_file_update_test.go
@@ -16,10 +16,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go
index ab5cf19a9c..fb3ae5e4dd 100644
--- a/tests/integration/api_repo_files_change_test.go
+++ b/tests/integration/api_repo_files_change_test.go
@@ -15,10 +15,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go
index de2a9d7d23..045567ce77 100644
--- a/tests/integration/editor_test.go
+++ b/tests/integration/editor_test.go
@@ -10,8 +10,8 @@ import (
 	"path"
 	"testing"
 
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
+	gitea_context "code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go
index 95350d79ca..818e1fa653 100644
--- a/tests/integration/git_test.go
+++ b/tests/integration/git_test.go
@@ -24,12 +24,12 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	gitea_context "code.gitea.io/gitea/services/context"
 	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
 
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index 1127de1afc..f9bd352b62 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -24,7 +24,6 @@ import (
 
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/unittest"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
@@ -33,6 +32,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers"
+	gitea_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/PuerkitoBio/goquery"
diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go
index 3dc719593c..1c262b3349 100644
--- a/tests/integration/mirror_push_test.go
+++ b/tests/integration/mirror_push_test.go
@@ -15,10 +15,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
+	gitea_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/migrations"
 	mirror_service "code.gitea.io/gitea/services/mirror"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go
index 19fbd1754c..49abeb83fb 100644
--- a/tests/integration/repofiles_change_test.go
+++ b/tests/integration/repofiles_change_test.go
@@ -12,11 +12,11 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 	files_service "code.gitea.io/gitea/services/repository/files"
 
 	"github.com/stretchr/testify/assert"

From bad4ad70181c747599e206c0e7a87b57c997385d Mon Sep 17 00:00:00 2001
From: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Date: Tue, 27 Feb 2024 15:40:21 +0800
Subject: [PATCH 190/679] Not trigger all jobs any more, when re-running the
 first job (#29439)

Previously, it will be treated as "re-run all jobs" when `jobIndex ==
0`. So when you click re-run button on the first job, it triggers all
the jobs actually.
---
 routers/web/repo/actions/view.go | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 2f26e710cd..52c3cf1d07 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -12,6 +12,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 	"time"
 
@@ -262,10 +263,14 @@ func ViewPost(ctx *context_module.Context) {
 }
 
 // Rerun will rerun jobs in the given run
-// jobIndex = 0 means rerun all jobs
+// If jobIndexStr is a blank string, it means rerun all jobs
 func Rerun(ctx *context_module.Context) {
 	runIndex := ctx.ParamsInt64("run")
-	jobIndex := ctx.ParamsInt64("job")
+	jobIndexStr := ctx.Params("job")
+	var jobIndex int64
+	if jobIndexStr != "" {
+		jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
+	}
 
 	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
 	if err != nil {
@@ -297,7 +302,7 @@ func Rerun(ctx *context_module.Context) {
 		return
 	}
 
-	if jobIndex != 0 {
+	if jobIndexStr != "" {
 		jobs = []*actions_model.ActionRunJob{job}
 	}
 

From eedb8f41297c343d6073a7bab46e4df6ee297a90 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 27 Feb 2024 17:10:51 +0800
Subject: [PATCH 191/679] Only use supported sort order for "explore/users"
 page (#29430)

Thanks to inferenceus : some sort orders on the "explore/users" page
could list users by their lastlogintime/updatetime.

It leaks user's activity unintentionally. This PR makes that page only
use "supported" sort orders.

Removing the "sort orders" could also be a good solution, while IMO at
the moment keeping the "create time" and "name" orders is also fine, in
case some users would like to find a target user in the search result,
the "sort order" might help.

![image](https://github.com/go-gitea/gitea/assets/2114189/ce5c39c1-1e86-484a-80c3-33cac6419af8)
---
 models/user/search.go                  |  3 ++
 routers/web/explore/org.go             | 15 +++++++--
 routers/web/explore/user.go            | 21 ++++++++++--
 templates/explore/search.tmpl          |  2 --
 tests/integration/explore_user_test.go | 44 ++++++++++++++++++++++++++
 5 files changed, 79 insertions(+), 6 deletions(-)
 create mode 100644 tests/integration/explore_user_test.go

diff --git a/models/user/search.go b/models/user/search.go
index 0fa278c257..9484bf4425 100644
--- a/models/user/search.go
+++ b/models/user/search.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 
@@ -30,6 +31,8 @@ type SearchUserOptions struct {
 	Actor         *User // The user doing the search
 	SearchByEmail bool  // Search by email as well as username/full name
 
+	SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set
+
 	IsActive           util.OptionalBool
 	IsAdmin            util.OptionalBool
 	IsRestricted       util.OptionalBool
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index 4a468482ae..f8fd6ec38e 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -6,6 +6,7 @@ package explore
 import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/services/context"
@@ -24,8 +25,16 @@ func Organizations(ctx *context.Context) {
 		visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
 	}
 
-	if ctx.FormString("sort") == "" {
-		ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+	supportedSortOrders := container.SetOf(
+		"newest",
+		"oldest",
+		"alphabetically",
+		"reversealphabetically",
+	)
+	sortOrder := ctx.FormString("sort")
+	if sortOrder == "" {
+		sortOrder = "newest"
+		ctx.SetFormString("sort", sortOrder)
 	}
 
 	RenderUserSearch(ctx, &user_model.SearchUserOptions{
@@ -33,5 +42,7 @@ func Organizations(ctx *context.Context) {
 		Type:        user_model.UserTypeOrganization,
 		ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
 		Visible:     visibleTypes,
+
+		SupportedSortOrders: supportedSortOrders,
 	}, tplExploreUsers)
 }
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index b67fac2fc1..41f440f9d9 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
@@ -79,10 +80,16 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
 		fallthrough
 	default:
 		// in case the sortType is not valid, we set it to recentupdate
+		sortOrder = "recentupdate"
 		ctx.Data["SortType"] = "recentupdate"
 		orderBy = "`user`.updated_unix DESC"
 	}
 
+	if opts.SupportedSortOrders != nil && !opts.SupportedSortOrders.Contains(sortOrder) {
+		ctx.NotFound("unsupported sort order", nil)
+		return
+	}
+
 	opts.Keyword = ctx.FormTrim("q")
 	opts.OrderBy = orderBy
 	if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
@@ -132,8 +139,16 @@ func Users(ctx *context.Context) {
 	ctx.Data["PageIsExploreUsers"] = true
 	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
-	if ctx.FormString("sort") == "" {
-		ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+	supportedSortOrders := container.SetOf(
+		"newest",
+		"oldest",
+		"alphabetically",
+		"reversealphabetically",
+	)
+	sortOrder := ctx.FormString("sort")
+	if sortOrder == "" {
+		sortOrder = "newest"
+		ctx.SetFormString("sort", sortOrder)
 	}
 
 	RenderUserSearch(ctx, &user_model.SearchUserOptions{
@@ -142,5 +157,7 @@ func Users(ctx *context.Context) {
 		ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
 		IsActive:    util.OptionalBoolTrue,
 		Visible:     []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
+
+		SupportedSortOrders: supportedSortOrders,
 	}, tplExploreUsers)
 }
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index 74b80436dc..2bb5f319d1 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -16,8 +16,6 @@
 			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
 			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
 			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
 		</div>
 	</div>
 </div>
diff --git a/tests/integration/explore_user_test.go b/tests/integration/explore_user_test.go
new file mode 100644
index 0000000000..046caf378e
--- /dev/null
+++ b/tests/integration/explore_user_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestExploreUser(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	cases := []struct{ sortOrder, expected string }{
+		{"", "/explore/users?sort=newest&q="},
+		{"newest", "/explore/users?sort=newest&q="},
+		{"oldest", "/explore/users?sort=oldest&q="},
+		{"alphabetically", "/explore/users?sort=alphabetically&q="},
+		{"reversealphabetically", "/explore/users?sort=reversealphabetically&q="},
+	}
+	for _, c := range cases {
+		req := NewRequest(t, "GET", "/explore/users?sort="+c.sortOrder)
+		resp := MakeRequest(t, req, http.StatusOK)
+		h := NewHTMLParser(t, resp.Body)
+		href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="/explore/users"]`).Attr("href")
+		assert.Equal(t, c.expected, href)
+	}
+
+	// these sort orders shouldn't be supported, to avoid leaking user activity
+	cases404 := []string{
+		"/explore/users?sort=lastlogin",
+		"/explore/users?sort=reverselastlogin",
+		"/explore/users?sort=leastupdate",
+		"/explore/users?sort=reverseleastupdate",
+	}
+	for _, c := range cases404 {
+		req := NewRequest(t, "GET", c).SetHeader("Accept", "text/html")
+		MakeRequest(t, req, http.StatusNotFound)
+	}
+}

From 6ed74a3fc792669d2d15ab777bd7408265a67ea5 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Tue, 27 Feb 2024 17:18:35 +0800
Subject: [PATCH 192/679] Update docs about `DEFAULT_ACTIONS_URL` (#29442)

Follow #25581.
---
 docs/content/usage/actions/faq.en-us.md | 15 +++++++--------
 docs/content/usage/actions/faq.zh-cn.md | 16 ++++++++--------
 2 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/docs/content/usage/actions/faq.en-us.md b/docs/content/usage/actions/faq.en-us.md
index 7ed59e02cd..427d57c43e 100644
--- a/docs/content/usage/actions/faq.en-us.md
+++ b/docs/content/usage/actions/faq.en-us.md
@@ -45,25 +45,24 @@ It is technically possible to implement, but we need to discuss whether it is ne
 
 ## Where will the runner download scripts when using actions such as `actions/checkout@v4`?
 
-You may be aware that there are tens of thousands of [marketplace actions](https://github.com/marketplace?type=actions) in GitHub.
-However, when you write `uses: actions/checkout@v4`, it actually downloads the scripts from [gitea.com/actions/checkout](http://gitea.com/actions/checkout) by default (not GitHub).
-This is a mirror of [github.com/actions/checkout](http://github.com/actions/checkout), but it's impossible to mirror all of them.
-That's why you may encounter failures when trying to use some actions that haven't been mirrored.
+There are tens of thousands of [actions scripts](https://github.com/marketplace?type=actions) in GitHub, and when you write `uses: actions/checkout@v4`, it downloads the scripts from [github.com/actions/checkout](http://github.com/actions/checkout) by default.
+But what if you want to use actions from other places such as gitea.com instead of GitHub?
 
 The good news is that you can specify the URL prefix to use actions from anywhere.
 This is an extra syntax in Gitea Actions.
 For example:
 
-- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: https://gitea.com/xxx/xxx@xxx`
+- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: http://your_gitea_instance.com/xxx@xxx`
 
 Be careful, the `https://` or `http://` prefix is necessary!
 
-Alternatively, if you want your runners to download actions from GitHub or your own Gitea instance by default, you can configure it by setting `[actions].DEFAULT_ACTIONS_URL`.
-See [Configuration Cheat Sheet](administration/config-cheat-sheet.md#actions-actions).
+This is one of the differences from GitHub Actions which supports actions scripts only from GitHub.
+But it should allow users much more flexibility in how they run Actions.
 
-This is one of the differences from GitHub Actions, but it should allow users much more flexibility in how they run Actions.
+Alternatively, if you want your runners to download actions from your own Gitea instance by default, you can configure it by setting `[actions].DEFAULT_ACTIONS_URL`.
+See [Configuration Cheat Sheet](administration/config-cheat-sheet.md#actions-actions).
 
 ## How to limit the permission of the runners?
 
diff --git a/docs/content/usage/actions/faq.zh-cn.md b/docs/content/usage/actions/faq.zh-cn.md
index ba5f87bf0c..d6e1466801 100644
--- a/docs/content/usage/actions/faq.zh-cn.md
+++ b/docs/content/usage/actions/faq.zh-cn.md
@@ -45,25 +45,25 @@ DEFAULT_REPO_UNITS = ...,repo.actions
 
 ## 使用`actions/checkout@v4`等Actions时,Job容器会从何处下载脚本?
 
-您可能知道GitHub上有成千上万个[Actions市场](https://github.com/marketplace?type=actions)。
-然而,当您编写`uses: actions/checkout@v4`时,它实际上默认从[gitea.com/actions/checkout](http://gitea.com/actions/checkout)下载脚本(而不是从GitHub下载)。
-这是[github.com/actions/checkout](http://github.com/actions/checkout)的镜像,但无法将它们全部镜像。
-这就是为什么在尝试使用尚未镜像的某些Actions时可能会遇到失败的原因。
+GitHub 上有成千上万个 [Actions 脚本](https://github.com/marketplace?type=actions)。
+当您编写 `uses: actions/checkout@v4` 时,它默认会从 [github.com/actions/checkout](https://github.com/actions/checkout) 下载脚本。
+那如果您想使用一些托管在其它平台上的脚本呢,比如在 gitea.com 上的?
 
 好消息是,您可以指定要从任何位置使用Actions的URL前缀。
 这是Gitea Actions中的额外语法。
 例如:
 
-- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: https://gitea.com/xxx/xxx@xxx`
+- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: http://your_gitea_instance.com/xxx@xxx`
 
 注意,`https://`或`http://`前缀是必需的!
 
-另外,如果您希望您的Runner默认从GitHub或您自己的Gitea实例下载Actions,可以通过设置 `[actions].DEFAULT_ACTIONS_URL`进行配置。
-参见[配置速查表](administration/config-cheat-sheet.md#actions-actions)。
+这是与 GitHub Actions 的一个区别,GitHub Actions 只允许使用托管在 GitHub 上的 actions 脚本。
+但用户理应拥有权利去灵活决定如何运行 Actions。
 
-这是与GitHub Actions的一个区别,但它应该允许用户以更灵活的方式运行Actions。
+另外,如果您希望您的 Runner 默认从您自己的 Gitea 实例下载 Actions,可以通过设置 `[actions].DEFAULT_ACTIONS_URL`进行配置。
+参见[配置速查表](administration/config-cheat-sheet.md#actions-actions)。
 
 ## 如何限制Runner的权限?
 

From 6bdfc84e6c579861e034962acf9727bd39774f0f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 27 Feb 2024 18:55:13 +0800
Subject: [PATCH 193/679] Allow to change primary email before account
 activation (#29412)

---
 models/user/email_address.go        | 46 ++++++++++++++++++-----------
 models/user/email_address_test.go   | 27 ++++++-----------
 options/locale/locale_en-US.ini     |  3 +-
 routers/web/auth/auth.go            | 31 +++++++++++++++++--
 routers/web/user/setting/account.go |  4 +--
 templates/user/auth/activate.tmpl   |  7 +++++
 tests/integration/signup_test.go    | 16 ++++++++--
 7 files changed, 91 insertions(+), 43 deletions(-)

diff --git a/models/user/email_address.go b/models/user/email_address.go
index 216840916d..9ddd1838d5 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -21,9 +21,6 @@ import (
 	"xorm.io/builder"
 )
 
-// ErrEmailNotActivated e-mail address has not been activated error
-var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")
-
 // ErrEmailCharIsNotSupported e-mail address contains unsupported character
 type ErrEmailCharIsNotSupported struct {
 	Email string
@@ -313,27 +310,27 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
 	return UpdateUserCols(ctx, user, "rands")
 }
 
-// MakeEmailPrimary sets primary email address of given user.
-func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
-	has, err := db.GetEngine(ctx).Get(email)
-	if err != nil {
+func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error {
+	return makeEmailPrimaryInternal(ctx, emailID, true)
+}
+
+func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error {
+	return makeEmailPrimaryInternal(ctx, emailID, false)
+}
+
+func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error {
+	email := &EmailAddress{}
+	if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil {
 		return err
 	} else if !has {
-		return ErrEmailAddressNotExist{Email: email.Email}
-	}
-
-	if !email.IsActivated {
-		return ErrEmailNotActivated
+		return ErrEmailAddressNotExist{}
 	}
 
 	user := &User{}
-	has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
-	if err != nil {
+	if has, err := db.GetEngine(ctx).ID(email.UID).Get(user); err != nil {
 		return err
 	} else if !has {
-		return ErrUserNotExist{
-			UID: email.UID,
-		}
+		return ErrUserNotExist{UID: email.UID}
 	}
 
 	ctx, committer, err := db.TxContext(ctx)
@@ -365,6 +362,21 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
 	return committer.Commit()
 }
 
+// ChangeInactivePrimaryEmail replaces the inactive primary email of a given user
+func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, newEmailAddr string) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		_, err := db.GetEngine(ctx).Where(builder.Eq{"uid": uid, "lower_email": strings.ToLower(oldEmailAddr)}).Delete(&EmailAddress{})
+		if err != nil {
+			return err
+		}
+		newEmail, err := InsertEmailAddress(ctx, &EmailAddress{UID: uid, Email: newEmailAddr})
+		if err != nil {
+			return err
+		}
+		return MakeInactiveEmailPrimary(ctx, newEmail.ID)
+	})
+}
+
 // VerifyActiveEmailCode verifies active email code when active account
 func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
 	minutes := setting.Service.ActiveCodeLives
diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go
index 140443f82f..dc3073f98b 100644
--- a/models/user/email_address_test.go
+++ b/models/user/email_address_test.go
@@ -45,31 +45,22 @@ func TestIsEmailUsed(t *testing.T) {
 func TestMakeEmailPrimary(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	email := &user_model.EmailAddress{
-		Email: "user567890@example.com",
-	}
-	err := user_model.MakeEmailPrimary(db.DefaultContext, email)
+	err := user_model.MakeActiveEmailPrimary(db.DefaultContext, 9999999)
 	assert.Error(t, err)
-	assert.EqualError(t, err, user_model.ErrEmailAddressNotExist{Email: email.Email}.Error())
+	assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
 
-	email = &user_model.EmailAddress{
-		Email: "user11@example.com",
-	}
-	err = user_model.MakeEmailPrimary(db.DefaultContext, email)
+	email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
+	err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
 	assert.Error(t, err)
-	assert.EqualError(t, err, user_model.ErrEmailNotActivated.Error())
+	assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
 
-	email = &user_model.EmailAddress{
-		Email: "user9999999@example.com",
-	}
-	err = user_model.MakeEmailPrimary(db.DefaultContext, email)
+	email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
+	err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
 	assert.Error(t, err)
 	assert.True(t, user_model.IsErrUserNotExist(err))
 
-	email = &user_model.EmailAddress{
-		Email: "user101@example.com",
-	}
-	err = user_model.MakeEmailPrimary(db.DefaultContext, email)
+	email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
+	err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
 	assert.NoError(t, err)
 
 	user, _ := user_model.GetUserByID(db.DefaultContext, int64(10))
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a0ad09f776..6d4e109e1d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -368,7 +368,7 @@ forgot_password_title= Forgot Password
 forgot_password = Forgot password?
 sign_up_now = Need an account? Register now.
 sign_up_successful = Account was successfully created. Welcome!
-confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process.
+confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it.
 must_change_password = Update your password
 allow_password_change = Require user to change password (recommended)
 reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process.
@@ -378,6 +378,7 @@ prohibit_login = Sign In Prohibited
 prohibit_login_desc = Your account is prohibited from signing in, please contact your site administrator.
 resent_limit_prompt = You have already requested an activation email recently. Please wait 3 minutes and try again.
 has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below.
+change_unconfirmed_mail_address = If your registration email address is incorrect, you can change it here and resend a new confirmation email.
 resend_mail = Click here to resend your activation email
 email_not_associate = The email address is not associated with any account.
 send_reset_mail = Send Account Recovery Email
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index fee6a89a99..7704a110a6 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -646,7 +646,7 @@ func sendActivateEmail(ctx *context.Context, u *user_model.User) {
 	mailer.SendActivateAccountMail(ctx.Locale, u)
 
 	activeCodeLives := timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
-	msgHTML := ctx.Locale.Tr("auth.confirmation_mail_sent_prompt", u.Email, activeCodeLives)
+	msgHTML := ctx.Locale.Tr("auth.confirmation_mail_sent_prompt_ex", u.Email, activeCodeLives)
 	renderActivationPromptMessage(ctx, msgHTML)
 }
 
@@ -656,6 +656,10 @@ func renderActivationVerifyPassword(ctx *context.Context, code string) {
 	ctx.HTML(http.StatusOK, TplActivate)
 }
 
+func renderActivationChangeEmail(ctx *context.Context) {
+	ctx.HTML(http.StatusOK, TplActivate)
+}
+
 // Activate render activate user page
 func Activate(ctx *context.Context) {
 	code := ctx.FormString("code")
@@ -674,7 +678,7 @@ func Activate(ctx *context.Context) {
 			return
 		}
 
-		// Resend confirmation email.
+		// Resend confirmation email. FIXME: ideally this should be in a POST request
 		sendActivateEmail(ctx, ctx.Doer)
 		return
 	}
@@ -698,7 +702,28 @@ func Activate(ctx *context.Context) {
 // ActivatePost handles account activation with password check
 func ActivatePost(ctx *context.Context) {
 	code := ctx.FormString("code")
-	if code == "" || (ctx.Doer != nil && ctx.Doer.IsActive) {
+	if ctx.Doer != nil && ctx.Doer.IsActive {
+		ctx.Redirect(setting.AppSubURL + "/user/activate") // it will redirect again to the correct page
+		return
+	}
+
+	if code == "" {
+		newEmail := strings.TrimSpace(ctx.FormString("change_email"))
+		if ctx.Doer != nil && newEmail != "" && !strings.EqualFold(ctx.Doer.Email, newEmail) {
+			if user_model.ValidateEmail(newEmail) != nil {
+				ctx.Flash.Error(ctx.Locale.Tr("form.email_invalid"), true)
+				renderActivationChangeEmail(ctx)
+				return
+			}
+			err := user_model.ChangeInactivePrimaryEmail(ctx, ctx.Doer.ID, ctx.Doer.Email, newEmail)
+			if err != nil {
+				ctx.Flash.Error(ctx.Locale.Tr("admin.emails.not_updated", newEmail), true)
+				renderActivationChangeEmail(ctx)
+				return
+			}
+			ctx.Doer.Email = newEmail
+		}
+		// FIXME: at the moment, GET request handles the "send confirmation email" action. But the old code does this redirect and then send a confirmation email.
 		ctx.Redirect(setting.AppSubURL + "/user/activate")
 		return
 	}
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 23d3ca3161..abb5873e98 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -92,9 +92,9 @@ func EmailPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsSettingsAccount"] = true
 
-	// Make emailaddress primary.
+	// Make email address primary.
 	if ctx.FormString("_method") == "PRIMARY" {
-		if err := user_model.MakeEmailPrimary(ctx, &user_model.EmailAddress{ID: ctx.FormInt64("id")}); err != nil {
+		if err := user_model.MakeActiveEmailPrimary(ctx, ctx.FormInt64("id")); err != nil {
 			ctx.ServerError("MakeEmailPrimary", err)
 			return
 		}
diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl
index 51dc1eb6a6..e32a5d8707 100644
--- a/templates/user/auth/activate.tmpl
+++ b/templates/user/auth/activate.tmpl
@@ -21,6 +21,13 @@
 						<input name="code" type="hidden" value="{{.ActivationCode}}">
 					{{else}}
 						<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" .SignedUser.Name .SignedUser.Email}}</p>
+						<details>
+							<summary>{{ctx.Locale.Tr "auth.change_unconfirmed_mail_address"}}</summary>
+							<div class="tw-py-2">
+								<label for="change-email">{{ctx.Locale.Tr "email"}}</label>
+								<input id="change-email" name="change_email" type="email" value="{{.SignedUser.Email}}">
+							</div>
+						</details>
 						<div class="divider"></div>
 						<div class="text right">
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go
index fbf586f696..e9a05201ee 100644
--- a/tests/integration/signup_test.go
+++ b/tests/integration/signup_test.go
@@ -107,13 +107,25 @@ func TestSignupEmailActive(t *testing.T) {
 	resp := MakeRequest(t, req, http.StatusOK)
 	assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`)
 
-	// access "user/active" means trying to re-send the activation email
+	// access "user/activate" means trying to re-send the activation email
 	session := loginUserWithPassword(t, "test-user-1", "password1")
 	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK)
 	assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently")
 
-	// access "user/active" with a valid activation code, then get the "verify password" page
+	// access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email
+	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `)
+
+	// post to "user/activate" with a new email
+	session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	assert.Equal(t, "email-changed@example.com", user.Email)
+	email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"})
+	assert.False(t, email.IsActivated)
+	assert.True(t, email.IsPrimary)
+
+	// access "user/activate" with a valid activation code, then get the "verify password" page
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
 	activationCode := user.GenerateEmailActivateCode(user.Email)
 	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK)
 	assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)

From 0900c1552b51c5d1d883bd3662e67891a5dac80d Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 27 Feb 2024 12:51:51 +0100
Subject: [PATCH 194/679] Lock issues and pulls faster (#29436)

also point to the docs to explain why we do so

followup to  #29433
---
 .github/workflows/cron-lock.yml | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml
index 746ec49bc6..d4172687d5 100644
--- a/.github/workflows/cron-lock.yml
+++ b/.github/workflows/cron-lock.yml
@@ -19,4 +19,9 @@ jobs:
     steps:
       - uses: dessant/lock-threads@v5
         with:
-          issue-inactive-days: 45
+          issue-inactive-days: 10
+          issue-comment: |
+            Automatically locked because of our [CONTRIBUTING guidelines](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#issue-locking)
+          pr-inactive-days: 7
+          pr-comment: |
+            Automatically locked because of our [CONTRIBUTING guidelines](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#issue-locking)

From 9a8c90ee18095d284192476834d5d23074d136f3 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 27 Feb 2024 22:31:41 +0800
Subject: [PATCH 195/679] Use tailwind instead of `gt-[wh]-` helper classes
 (#29423)

Follow #29357
- Replace `gt-w-*` -> `tw-w-*` and remove `gt-w-*`
- Replace `gt-h-*` -> `tw-h-*` and remove `gt-h-*`
---
 templates/admin/self_check.tmpl       | 2 +-
 templates/base/head_navbar.tmpl       | 4 ++--
 templates/devtest/fomantic-modal.tmpl | 8 ++++----
 templates/repo/diff/box.tmpl          | 2 +-
 templates/repo/issue/card.tmpl        | 2 +-
 templates/status/404.tmpl             | 2 +-
 web_src/css/helpers.css               | 6 ------
 7 files changed, 10 insertions(+), 16 deletions(-)

diff --git a/templates/admin/self_check.tmpl b/templates/admin/self_check.tmpl
index 6bca01ec65..fafaf9242d 100644
--- a/templates/admin/self_check.tmpl
+++ b/templates/admin/self_check.tmpl
@@ -20,7 +20,7 @@
 			{{if .DatabaseCheckInconsistentCollationColumns}}
 				<div class="ui red message">
 					{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}
-					<ul class="gt-w-full">
+					<ul class="tw-w-full">
 					{{range .DatabaseCheckInconsistentCollationColumns}}
 						<li>{{.}}</li>
 					{{end}}
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index effe4dcea9..3797de0a0f 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -13,14 +13,14 @@
 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
 		<div class="ui secondary menu item navbar-mobile-right">
 			{{if .IsSigned}}
-			<a id="mobile-notifications-icon" class="item gt-w-auto gt-p-3" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
+			<a id="mobile-notifications-icon" class="item tw-w-auto gt-p-3" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 				<div class="gt-relative">
 					{{svg "octicon-bell"}}
 					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
 			</a>
 			{{end}}
-			<button class="item gt-w-auto ui icon mini button gt-p-3 gt-m-0" id="navbar-expand-toggle">{{svg "octicon-three-bars"}}</button>
+			<button class="item tw-w-auto ui icon mini button gt-p-3 gt-m-0" id="navbar-expand-toggle">{{svg "octicon-three-bars"}}</button>
 		</div>
 
 		<!-- navbar links non-mobile -->
diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/fomantic-modal.tmpl
index eda169a043..0b4199a197 100644
--- a/templates/devtest/fomantic-modal.tmpl
+++ b/templates/devtest/fomantic-modal.tmpl
@@ -5,7 +5,7 @@
 	<div id="test-modal-form-1" class="ui mini modal">
 		<div class="header">Form dialog (layout 1)</div>
 		<form class="content" method="post">
-			<div class="ui input gt-w-full"><input name="user_input"></div>
+			<div class="ui input tw-w-full"><input name="user_input"></div>
 			{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 		</form>
 	</div>
@@ -14,7 +14,7 @@
 		<div class="header">Form dialog (layout 2)</div>
 		<form method="post">
 			<div class="content">
-				<div class="ui input gt-w-full"><input name="user_input"></div>
+				<div class="ui input tw-w-full"><input name="user_input"></div>
 				{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 			</div>
 		</form>
@@ -24,7 +24,7 @@
 		<div class="header">Form dialog (layout 3)</div>
 		<form method="post">
 			<div class="content">
-				<div class="ui input gt-w-full"><input name="user_input"></div>
+				<div class="ui input tw-w-full"><input name="user_input"></div>
 			</div>
 			{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 		</form>
@@ -33,7 +33,7 @@
 	<div id="test-modal-form-4" class="ui mini modal">
 		<div class="header">Form dialog (layout 4)</div>
 		<div class="content">
-			<div class="ui input gt-w-full"><input name="user_input"></div>
+			<div class="ui input tw-w-full"><input name="user_input"></div>
 		</div>
 		<form method="post">
 			{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index b9a43a0612..c24500a149 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -203,7 +203,7 @@
 							{{if $showFileViewToggle}}
 								{{/* for image or CSV, it can have a horizontal scroll bar, there won't be review comment context menu (position absolute) which would be clipped by "overflow" */}}
 								<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} gt-overflow-x-scroll">
-									<table class="chroma gt-w-full">
+									<table class="chroma tw-w-full">
 										{{if $isImage}}
 											{{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}}
 										{{else}}
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 7fb3d82827..5e524079c8 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -6,7 +6,7 @@
 			{{end}}
 		</div>
 	{{end}}
-	<div class="content gt-p-0 gt-w-full">
+	<div class="content gt-p-0 tw-w-full">
 		<div class="gt-df gt-items-start">
 			<div class="issue-card-icon">
 				{{template "shared/issueicon" .}}
diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl
index a8cd3d3290..f1f1199665 100644
--- a/templates/status/404.tmpl
+++ b/templates/status/404.tmpl
@@ -1,5 +1,5 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content ui container center gt-w-screen {{if .IsRepo}}repository{{end}}">
+<div role="main" aria-label="{{.Title}}" class="page-content ui container center tw-w-screen {{if .IsRepo}}repository{{end}}">
 	{{if .IsRepo}}{{template "repo/header" .}}{{end}}
 	<div class="ui container center">
 		<p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/404.png" alt="404"></p>
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index da94ebb486..3579c193b1 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -73,12 +73,6 @@ Gitea's private styles use `g-` prefix.
 .gt-overflow-x-scroll { overflow-x: scroll !important; }
 .gt-overflow-y-hidden { overflow-y: hidden !important; }
 
-.gt-h-screen { height: 100vh !important; }
-.gt-h-full { height: 100% !important; }
-.gt-w-auto { width: auto !important; }
-.gt-w-screen { width: 100vw !important; }
-.gt-w-full { width: 100% !important; }
-
 .gt-float-left { float: left !important; }
 .gt-float-right { float: right !important; }
 .gt-clear-both { clear: both !important; }

From e9f4c2db8291c54044345aebd9381ac820ed9687 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 27 Feb 2024 23:09:13 +0800
Subject: [PATCH 196/679] Fix missed return (#29450)

---
 routers/api/v1/repo/file.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 907a5b568e..4895f7b1b3 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -655,6 +655,7 @@ func UpdateFile(ctx *context.APIContext) {
 	apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
 	if ctx.Repo.Repository.IsEmpty {
 		ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
+		return
 	}
 
 	if apiOpts.BranchName == "" {

From db545b208b4bd3d1961c519da66ee2b4421afa5c Mon Sep 17 00:00:00 2001
From: Nanguan Lin <nanguanlin6@gmail.com>
Date: Wed, 28 Feb 2024 01:56:18 +0800
Subject: [PATCH 197/679] Implement actions badge svgs (#28102)

replace #27187
close #23688
The badge has two parts: label(workflow name) and message(action
status). 5 colors are provided with 7 statuses.
Color mapping:
```go
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
}
```
preview:

![1](https://github.com/go-gitea/gitea/assets/70063547/5465cbaf-23cd-4437-9848-2738c3cb8985)

![2](https://github.com/go-gitea/gitea/assets/70063547/ec393d26-c6e6-4d38-b72c-51f2494c5e71)

![3](https://github.com/go-gitea/gitea/assets/70063547/3edb4fdf-1b08-4a02-ab2a-6bdd7f532fb2)

![4](https://github.com/go-gitea/gitea/assets/70063547/8c189de2-2169-4251-b115-0e39a52f3df8)

![5](https://github.com/go-gitea/gitea/assets/70063547/3fe22c73-c2d7-4fec-9ea4-c501a1e4e3bd)

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: delvh <dev.lh@web.de>
---
 docs/content/usage/badge.en-us.md          |  37 ++++++++
 models/actions/run.go                      |  17 ++++
 modules/badge/badge.go                     | 104 +++++++++++++++++++++
 routers/web/repo/actions/badge.go          |  56 +++++++++++
 routers/web/web.go                         |   3 +
 templates/shared/actions/runner_badge.tmpl |  25 +++++
 6 files changed, 242 insertions(+)
 create mode 100644 docs/content/usage/badge.en-us.md
 create mode 100644 modules/badge/badge.go
 create mode 100644 routers/web/repo/actions/badge.go
 create mode 100644 templates/shared/actions/runner_badge.tmpl

diff --git a/docs/content/usage/badge.en-us.md b/docs/content/usage/badge.en-us.md
new file mode 100644
index 0000000000..212134e01c
--- /dev/null
+++ b/docs/content/usage/badge.en-us.md
@@ -0,0 +1,37 @@
+---
+date: "2023-02-25T00:00:00+00:00"
+title: "Badge"
+slug: "badge"
+sidebar_position: 11
+toc: false
+draft: false
+aliases:
+  - /en-us/badge
+menu:
+  sidebar:
+    parent: "usage"
+    name: "Badge"
+    sidebar_position: 11
+    identifier: "Badge"
+---
+
+# Badge
+
+Gitea has its builtin Badge system which allows you to display the status of your repository in other places. You can use the following badges:
+
+## Workflow Badge
+
+The Gitea Actions workflow badge is a badge that shows the status of the latest workflow run.
+It is designed to be compatible with [GitHub Actions workflow badge](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge).
+
+You can use the following URL to get the badge:
+
+```
+https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
+```
+
+- `{owner}`: The owner of the repository.
+- `{repo}`: The name of the repository.
+- `{workflow_file}`: The name of the workflow file.
+- `{branch}`: Optional. The branch of the workflow. Default to your repository's default branch.
+- `{event}`: Optional. The event of the workflow. Default to none.
diff --git a/models/actions/run.go b/models/actions/run.go
index fcac58d515..7b3125949b 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -339,6 +339,23 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
 	return run, nil
 }
 
+func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) {
+	var run ActionRun
+	q := db.GetEngine(ctx).Where("repo_id=?", repoID).
+		And("ref = ?", branch).
+		And("workflow_id = ?", workflowFile)
+	if event != "" {
+		q.And("event = ?", event)
+	}
+	has, err := q.Desc("id").Get(&run)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile)
+	}
+	return &run, nil
+}
+
 // UpdateRun updates a run.
 // It requires the inputted run has Version set.
 // It will return error if the version is not matched (it means the run has been changed after loaded).
diff --git a/modules/badge/badge.go b/modules/badge/badge.go
new file mode 100644
index 0000000000..b30d0b4729
--- /dev/null
+++ b/modules/badge/badge.go
@@ -0,0 +1,104 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package badge
+
+import (
+	actions_model "code.gitea.io/gitea/models/actions"
+)
+
+// The Badge layout: |offset|label|message|
+// We use 10x scale to calculate more precisely
+// Then scale down to normal size in tmpl file
+
+type Label struct {
+	text  string
+	width int
+}
+
+func (l Label) Text() string {
+	return l.text
+}
+
+func (l Label) Width() int {
+	return l.width
+}
+
+func (l Label) TextLength() int {
+	return int(float64(l.width-defaultOffset) * 9.5)
+}
+
+func (l Label) X() int {
+	return l.width*5 + 10
+}
+
+type Message struct {
+	text  string
+	width int
+	x     int
+}
+
+func (m Message) Text() string {
+	return m.text
+}
+
+func (m Message) Width() int {
+	return m.width
+}
+
+func (m Message) X() int {
+	return m.x
+}
+
+func (m Message) TextLength() int {
+	return int(float64(m.width-defaultOffset) * 9.5)
+}
+
+type Badge struct {
+	Color    string
+	FontSize int
+	Label    Label
+	Message  Message
+}
+
+func (b Badge) Width() int {
+	return b.Label.width + b.Message.width
+}
+
+const (
+	defaultOffset    = 9
+	defaultFontSize  = 11
+	DefaultColor     = "#9f9f9f" // Grey
+	defaultFontWidth = 7         // approximate speculation
+)
+
+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
+}
+
+// GenerateBadge generates badge with given template
+func GenerateBadge(label, message, color string) Badge {
+	lw := defaultFontWidth*len(label) + defaultOffset
+	mw := defaultFontWidth*len(message) + defaultOffset
+	x := lw*10 + mw*5 - 10
+	return Badge{
+		Label: Label{
+			text:  label,
+			width: lw,
+		},
+		Message: Message{
+			text:  message,
+			width: mw,
+			x:     x,
+		},
+		FontSize: defaultFontSize * 10,
+		Color:    color,
+	}
+}
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
new file mode 100644
index 0000000000..6fa951826c
--- /dev/null
+++ b/routers/web/repo/actions/badge.go
@@ -0,0 +1,56 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/modules/badge"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
+)
+
+func GetWorkflowBadge(ctx *context.Context) {
+	workflowFile := ctx.Params("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")
+
+	badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+	if err != nil {
+		ctx.ServerError("GetWorkflowBadge", err)
+		return
+	}
+
+	ctx.Data["Badge"] = badge
+	ctx.RespHeader().Set("Content-Type", "image/svg+xml")
+	ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+}
+
+func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
+	extension := filepath.Ext(workflowFile)
+	workflowName := strings.TrimSuffix(workflowFile, extension)
+
+	run, err := actions_model.GetWorkflowLatestRun(ctx, ctx.Repo.Repository.ID, workflowFile, branchName, event)
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			return badge.GenerateBadge(workflowName, "no status", badge.DefaultColor), nil
+		}
+		return badge.Badge{}, err
+	}
+
+	color, ok := badge.StatusColorMap[run.Status]
+	if !ok {
+		return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
+	}
+	return badge.GenerateBadge(workflowName, run.Status.String(), color), nil
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 452998703a..b6dd9500c8 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1371,6 +1371,9 @@ func registerRoutes(m *web.Route) {
 				m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
 				m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
 			})
+			m.Group("/workflows/{workflow_name}", func() {
+				m.Get("/badge.svg", actions.GetWorkflowBadge)
+			})
 		}, reqRepoActionsReader, actions.MustEnableActions)
 
 		m.Group("/wiki", func() {
diff --git a/templates/shared/actions/runner_badge.tmpl b/templates/shared/actions/runner_badge.tmpl
new file mode 100644
index 0000000000..816e87e177
--- /dev/null
+++ b/templates/shared/actions/runner_badge.tmpl
@@ -0,0 +1,25 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="18"
+	role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}">
+	<title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title>
+	<linearGradient id="s" x2="0" y2="100%">
+		<stop offset="0" stop-color="#fff" stop-opacity=".7" />
+		<stop offset=".1" stop-color="#aaa" stop-opacity=".1" />
+		<stop offset=".9" stop-color="#000" stop-opacity=".3" />
+		<stop offset="1" stop-color="#000" stop-opacity=".5" />
+	</linearGradient>
+	<clipPath id="r">
+		<rect width="{{.Badge.Width}}" height="18" rx="4" fill="#fff" />
+	</clipPath>
+	<g clip-path="url(#r)">
+		<rect width="{{.Badge.Label.Width}}" height="18" fill="#555" />
+		<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="18" fill="{{.Badge.Color}}" />
+		<rect width="{{.Badge.Width}}" height="18" fill="url(#s)" />
+	</g>
+	<g fill="#fff" text-anchor="middle" font-family="Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision"
+		font-size="{{.Badge.FontSize}}"><text aria-hidden="true" x="{{.Badge.Label.X}}" y="140" fill="#010101" fill-opacity=".3"
+			transform="scale(.1)" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text x="{{.Badge.Label.X}}" y="130"
+			transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text aria-hidden="true"
+			x="{{.Badge.Message.X}}" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)"
+			textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text><text x="{{.Badge.Message.X}}" y="130" transform="scale(.1)"
+			fill="#fff" textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text></g>
+</svg>

From 274c0aea2e88db9bc41690c90e13e8aedf6193d4 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 28 Feb 2024 06:39:12 +0100
Subject: [PATCH 198/679] Let ctx.FormOptionalBool() return
 optional.Option[bool] (#29461)

just some refactoring bits towards replacing **util.OptionalBool** with
**optional.Option[bool]**
---
 models/repo/release.go      | 21 +++++++++++----------
 routers/web/repo/release.go |  3 ++-
 routers/web/user/search.go  |  3 ++-
 services/context/base.go    | 12 ++++++------
 services/context/repo.go    |  2 +-
 5 files changed, 22 insertions(+), 19 deletions(-)

diff --git a/models/repo/release.go b/models/repo/release.go
index 1f37f11b2e..9287931dd5 100644
--- a/models/repo/release.go
+++ b/models/repo/release.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -228,10 +229,10 @@ type FindReleasesOptions struct {
 	RepoID        int64
 	IncludeDrafts bool
 	IncludeTags   bool
-	IsPreRelease  util.OptionalBool
-	IsDraft       util.OptionalBool
+	IsPreRelease  optional.Option[bool]
+	IsDraft       optional.Option[bool]
 	TagNames      []string
-	HasSha1       util.OptionalBool // useful to find draft releases which are created with existing tags
+	HasSha1       optional.Option[bool] // useful to find draft releases which are created with existing tags
 }
 
 func (opts FindReleasesOptions) ToConds() builder.Cond {
@@ -246,14 +247,14 @@ func (opts FindReleasesOptions) ToConds() builder.Cond {
 	if len(opts.TagNames) > 0 {
 		cond = cond.And(builder.In("tag_name", opts.TagNames))
 	}
-	if !opts.IsPreRelease.IsNone() {
-		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()})
+	if opts.IsPreRelease.Has() {
+		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.Value()})
 	}
-	if !opts.IsDraft.IsNone() {
-		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()})
+	if opts.IsDraft.Has() {
+		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()})
 	}
-	if !opts.HasSha1.IsNone() {
-		if opts.HasSha1.IsTrue() {
+	if opts.HasSha1.Has() {
+		if opts.HasSha1.Value() {
 			cond = cond.And(builder.Neq{"sha1": ""})
 		} else {
 			cond = cond.And(builder.Eq{"sha1": ""})
@@ -275,7 +276,7 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
 		ListOptions:   listOptions,
 		IncludeDrafts: true,
 		IncludeTags:   true,
-		HasSha1:       util.OptionalBoolTrue,
+		HasSha1:       optional.Some(true),
 		RepoID:        repoID,
 	}
 
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index f9ab956d4c..a730c2d3b7 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
@@ -223,7 +224,7 @@ func TagsList(ctx *context.Context) {
 		// the drafts should also be included because a real tag might be used as a draft.
 		IncludeDrafts: true,
 		IncludeTags:   true,
-		HasSha1:       util.OptionalBoolTrue,
+		HasSha1:       optional.Some(true),
 		RepoID:        ctx.Repo.Repository.ID,
 	}
 
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index fb7729bbe1..5ef61c88d4 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -8,6 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
@@ -24,7 +25,7 @@ func Search(ctx *context.Context) {
 		Keyword:     ctx.FormTrim("q"),
 		UID:         ctx.FormInt64("uid"),
 		Type:        user_model.UserTypeIndividual,
-		IsActive:    ctx.FormOptionalBool("active"),
+		IsActive:    util.OptionalBoolFromGeneric(ctx.FormOptionalBool("active")),
 		ListOptions: listOptions,
 	})
 	if err != nil {
diff --git a/services/context/base.go b/services/context/base.go
index ddd04f4767..c4aa467ff4 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -17,8 +17,8 @@ import (
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/translation"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 
 	"github.com/go-chi/chi/v5"
@@ -207,17 +207,17 @@ func (b *Base) FormBool(key string) bool {
 	return v
 }
 
-// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value
-// for the provided key exists in the form else it returns OptionalBoolNone
-func (b *Base) FormOptionalBool(key string) util.OptionalBool {
+// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value
+// for the provided key exists in the form else it returns optional.None[bool]()
+func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
 	value := b.Req.FormValue(key)
 	if len(value) == 0 {
-		return util.OptionalBoolNone
+		return optional.None[bool]()
 	}
 	s := b.Req.FormValue(key)
 	v, _ := strconv.ParseBool(s)
 	v = v || strings.EqualFold(s, "on")
-	return util.OptionalBoolOf(v)
+	return optional.Some(v)
 }
 
 func (b *Base) SetFormString(key, value string) {
diff --git a/services/context/repo.go b/services/context/repo.go
index a73d09ee21..d6a68c0c1a 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -546,7 +546,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 	ctx.Data["NumTags"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		IncludeDrafts: true,
 		IncludeTags:   true,
-		HasSha1:       util.OptionalBoolTrue, // only draft releases which are created with existing tags
+		HasSha1:       optional.Some(true), // only draft releases which are created with existing tags
 		RepoID:        ctx.Repo.Repository.ID,
 	})
 	if err != nil {

From d557fbc5a715a1920a2860cb04ae6c8fe2225182 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 28 Feb 2024 11:16:15 +0100
Subject: [PATCH 199/679] Recolor dark theme to blue shade (#29283)

Now uses the same primary color as light theme. The secondary colors are
shifted towards a slightly blue shade. Could maybe desaturate a bit
more, but overall I think I'm happy with it.

Fixes: https://github.com/go-gitea/gitea/issues/27097

<img width="1343" alt="Screenshot 2024-02-27 at 22 21 46"
src="https://github.com/go-gitea/gitea/assets/115237/4163c393-b469-4a53-8f4b-1c33aa04f3ac">
<img width="581" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/e621f7f8-5679-4605-bf42-3d5ff1071e1e">
<img width="581" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/20e66493-2457-482b-b8f1-e5710934e189">

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/themes/theme-gitea-dark.css | 190 ++++++++++++------------
 1 file changed, 95 insertions(+), 95 deletions(-)

diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index bac002e3db..9cc2a656cb 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -3,72 +3,72 @@
 
 :root {
   --is-dark-theme: true;
-  --color-primary: #87ab63;
+  --color-primary: #4183c4;
   --color-primary-contrast: #ffffff;
-  --color-primary-dark-1: #93b373;
-  --color-primary-dark-2: #9fbc82;
-  --color-primary-dark-3: #abc492;
-  --color-primary-dark-4: #b7cda1;
-  --color-primary-dark-5: #cfddc1;
-  --color-primary-dark-6: #e7eee0;
-  --color-primary-dark-7: #f8faf6;
-  --color-primary-light-1: #7a9e55;
-  --color-primary-light-2: #6c8c4c;
-  --color-primary-light-3: #5f7b42;
-  --color-primary-light-4: #516939;
-  --color-primary-light-5: #364626;
-  --color-primary-light-6: #1b2313;
-  --color-primary-light-7: #080b06;
-  --color-primary-alpha-10: #87ab6319;
-  --color-primary-alpha-20: #87ab6333;
-  --color-primary-alpha-30: #87ab634b;
-  --color-primary-alpha-40: #87ab6366;
-  --color-primary-alpha-50: #87ab6380;
-  --color-primary-alpha-60: #87ab6399;
-  --color-primary-alpha-70: #87ab63b3;
-  --color-primary-alpha-80: #87ab63cc;
-  --color-primary-alpha-90: #87ab63e1;
+  --color-primary-dark-1: #548fca;
+  --color-primary-dark-2: #679cd0;
+  --color-primary-dark-3: #7aa8d6;
+  --color-primary-dark-4: #8db5dc;
+  --color-primary-dark-5: #b3cde7;
+  --color-primary-dark-6: #d9e6f3;
+  --color-primary-dark-7: #f4f8fb;
+  --color-primary-light-1: #3876b3;
+  --color-primary-light-2: #31699f;
+  --color-primary-light-3: #2b5c8b;
+  --color-primary-light-4: #254f77;
+  --color-primary-light-5: #193450;
+  --color-primary-light-6: #0c1a28;
+  --color-primary-light-7: #04080c;
+  --color-primary-alpha-10: #4183c419;
+  --color-primary-alpha-20: #4183c433;
+  --color-primary-alpha-30: #4183c44b;
+  --color-primary-alpha-40: #4183c466;
+  --color-primary-alpha-50: #4183c480;
+  --color-primary-alpha-60: #4183c499;
+  --color-primary-alpha-70: #4183c4b3;
+  --color-primary-alpha-80: #4183c4cc;
+  --color-primary-alpha-90: #4183c4e1;
   --color-primary-hover: var(--color-primary-light-1);
   --color-primary-active: var(--color-primary-light-2);
-  --color-secondary: #525767;
-  --color-secondary-dark-1: #5c6374;
-  --color-secondary-dark-2: #666e81;
-  --color-secondary-dark-3: #7c8497;
-  --color-secondary-dark-4: #8990a1;
-  --color-secondary-dark-5: #959cab;
-  --color-secondary-dark-6: #a2a8b5;
-  --color-secondary-dark-7: #afb4c0;
-  --color-secondary-dark-8: #bcc0ca;
-  --color-secondary-dark-9: #c9cbd4;
-  --color-secondary-dark-10: #d6d7de;
-  --color-secondary-dark-11: #e2e3e8;
-  --color-secondary-dark-12: #eeeff2;
-  --color-secondary-dark-13: #fbfbfc;
-  --color-secondary-light-1: #454a57;
-  --color-secondary-light-2: #383c47;
-  --color-secondary-light-3: #2c2f37;
-  --color-secondary-light-4: #1f2226;
-  --color-secondary-alpha-10: #52576719;
-  --color-secondary-alpha-20: #52576733;
-  --color-secondary-alpha-30: #5257674b;
-  --color-secondary-alpha-40: #52576766;
-  --color-secondary-alpha-50: #52576780;
-  --color-secondary-alpha-60: #52576799;
-  --color-secondary-alpha-70: #525767b3;
-  --color-secondary-alpha-80: #525767cc;
-  --color-secondary-alpha-90: #525767e1;
+  --color-secondary: #3f4346;
+  --color-secondary-dark-1: #464a4d;
+  --color-secondary-dark-2: #4f5356;
+  --color-secondary-dark-3: #5f6366;
+  --color-secondary-dark-4: #72767a;
+  --color-secondary-dark-5: #7f8488;
+  --color-secondary-dark-6: #8d9297;
+  --color-secondary-dark-7: #999ea3;
+  --color-secondary-dark-8: #a6abaf;
+  --color-secondary-dark-9: #aeb3b8;
+  --color-secondary-dark-10: #babfc4;
+  --color-secondary-dark-11: #c5cbd0;
+  --color-secondary-dark-12: #ced4da;
+  --color-secondary-dark-13: #d1d7dd;
+  --color-secondary-light-1: #313538;
+  --color-secondary-light-2: #272b2e;
+  --color-secondary-light-3: #1e2225;
+  --color-secondary-light-4: #171b1e;
+  --color-secondary-alpha-10: #3f434619;
+  --color-secondary-alpha-20: #3f434633;
+  --color-secondary-alpha-30: #3f43464b;
+  --color-secondary-alpha-40: #3f434666;
+  --color-secondary-alpha-50: #3f434680;
+  --color-secondary-alpha-60: #3f434699;
+  --color-secondary-alpha-70: #3f4346b3;
+  --color-secondary-alpha-80: #3f4346cc;
+  --color-secondary-alpha-90: #3f4346e1;
   --color-secondary-button: var(--color-secondary-dark-4);
   --color-secondary-hover: var(--color-secondary-dark-3);
   --color-secondary-active: var(--color-secondary-dark-2);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #eeeff2;
-  --color-console-fg-subtle: #959cab;
-  --color-console-bg: #262936;
-  --color-console-border: #383c47;
+  --color-console-fg: #ced4da;
+  --color-console-fg-subtle: #7f8488;
+  --color-console-bg: #1c2023;
+  --color-console-border: #272b2e;
   --color-console-hover-bg: #ffffff16;
-  --color-console-active-bg: #454a57;
-  --color-console-menu-bg: #383c47;
-  --color-console-menu-border: #5c6374;
+  --color-console-active-bg: #313538;
+  --color-console-menu-bg: #272b2e;
+  --color-console-menu-border: #464a4d;
   /* named colors */
   --color-red: #cc4848;
   --color-orange: #cc580c;
@@ -81,7 +81,7 @@
   --color-purple: #b259d0;
   --color-pink: #d22e8b;
   --color-brown: #a47252;
-  --color-black: #2e323e;
+  --color-black: #1f2326;
   /* light variants - produced via Sass scale-color(color, $lightness: +10%) */
   --color-red-light: #d15a5a;
   --color-orange-light: #f6a066;
@@ -94,7 +94,7 @@
   --color-purple-light: #ba6ad5;
   --color-pink-light: #d74397;
   --color-brown-light: #b08061;
-  --color-black-light: #3f4555;
+  --color-black-light: #46494d;
   /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
   --color-red-dark-1: #c23636;
   --color-orange-dark-1: #f38236;
@@ -107,7 +107,7 @@
   --color-purple-dark-1: #a742c9;
   --color-pink-dark-1: #be297d;
   --color-brown-dark-1: #94674a;
-  --color-black-dark-1: #292d38;
+  --color-black-dark-1: #2c2f35;
   /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
   --color-red-dark-2: #ad3030;
   --color-orange-dark-2: #f16e17;
@@ -120,7 +120,7 @@
   --color-purple-dark-2: #9834b9;
   --color-pink-dark-2: #a9246f;
   --color-brown-dark-2: #835b42;
-  --color-black-dark-2: #252832;
+  --color-black-dark-2: #292a2e;
   /* ansi colors used for actions console and console files */
   --color-ansi-black: var(--color-black);
   --color-ansi-red: var(--color-red);
@@ -139,8 +139,8 @@
   --color-ansi-bright-cyan: var(--color-teal-light);
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
-  --color-grey: #505665;
-  --color-grey-light: #a1a6b7;
+  --color-grey: #3c4043;
+  --color-grey-light: #898e92;
   --color-gold: #b1983b;
   --color-white: #ffffff;
   --color-diff-removed-word-bg: #6f3333;
@@ -151,7 +151,7 @@
   --color-diff-removed-row-border: #634343;
   --color-diff-moved-row-border: #bcca6f;
   --color-diff-added-row-border: #314a37;
-  --color-diff-inactive: #353846;
+  --color-diff-inactive: #24282b;
   --color-error-border: #a04141;
   --color-error-bg: #522;
   --color-error-bg-active: #744;
@@ -180,40 +180,40 @@
   --color-orange-badge-hover-bg: #f2711c4d;
   --color-git: #f05133;
   /* target-based colors */
-  --color-body: #2e323e;
-  --color-box-header: #303340;
-  --color-box-body: #222733;
-  --color-box-body-highlight: #262b36;
-  --color-text-dark: #dbe0ea;
-  --color-text: #cbd0da;
-  --color-text-light: #bbbfca;
-  --color-text-light-1: #aaafb9;
-  --color-text-light-2: #9a9ea9;
-  --color-text-light-3: #8a8e99;
-  --color-footer: #232834;
-  --color-timeline: #4c525e;
-  --color-input-text: #dfe3ec;
-  --color-input-background: #1e252e;
-  --color-input-toggle-background: #454a57;
+  --color-body: #1f2326;
+  --color-box-header: #202427;
+  --color-box-body: #191d20;
+  --color-box-body-highlight: #1d2124;
+  --color-text-dark: #c4cace;
+  --color-text: #babfc3;
+  --color-text-light: #a8acb0;
+  --color-text-light-1: #9ca0a5;
+  --color-text-light-2: #8f9397;
+  --color-text-light-3: #828689;
+  --color-footer: #1b1f22;
+  --color-timeline: #383c3f;
+  --color-input-text: #c7ccd1;
+  --color-input-background: #161a1d;
+  --color-input-toggle-background: #313538;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: #202430;
+  --color-header-wrapper: #191d20;
   --color-light: #00000028;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
   --color-light-border: #ffffff28;
   --color-hover: #ffffff19;
   --color-active: #ffffff24;
-  --color-menu: #1e252e;
-  --color-card: #1e252e;
+  --color-menu: #161a1d;
+  --color-card: #161a1d;
   --color-markup-table-row: #ffffff06;
   --color-markup-code-block: #ffffff16;
-  --color-button: #1e252e;
-  --color-code-bg: #222733;
-  --color-code-sidebar-bg: #232834;
+  --color-button: #161a1d;
+  --color-code-bg: #191d20;
+  --color-code-sidebar-bg: #1b1f22;
   --color-shadow: #00000058;
-  --color-secondary-bg: #2a2e3a;
-  --color-expand-button: #3c404d;
-  --color-placeholder-text: #8a8e99;
+  --color-secondary-bg: #2f3135;
+  --color-expand-button: #414348;
+  --color-placeholder-text: #777b7f;
   --color-editor-line-highlight: var(--color-primary-light-5);
   --color-project-board-bg: var(--color-secondary-light-2);
   --color-project-board-dark-label: #111111;
@@ -224,13 +224,13 @@
   --color-reaction-active-bg: var(--color-primary-light-5);
   --color-tooltip-text: #ffffff;
   --color-tooltip-bg: #000000f0;
-  --color-nav-bg: #232834;
-  --color-nav-hover-bg: #383c47;
+  --color-nav-bg: #1b1f22;
+  --color-nav-hover-bg: #272b2e;
   --color-nav-text: var(--color-text);
-  --color-label-text: #dfe3ec;
-  --color-label-bg: #7c84974b;
-  --color-label-hover-bg: #7c8497a0;
-  --color-label-active-bg: #7c8497ff;
+  --color-label-text: #ced2d7;
+  --color-label-bg: #7a7f834b;
+  --color-label-hover-bg: #7a7f83a0;
+  --color-label-active-bg: #7a7f83ff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-5);
   --color-active-line: #534d1b;

From d0fe6ea4e101198911383058a2e121e384934a9c Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Wed, 28 Feb 2024 18:54:44 +0800
Subject: [PATCH 200/679] The job should always run when `if` is `always()`
 (#29464)

Fix #27906

According to GitHub's
[documentation](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds),
a job should always run when its `if` is `always()`

> If you would like a job to run even if a job it is dependent on did
not succeed, use the `always()` conditional expression in
`jobs.<job_id>.if`.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 services/actions/job_emitter.go      | 21 ++++++++++-
 services/actions/job_emitter_test.go | 56 ++++++++++++++++++++++++++++
 2 files changed, 76 insertions(+), 1 deletion(-)

diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index fe39312386..d2bbbd9a7c 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -7,12 +7,14 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/queue"
 
+	"github.com/nektos/act/pkg/jobparser"
 	"xorm.io/builder"
 )
 
@@ -76,12 +78,15 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
 type jobStatusResolver struct {
 	statuses map[int64]actions_model.Status
 	needs    map[int64][]int64
+	jobMap   map[int64]*actions_model.ActionRunJob
 }
 
 func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
 	idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
+	jobMap := make(map[int64]*actions_model.ActionRunJob)
 	for _, job := range jobs {
 		idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
+		jobMap[job.ID] = job
 	}
 
 	statuses := make(map[int64]actions_model.Status, len(jobs))
@@ -97,6 +102,7 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
 	return &jobStatusResolver{
 		statuses: statuses,
 		needs:    needs,
+		jobMap:   jobMap,
 	}
 }
 
@@ -135,7 +141,20 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
 			if allSucceed {
 				ret[id] = actions_model.StatusWaiting
 			} else {
-				ret[id] = actions_model.StatusSkipped
+				// If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed.
+				// See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
+				always := false
+				if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
+					_, wfJob := wfJobs[0].Job()
+					expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}"))
+					always = expr == "always()"
+				}
+
+				if always {
+					ret[id] = actions_model.StatusWaiting
+				} else {
+					ret[id] = actions_model.StatusSkipped
+				}
 			}
 		}
 	}
diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go
index e81aa61d80..038df7d4f8 100644
--- a/services/actions/job_emitter_test.go
+++ b/services/actions/job_emitter_test.go
@@ -70,6 +70,62 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
 			},
 			want: map[int64]actions_model.Status{},
 		},
+		{
+			name: "with ${{ always() }} condition",
+			jobs: actions_model.ActionJobList{
+				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+					`
+name: test
+on: push
+jobs:
+  job2:
+    runs-on: ubuntu-latest
+    needs: job1
+    if: ${{ always() }}
+    steps:
+      - run: echo "always run"
+`)},
+			},
+			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
+		},
+		{
+			name: "with always() condition",
+			jobs: actions_model.ActionJobList{
+				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+					`
+name: test
+on: push
+jobs:
+  job2:
+    runs-on: ubuntu-latest
+    needs: job1
+    if: always()
+    steps:
+      - run: echo "always run"
+`)},
+			},
+			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
+		},
+		{
+			name: "without always() condition",
+			jobs: actions_model.ActionJobList{
+				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+					`
+name: test
+on: push
+jobs:
+  job2:
+    runs-on: ubuntu-latest
+    needs: job1
+    steps:
+      - run: echo "not always run"
+`)},
+			},
+			want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {

From b5188cd55c535a588492fb4e153d646ec4f3232a Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 28 Feb 2024 21:40:36 +0800
Subject: [PATCH 201/679] Move generate from module to service (#29465)

---
 modules/repository/init.go                    | 68 ---------------
 routers/api/v1/repo/repo.go                   |  2 +-
 routers/web/repo/repo.go                      |  2 +-
 services/repository/create.go                 |  2 +-
 {modules => services}/repository/generate.go  | 15 ++--
 .../repository/generate_test.go               |  0
 services/repository/init.go                   | 83 +++++++++++++++++++
 services/repository/template.go               |  7 +-
 8 files changed, 97 insertions(+), 82 deletions(-)
 rename {modules => services}/repository/generate.go (94%)
 rename {modules => services}/repository/generate_test.go (100%)
 create mode 100644 services/repository/init.go

diff --git a/modules/repository/init.go b/modules/repository/init.go
index b90b234a73..5f500c5233 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -6,22 +6,18 @@ package repository
 import (
 	"context"
 	"fmt"
-	"os"
 	"path/filepath"
 	"sort"
 	"strings"
-	"time"
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/label"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/options"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
-	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 type OptionFile struct {
@@ -124,70 +120,6 @@ func LoadRepoConfig() error {
 	return nil
 }
 
-// InitRepoCommit temporarily changes with work directory.
-func InitRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
-	commitTimeStr := time.Now().Format(time.RFC3339)
-
-	sig := u.NewGitSig()
-	// Because this may call hooks we should pass in the environment
-	env := append(os.Environ(),
-		"GIT_AUTHOR_NAME="+sig.Name,
-		"GIT_AUTHOR_EMAIL="+sig.Email,
-		"GIT_AUTHOR_DATE="+commitTimeStr,
-		"GIT_COMMITTER_DATE="+commitTimeStr,
-	)
-	committerName := sig.Name
-	committerEmail := sig.Email
-
-	if stdout, _, err := git.NewCommand(ctx, "add", "--all").
-		SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
-		RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
-		log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
-		return fmt.Errorf("git add --all: %w", err)
-	}
-
-	cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
-		AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
-
-	sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
-	if sign {
-		cmd.AddOptionFormat("-S%s", keyID)
-
-		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
-			// need to set the committer to the KeyID owner
-			committerName = signer.Name
-			committerEmail = signer.Email
-		}
-	} else {
-		cmd.AddArguments("--no-gpg-sign")
-	}
-
-	env = append(env,
-		"GIT_COMMITTER_NAME="+committerName,
-		"GIT_COMMITTER_EMAIL="+committerEmail,
-	)
-
-	if stdout, _, err := cmd.
-		SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
-		RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
-		log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
-		return fmt.Errorf("git commit: %w", err)
-	}
-
-	if len(defaultBranch) == 0 {
-		defaultBranch = setting.Repository.DefaultBranch
-	}
-
-	if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
-		SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
-		RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil {
-		log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
-		return fmt.Errorf("git push: %w", err)
-	}
-
-	return nil
-}
-
 func CheckInitRepository(ctx context.Context, owner, name, objectFormatName string) (err error) {
 	// Somehow the directory could exist.
 	repoPath := repo_model.RepoPath(owner, name)
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 8685d88913..da443bbf18 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -358,7 +358,7 @@ func Generate(ctx *context.APIContext) {
 		return
 	}
 
-	opts := repo_module.GenerateRepoOptions{
+	opts := repo_service.GenerateRepoOptions{
 		Name:            form.Name,
 		DefaultBranch:   form.DefaultBranch,
 		Description:     form.Description,
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 0fad8752e3..94e9d1267c 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -244,7 +244,7 @@ func CreatePost(ctx *context.Context) {
 	var repo *repo_model.Repository
 	var err error
 	if form.RepoTemplate > 0 {
-		opts := repo_module.GenerateRepoOptions{
+		opts := repo_service.GenerateRepoOptions{
 			Name:            form.RepoName,
 			Description:     form.Description,
 			Private:         form.Private,
diff --git a/services/repository/create.go b/services/repository/create.go
index a648c0d816..c47ce9c413 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -157,7 +157,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re
 		}
 
 		// Apply changes and commit.
-		if err = repo_module.InitRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
+		if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
 			return fmt.Errorf("initRepoCommit: %w", err)
 		}
 	}
diff --git a/modules/repository/generate.go b/services/repository/generate.go
similarity index 94%
rename from modules/repository/generate.go
rename to services/repository/generate.go
index f622383bb5..c444b60b2c 100644
--- a/modules/repository/generate.go
+++ b/services/repository/generate.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/gobwas/glob"
@@ -242,7 +243,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
 		defaultBranch = templateRepo.DefaultBranch
 	}
 
-	return InitRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
+	return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
 }
 
 func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
@@ -292,7 +293,7 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo
 		return err
 	}
 
-	if err := UpdateRepoSize(ctx, generateRepo); err != nil {
+	if err := repo_module.UpdateRepoSize(ctx, generateRepo); err != nil {
 		return fmt.Errorf("failed to update size for repository: %w", err)
 	}
 
@@ -323,8 +324,8 @@ func (gro GenerateRepoOptions) IsValid() bool {
 		gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
 }
 
-// GenerateRepository generates a repository from a template
-func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
+// generateRepository generates a repository from a template
+func generateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
 	generateRepo := &repo_model.Repository{
 		OwnerID:          owner.ID,
 		Owner:            owner,
@@ -341,7 +342,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
 		ObjectFormatName: templateRepo.ObjectFormatName,
 	}
 
-	if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
+	if err = repo_module.CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
 		return nil, err
 	}
 
@@ -358,11 +359,11 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
 		}
 	}
 
-	if err = CheckInitRepository(ctx, owner.Name, generateRepo.Name, generateRepo.ObjectFormatName); err != nil {
+	if err = repo_module.CheckInitRepository(ctx, owner.Name, generateRepo.Name, generateRepo.ObjectFormatName); err != nil {
 		return generateRepo, err
 	}
 
-	if err = CheckDaemonExportOK(ctx, generateRepo); err != nil {
+	if err = repo_module.CheckDaemonExportOK(ctx, generateRepo); err != nil {
 		return generateRepo, fmt.Errorf("checkDaemonExportOK: %w", err)
 	}
 
diff --git a/modules/repository/generate_test.go b/services/repository/generate_test.go
similarity index 100%
rename from modules/repository/generate_test.go
rename to services/repository/generate_test.go
diff --git a/services/repository/init.go b/services/repository/init.go
new file mode 100644
index 0000000000..817fa4abd7
--- /dev/null
+++ b/services/repository/init.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+	repo_module "code.gitea.io/gitea/modules/repository"
+	"code.gitea.io/gitea/modules/setting"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
+)
+
+// initRepoCommit temporarily changes with work directory.
+func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
+	commitTimeStr := time.Now().Format(time.RFC3339)
+
+	sig := u.NewGitSig()
+	// Because this may call hooks we should pass in the environment
+	env := append(os.Environ(),
+		"GIT_AUTHOR_NAME="+sig.Name,
+		"GIT_AUTHOR_EMAIL="+sig.Email,
+		"GIT_AUTHOR_DATE="+commitTimeStr,
+		"GIT_COMMITTER_DATE="+commitTimeStr,
+	)
+	committerName := sig.Name
+	committerEmail := sig.Email
+
+	if stdout, _, err := git.NewCommand(ctx, "add", "--all").
+		SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
+		RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
+		log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
+		return fmt.Errorf("git add --all: %w", err)
+	}
+
+	cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
+		AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
+
+	sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
+	if sign {
+		cmd.AddOptionFormat("-S%s", keyID)
+
+		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
+			// need to set the committer to the KeyID owner
+			committerName = signer.Name
+			committerEmail = signer.Email
+		}
+	} else {
+		cmd.AddArguments("--no-gpg-sign")
+	}
+
+	env = append(env,
+		"GIT_COMMITTER_NAME="+committerName,
+		"GIT_COMMITTER_EMAIL="+committerEmail,
+	)
+
+	if stdout, _, err := cmd.
+		SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
+		RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
+		log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
+		return fmt.Errorf("git commit: %w", err)
+	}
+
+	if len(defaultBranch) == 0 {
+		defaultBranch = setting.Repository.DefaultBranch
+	}
+
+	if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
+		SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
+		RunStdString(&git.RunOpts{Dir: tmpPath, Env: repo_module.InternalPushingEnvironment(u, repo)}); err != nil {
+		log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
+		return fmt.Errorf("git push: %w", err)
+	}
+
+	return nil
+}
diff --git a/services/repository/template.go b/services/repository/template.go
index 06cf05026f..36a680c8e2 100644
--- a/services/repository/template.go
+++ b/services/repository/template.go
@@ -11,7 +11,6 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	repo_module "code.gitea.io/gitea/modules/repository"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
@@ -63,7 +62,7 @@ func GenerateProtectedBranch(ctx context.Context, templateRepo, generateRepo *re
 }
 
 // GenerateRepository generates a repository from a template
-func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts repo_module.GenerateRepoOptions) (_ *repo_model.Repository, err error) {
+func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
 	if !doer.IsAdmin && !owner.CanCreateRepo() {
 		return nil, repo_model.ErrReachLimitOfRepo{
 			Limit: owner.MaxRepoCreation,
@@ -72,14 +71,14 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
 
 	var generateRepo *repo_model.Repository
 	if err = db.WithTx(ctx, func(ctx context.Context) error {
-		generateRepo, err = repo_module.GenerateRepository(ctx, doer, owner, templateRepo, opts)
+		generateRepo, err = generateRepository(ctx, doer, owner, templateRepo, opts)
 		if err != nil {
 			return err
 		}
 
 		// Git Content
 		if opts.GitContent && !templateRepo.IsEmpty {
-			if err = repo_module.GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
+			if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
 				return err
 			}
 		}

From 71e0f185f9773d1cc4909867a10c86f74d12ce8d Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 28 Feb 2024 16:11:54 +0200
Subject: [PATCH 202/679] Remove jQuery from the "find file" page (#29456)

- Switched to plain JavaScript
- Tested the file searching functionality and it works as before

# Demo using JavaScript without jQuery

![action](https://github.com/go-gitea/gitea/assets/20454870/8ceef0ed-ab87-448c-8b9b-9b5c0cd8bebd)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-findfile.js | 60 ++++++++++++++--------------
 1 file changed, 29 insertions(+), 31 deletions(-)

diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js
index 158732acc2..cb03d9e803 100644
--- a/web_src/js/features/repo-findfile.js
+++ b/web_src/js/features/repo-findfile.js
@@ -1,13 +1,11 @@
-import $ from 'jquery';
 import {svg} from '../svg.js';
 import {toggleElem} from '../utils/dom.js';
 import {pathEscapeSegments} from '../utils/url.js';
-
-const {csrf} = window.config;
+import {GET} from '../modules/fetch.js';
 
 const threshold = 50;
 let files = [];
-let $repoFindFileInput, $repoFindFileTableBody, $repoFindFileNoResult;
+let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult;
 
 // return the case-insensitive sub-match result as an array:  [unmatched, matched, unmatched, matched, ...]
 // res[even] is unmatched, res[odd] is matched, see unit tests for examples
@@ -74,46 +72,46 @@ export function filterRepoFilesWeighted(files, filter) {
 }
 
 function filterRepoFiles(filter) {
-  const treeLink = $repoFindFileInput.attr('data-url-tree-link');
-  $repoFindFileTableBody.empty();
+  const treeLink = repoFindFileInput.getAttribute('data-url-tree-link');
+  repoFindFileTableBody.innerHTML = '';
 
   const filterResult = filterRepoFilesWeighted(files, filter);
-  const tmplRow = `<tr><td><a></a></td></tr>`;
 
-  toggleElem($repoFindFileNoResult, filterResult.length === 0);
+  toggleElem(repoFindFileNoResult, filterResult.length === 0);
   for (const r of filterResult) {
-    const $row = $(tmplRow);
-    const $a = $row.find('a');
-    $a.attr('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`);
-    const $octiconFile = $(svg('octicon-file')).addClass('gt-mr-3');
-    $a.append($octiconFile);
-    // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
-    // the matchResult[odd] is matched and highlighted to red.
-    for (let j = 0; j < r.matchResult.length; j++) {
-      if (!r.matchResult[j]) continue;
-      const $span = $('<span>').text(r.matchResult[j]);
-      if (j % 2 === 1) $span.addClass('ui text red');
-      $a.append($span);
+    const row = document.createElement('tr');
+    const cell = document.createElement('td');
+    const a = document.createElement('a');
+    a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`);
+    a.innerHTML = svg('octicon-file', 16, 'gt-mr-3');
+    row.append(cell);
+    cell.append(a);
+    for (const [index, part] of r.matchResult.entries()) {
+      const span = document.createElement('span');
+      // safely escape by using textContent
+      span.textContent = part;
+      // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
+      // the matchResult[odd] is matched and highlighted to red.
+      if (index % 2 === 1) span.classList.add('ui', 'text', 'red');
+      a.append(span);
     }
-    $repoFindFileTableBody.append($row);
+    repoFindFileTableBody.append(row);
   }
 }
 
 async function loadRepoFiles() {
-  files = await $.ajax({
-    url: $repoFindFileInput.attr('data-url-data-link'),
-    headers: {'X-Csrf-Token': csrf}
-  });
-  filterRepoFiles($repoFindFileInput.val());
+  const response = await GET(repoFindFileInput.getAttribute('data-url-data-link'));
+  files = await response.json();
+  filterRepoFiles(repoFindFileInput.value);
 }
 
 export function initFindFileInRepo() {
-  $repoFindFileInput = $('#repo-file-find-input');
-  if (!$repoFindFileInput.length) return;
+  repoFindFileInput = document.getElementById('repo-file-find-input');
+  if (!repoFindFileInput) return;
 
-  $repoFindFileTableBody = $('#repo-find-file-table tbody');
-  $repoFindFileNoResult = $('#repo-find-file-no-result');
-  $repoFindFileInput.on('input', () => filterRepoFiles($repoFindFileInput.val()));
+  repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody');
+  repoFindFileNoResult = document.getElementById('repo-find-file-no-result');
+  repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value));
 
   loadRepoFiles();
 }

From 82405f808d7b50c3580f26e5ca645e2ed6d284ab Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 28 Feb 2024 16:04:04 +0100
Subject: [PATCH 203/679] Fix URL calculation in clone input box (#29470)

Ported the function as-is and added comments so we don't forget about
this in the future.

Fixes: https://github.com/go-gitea/gitea/issues/29462
---
 templates/repo/clone_script.tmpl           | 22 +++++++++++++++-------
 web_src/js/webcomponents/GiteaOriginUrl.js |  5 +++--
 2 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl
index 0376da4a71..40dae76dc7 100644
--- a/templates/repo/clone_script.tmpl
+++ b/templates/repo/clone_script.tmpl
@@ -24,14 +24,22 @@
 		const btn = isSSH ? sshBtn : httpsBtn;
 		if (!btn) return;
 
-		let link = btn.getAttribute('data-link');
-		if (link.startsWith('http://') || link.startsWith('https://')) {
-			// use current protocol/host as the clone link
-			const url = new URL(link);
-			url.protocol = window.location.protocol;
-			url.host = window.location.host;
-			link = url.toString();
+		// NOTE: Keep this function in sync with the one in the js folder
+		function toOriginUrl(urlStr) {
+			try {
+				if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
+					const {origin, protocol, hostname, port} = window.location;
+					const url = new URL(urlStr, origin);
+					url.protocol = protocol;
+					url.hostname = hostname;
+					url.port = port || (protocol === 'https:' ? '443' : '80');
+					return url.toString();
+				}
+			} catch {}
+			return urlStr;
 		}
+		const link = toOriginUrl(btn.getAttribute('data-link'));
+
 		for (const el of document.getElementsByClassName('js-clone-url')) {
 			el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
 		}
diff --git a/web_src/js/webcomponents/GiteaOriginUrl.js b/web_src/js/webcomponents/GiteaOriginUrl.js
index 5d71d95c60..6e6f84d739 100644
--- a/web_src/js/webcomponents/GiteaOriginUrl.js
+++ b/web_src/js/webcomponents/GiteaOriginUrl.js
@@ -1,7 +1,8 @@
-// Convert an absolute or relative URL to an absolute URL with the current origin
+// Convert an absolute or relative URL to an absolute URL with the current origin. It only
+// processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'.
+// NOTE: Keep this function in sync with clone_script.tmpl
 export function toOriginUrl(urlStr) {
   try {
-    // only process absolute HTTP/HTTPS URL or relative URLs ('/xxx' or '//host/xxx')
     if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
       const {origin, protocol, hostname, port} = window.location;
       const url = new URL(urlStr, origin);

From 1ad4bb9eb7641a552c5b88a43eb91d59ec5c0edf Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Wed, 28 Feb 2024 23:35:04 +0800
Subject: [PATCH 204/679] Fix workflow trigger event bugs (#29467)

1. Fix incorrect `HookEventType` for issue-related events in
`IssueChangeAssignee`
2. Add `case "types"` in the `switch` block in `matchPullRequestEvent`
to avoid warning logs
---
 modules/actions/workflows.go | 3 +++
 services/actions/notifier.go | 8 +++++++-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go
index 2db4a9296f..595fd8bbb0 100644
--- a/modules/actions/workflows.go
+++ b/modules/actions/workflows.go
@@ -441,6 +441,9 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 	// all acts conditions should be satisfied
 	for cond, vals := range acts {
 		switch cond {
+		case "types":
+			// types have been checked
+			continue
 		case "branches":
 			refName := git.RefName(prPayload.PullRequest.Base.Ref)
 			patterns, err := workflowpattern.CompilePatterns(vals...)
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index e144484dab..1e99c51a8b 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -152,7 +152,13 @@ func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo
 	} else {
 		action = api.HookIssueAssigned
 	}
-	notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestAssign, action)
+
+	hookEvent := webhook_module.HookEventIssueAssign
+	if issue.IsPull {
+		hookEvent = webhook_module.HookEventPullRequestAssign
+	}
+
+	notifyIssueChange(ctx, doer, issue, hookEvent, action)
 }
 
 // IssueChangeMilestone notifies assignee to notifiers

From 10cfa0879a538a470598281d7093de3555c018be Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 29 Feb 2024 00:03:06 +0800
Subject: [PATCH 205/679] Fix incorrect user location link on profile page
 (#29474)

Fix #29472. Regression of #29236, a "if" check was missing.
---
 routers/web/shared/user/header.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index eb108268ae..2253b8840d 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -35,8 +35,9 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 	prepareContextForCommonProfile(ctx)
 
 	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
-	ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
-
+	if setting.Service.UserLocationMapURL != "" {
+		ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
+	}
 	// Show OpenID URIs
 	openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
 	if err != nil {

From 252047ed2e09e3f1f1ab394cd62995cf4cabe506 Mon Sep 17 00:00:00 2001
From: charles <30816317+charles7668@users.noreply.github.com>
Date: Thu, 29 Feb 2024 04:23:49 +0800
Subject: [PATCH 206/679] Fix counter display number incorrectly displayed on
 the page (#29448)

issue : #28239

The counter number script uses the 'checkbox' attribute to determine
whether an item is selected or not.

However, the input event only increments the counter value, and when
more items are displayed, it does not update all previously loaded
items.

As a result, the display becomes incorrect because it triggers the
update counter script, but checkboxes that are selected without the
'checked' attribute are not counted
---
 web_src/js/features/pull-view-file.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/web_src/js/features/pull-view-file.js b/web_src/js/features/pull-view-file.js
index 90881ee989..2472e5a0bd 100644
--- a/web_src/js/features/pull-view-file.js
+++ b/web_src/js/features/pull-view-file.js
@@ -43,9 +43,11 @@ export function initViewedCheckboxListenerFor() {
       // Mark the file as viewed visually - will especially change the background
       if (this.checked) {
         form.classList.add(viewedStyleClass);
+        checkbox.setAttribute('checked', '');
         prReview.numberOfViewedFiles++;
       } else {
         form.classList.remove(viewedStyleClass);
+        checkbox.removeAttribute('checked');
         prReview.numberOfViewedFiles--;
       }
 

From 850fc2516e67049ec195c72d861896b275bd09d1 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 28 Feb 2024 21:26:12 +0100
Subject: [PATCH 207/679] Apply compact padding to small buttons with svg icons
 (#29471)

The buttons on the repo release tab were larger in height than on other
tabs because one of them contained the RSS icon which stretched the
button height by 3px. Workaround this problem by applying the "compact"
padding to any such button. They are within 0.4px in height now to
non-icon buttons.

Before:

<img width="406" alt="Screenshot 2024-02-28 at 15 30 23"
src="https://github.com/go-gitea/gitea/assets/115237/805bb93a-6fe4-40a0-82d1-03001bee8ecf">

After:

<img width="407" alt="Screenshot 2024-02-28 at 15 38 43"
src="https://github.com/go-gitea/gitea/assets/115237/27707588-890f-4852-ab08-105a57eda880">


For comparison, button on issue tab:

<img width="452" alt="Screenshot 2024-02-28 at 15 31 46"
src="https://github.com/go-gitea/gitea/assets/115237/74ac13d5-d016-49ba-9dd9-40ed32a748e9">
---
 templates/repo/release_tag_header.tmpl | 2 +-
 web_src/css/modules/button.css         | 7 +++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl
index f474fb89ea..31c151da08 100644
--- a/templates/repo/release_tag_header.tmpl
+++ b/templates/repo/release_tag_header.tmpl
@@ -13,7 +13,7 @@
 		</div>
 		{{if .EnableFeed}}
 			<a class="ui small button" href="{{.RepoLink}}/{{if .PageIsTagList}}tags{{else}}releases{{end}}.rss">
-				{{svg "octicon-rss" 18}} {{ctx.Locale.Tr "rss_feed"}}
+				{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "rss_feed"}}
 			</a>
 		{{end}}
 		{{if and (not .PageIsTagList) .CanCreateRelease}}
diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css
index 36cb499aeb..26f8fcf94c 100644
--- a/web_src/css/modules/button.css
+++ b/web_src/css/modules/button.css
@@ -85,6 +85,13 @@ It needs some tricks to tweak the left/right borders with active state */
   box-shadow: none;
 }
 
+/* apply the vertical padding of .compact to non-compact buttons when they contain a svg as they
+   would otherwise appear too large. Seen on "RSS Feed" button on repo releases tab. */
+.ui.small.button:not(.compact):has(.svg) {
+  padding-top: 0.58928571em;
+  padding-bottom: 0.58928571em;
+}
+
 .ui.labeled.button.disabled > .button,
 .ui.basic.buttons .button,
 .ui.basic.button,

From 6d9b7253a2de00b5dfc27550cf7e015e819d6fd2 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 28 Feb 2024 23:20:53 +0100
Subject: [PATCH 208/679] Fix/Improve `processWindowErrorEvent` (#29407)

- `e.error` can be undefined in some cases which would raise an error
inside this error handler, fixed that.
- The displayed message mentions looking into the console, but in my
case of error from `ResizeObserver` there was nothing there, so add this
logging. I think this logging was once there but got lost during
refactoring.
---
 web_src/js/bootstrap.js | 57 ++++++++++++++++++++++++++---------------
 1 file changed, 36 insertions(+), 21 deletions(-)

diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
index e46c91e5e6..c0047b0ac2 100644
--- a/web_src/js/bootstrap.js
+++ b/web_src/js/bootstrap.js
@@ -1,5 +1,6 @@
 // DO NOT IMPORT window.config HERE!
-// to make sure the error handler always works, we should never import `window.config`, because some user's custom template breaks it.
+// to make sure the error handler always works, we should never import `window.config`, because
+// some user's custom template breaks it.
 
 // This sets up the URL prefix used in webpack's chunk loading.
 // This file must be imported before any lazy-loading is being attempted.
@@ -26,29 +27,42 @@ export function showGlobalErrorMessage(msg) {
 }
 
 /**
- * @param {ErrorEvent} e
+ * @param {ErrorEvent|PromiseRejectionEvent} event - Event
+ * @param {string} event.message - Only present on ErrorEvent
+ * @param {string} event.error - Only present on ErrorEvent
+ * @param {string} event.type - Only present on ErrorEvent
+ * @param {string} event.filename - Only present on ErrorEvent
+ * @param {number} event.lineno - Only present on ErrorEvent
+ * @param {number} event.colno - Only present on ErrorEvent
+ * @param {string} event.reason - Only present on PromiseRejectionEvent
+ * @param {number} event.promise - Only present on PromiseRejectionEvent
  */
-function processWindowErrorEvent(e) {
-  const err = e.error ?? e.reason;
+function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}) {
+  const err = error ?? reason;
   const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin));
+  const {runModeIsProd} = window.config ?? {};
 
-  // error is likely from browser extension or inline script. Do not show these in production builds.
-  if (!err.stack?.includes(assetBaseUrl) && window.config?.runModeIsProd) return;
-
-  let message;
-  if (e.type === 'unhandledrejection') {
-    message = `JavaScript promise rejection: ${err.message}.`;
-  } else {
-    message = `JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}).`;
+  // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likly a
+  // non-critical event from the browser. We log them but don't show them to users. Examples:
+  // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
+  // - https://github.com/mozilla-mobile/firefox-ios/issues/10817
+  // - https://github.com/go-gitea/gitea/issues/20240
+  if (!err) {
+    if (message) console.error(new Error(message));
+    if (runModeIsProd) return;
   }
 
-  if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
-    // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
-    // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0.
-    return; // ignore such nonsense error event
+  // If the error stack trace does not include the base URL of our script assets, it likely came
+  // from a browser extension or inline script. Do not show such errors in production.
+  if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) {
+    return;
   }
 
-  showGlobalErrorMessage(`${message} Open browser console to see more details.`);
+  let msg = err?.message ?? message;
+  if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
+  const dot = msg.endsWith('.') ? '' : '.';
+  const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type;
+  showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`);
 }
 
 function initGlobalErrorHandler() {
@@ -59,13 +73,14 @@ function initGlobalErrorHandler() {
   if (!window.config) {
     showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
   }
-  // we added an event handler for window error at the very beginning of <script> of page head
-  // the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init
-  // then in this init, we can collect all error events and show them
+  // we added an event handler for window error at the very beginning of <script> of page head the
+  // handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before
+  // this init then in this init, we can collect all error events and show them.
   for (const e of window._globalHandlerErrors || []) {
     processWindowErrorEvent(e);
   }
-  // then, change _globalHandlerErrors to an object with push method, to process further error events directly
+  // then, change _globalHandlerErrors to an object with push method, to process further error
+  // events directly
   window._globalHandlerErrors = {_inited: true, push: (e) => processWindowErrorEvent(e)};
 }
 

From 6e1873288f86ca4de4d1943919343f342c7abcd9 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 29 Feb 2024 03:00:33 +0100
Subject: [PATCH 209/679] Improve contrast on blame timestamp, fix double
 border (#29482)

Before, double border on top, bad contrast on dark:
<img width="155" alt="Screenshot 2024-02-29 at 02 06 17"
src="https://github.com/go-gitea/gitea/assets/115237/fc0f1e08-a5ce-47ed-9eb6-135eed5a1abb">
<img width="126" alt="Screenshot 2024-02-29 at 02 07 28"
src="https://github.com/go-gitea/gitea/assets/115237/38ae8483-8d9b-484c-8909-d4466131ea16">

After, no double border on top, good contrast:
<img width="154" alt="Screenshot 2024-02-29 at 02 20 20"
src="https://github.com/go-gitea/gitea/assets/115237/ad91282b-e9f5-4f41-8f5e-6ba28db3beac">
<img width="147" alt="Screenshot 2024-02-29 at 02 20 38"
src="https://github.com/go-gitea/gitea/assets/115237/7ee2ec92-e72a-4981-aec3-98fc8e579bae">
---
 web_src/css/base.css | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 280808a5ce..77359b36e5 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1576,7 +1576,7 @@ a.ui.active.label:hover {
 
 .lines-commit {
   vertical-align: top;
-  color: var(--color-grey);
+  color: var(--color-text-light-2);
   padding: 0 !important;
   background: var(--color-code-sidebar-bg);
   width: 1%;
@@ -1619,6 +1619,10 @@ a.ui.active.label:hover {
   border-top: 1px solid var(--color-secondary);
 }
 
+.code-view tr.top-line-blame:first-of-type {
+  border-top: none;
+}
+
 .lines-code .bottom-line,
 .lines-commit .bottom-line {
   border-bottom: 1px solid var(--color-secondary);

From a6fd0176debb733e9d83826c08c7834dfdf9f486 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Thu, 29 Feb 2024 04:39:24 +0100
Subject: [PATCH 210/679] Fix wrong test usage of `AppSubURL` (#29459)

The tests use an invalid `setting.AppSubURL`. The wrong behaviour
disturbs other PRs like #29222 and #29427.
---
 modules/markup/markdown/markdown_test.go | 39 +++++++++++-------------
 routers/api/v1/misc/markup_test.go       | 18 +++++------
 2 files changed, 26 insertions(+), 31 deletions(-)

diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index bdf4011fa2..398efedb98 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -21,12 +21,11 @@ import (
 )
 
 const (
-	AppURL    = "http://localhost:3000/"
-	Repo      = "gogits/gogs"
-	AppSubURL = AppURL + Repo + "/"
+	AppURL  = "http://localhost:3000/"
+	FullURL = AppURL + "gogits/gogs/"
 )
 
-// these values should match the Repo const above
+// these values should match the const above
 var localMetas = map[string]string{
 	"user":     "gogits",
 	"repo":     "gogs",
@@ -48,13 +47,12 @@ func TestMain(m *testing.M) {
 
 func TestRender_StandardLinks(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
 	test := func(input, expected, expectedWiki string) {
 		buffer, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 		}, input)
 		assert.NoError(t, err)
@@ -63,7 +61,7 @@ func TestRender_StandardLinks(t *testing.T) {
 		buffer, err = markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 			IsWiki: true,
 		}, input)
@@ -74,8 +72,8 @@ func TestRender_StandardLinks(t *testing.T) {
 	googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
 	test("<https://google.com/>", googleRendered, googleRendered)
 
-	lnk := util.URLJoin(AppSubURL, "WikiPage")
-	lnkWiki := util.URLJoin(AppSubURL, "wiki", "WikiPage")
+	lnk := util.URLJoin(FullURL, "WikiPage")
+	lnkWiki := util.URLJoin(FullURL, "wiki", "WikiPage")
 	test("[WikiPage](WikiPage)",
 		`<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`,
 		`<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`)
@@ -83,13 +81,12 @@ func TestRender_StandardLinks(t *testing.T) {
 
 func TestRender_Images(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
 	test := func(input, expected string) {
 		buffer, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 		}, input)
 		assert.NoError(t, err)
@@ -99,7 +96,7 @@ func TestRender_Images(t *testing.T) {
 	url := "../../.images/src/02/train.jpg"
 	title := "Train"
 	href := "https://gitea.io"
-	result := util.URLJoin(AppSubURL, url)
+	result := util.URLJoin(FullURL, url)
 	// hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now
 
 	test(
@@ -289,15 +286,14 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
 
 func TestTotal_RenderWiki(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
-	answers := testAnswers(util.URLJoin(AppSubURL, "wiki"), util.URLJoin(AppSubURL, "wiki", "raw"))
+	answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
 
 	for i := 0; i < len(sameCases); i++ {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 			Metas:  localMetas,
 			IsWiki: true,
@@ -310,12 +306,12 @@ func TestTotal_RenderWiki(t *testing.T) {
 		// Guard wiki sidebar: special syntax
 		`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
+		`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
 `,
 		// special syntax
 		`[[Name|Link]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
+		`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
 `,
 	}
 
@@ -323,7 +319,7 @@ func TestTotal_RenderWiki(t *testing.T) {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 			IsWiki: true,
 		}, testCases[i])
@@ -334,15 +330,14 @@ func TestTotal_RenderWiki(t *testing.T) {
 
 func TestTotal_RenderString(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
-	answers := testAnswers(util.URLJoin(AppSubURL, "src", "master"), util.URLJoin(AppSubURL, "media", "master"))
+	answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
 
 	for i := 0; i < len(sameCases); i++ {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base:       AppSubURL,
+				Base:       FullURL,
 				BranchPath: "master",
 			},
 			Metas: localMetas,
@@ -357,7 +352,7 @@ func TestTotal_RenderString(t *testing.T) {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: AppSubURL,
+				Base: FullURL,
 			},
 		}, testCases[i])
 		assert.NoError(t, err)
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index f499501c2f..5236fd06ae 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -20,9 +20,9 @@ import (
 )
 
 const (
-	AppURL    = "http://localhost:3000/"
-	Repo      = "gogits/gogs"
-	AppSubURL = AppURL + Repo + "/"
+	AppURL  = "http://localhost:3000/"
+	Repo    = "gogits/gogs"
+	FullURL = AppURL + Repo + "/"
 )
 
 func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
@@ -74,20 +74,20 @@ func TestAPI_RenderGFM(t *testing.T) {
 		// rendered
 		`<p>Wiki! Enjoy :)</p>
 <ul>
-<li><a href="` + AppSubURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
-<li><a href="` + AppSubURL + `wiki/Tips" rel="nofollow">Tips</a></li>
+<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
+<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li>
 <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
 </ul>
 `,
 		// Guard wiki sidebar: special syntax
 		`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
+		`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
 `,
 		// special syntax
 		`[[Name|Link]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
+		`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
 `,
 		// empty
 		``,
@@ -111,8 +111,8 @@ Here are some links to the most important topics. You can find the full list of
 <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
 <h2 id="user-content-quick-links">Quick Links</h2>
 <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
-<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a>
-<a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
+<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a>
+<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
 `,
 	}
 

From e94e2fb6c5484070d56977644213d735df9e0c10 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 29 Feb 2024 06:11:11 +0100
Subject: [PATCH 211/679] Lighten text colors on dark theme for increased
 contrast (#29481)

Improve contrast by lightening the text colors in dark theme by around
35%. Additionally, share some variables that had the same or similar
color, which will ease future theme creation.
---
 web_src/css/themes/theme-gitea-dark.css  | 24 ++++++++++++------------
 web_src/css/themes/theme-gitea-light.css |  8 ++++----
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index 9cc2a656cb..ac77b7bbd9 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -61,8 +61,8 @@
   --color-secondary-hover: var(--color-secondary-dark-3);
   --color-secondary-active: var(--color-secondary-dark-2);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #ced4da;
-  --color-console-fg-subtle: #7f8488;
+  --color-console-fg: #d9dde2;
+  --color-console-fg-subtle: #95989c;
   --color-console-bg: #1c2023;
   --color-console-border: #272b2e;
   --color-console-hover-bg: #ffffff16;
@@ -184,15 +184,15 @@
   --color-box-header: #202427;
   --color-box-body: #191d20;
   --color-box-body-highlight: #1d2124;
-  --color-text-dark: #c4cace;
-  --color-text: #babfc3;
-  --color-text-light: #a8acb0;
-  --color-text-light-1: #9ca0a5;
-  --color-text-light-2: #8f9397;
-  --color-text-light-3: #828689;
-  --color-footer: #1b1f22;
+  --color-text-dark: #f8f8f9;
+  --color-text: #ced2d5;
+  --color-text-light: #bec4c8;
+  --color-text-light-1: #acb3b8;
+  --color-text-light-2: #8d969c;
+  --color-text-light-3: #747f87;
+  --color-footer: var(--color-nav-bg);
   --color-timeline: #383c3f;
-  --color-input-text: #c7ccd1;
+  --color-input-text: var(--color-text-dark);
   --color-input-background: #161a1d;
   --color-input-toggle-background: #313538;
   --color-input-border: var(--color-secondary);
@@ -213,7 +213,7 @@
   --color-shadow: #00000058;
   --color-secondary-bg: #2f3135;
   --color-expand-button: #414348;
-  --color-placeholder-text: #777b7f;
+  --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-5);
   --color-project-board-bg: var(--color-secondary-light-2);
   --color-project-board-dark-label: #111111;
@@ -227,7 +227,7 @@
   --color-nav-bg: #1b1f22;
   --color-nav-hover-bg: #272b2e;
   --color-nav-text: var(--color-text);
-  --color-label-text: #ced2d7;
+  --color-label-text: var(--color-text);
   --color-label-bg: #7a7f834b;
   --color-label-hover-bg: #7a7f83a0;
   --color-label-active-bg: #7a7f83ff;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index ca5d15cd25..5c375712d8 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -190,9 +190,9 @@
   --color-text-light-1: #6a6a6a;
   --color-text-light-2: #808080;
   --color-text-light-3: #a0a0a0;
-  --color-footer: #ffffff;
+  --color-footer: var(--color-nav-bg);
   --color-timeline: #ececec;
-  --color-input-text: #212121;
+  --color-input-text: var(--color-text-dark);
   --color-input-background: #fafafa;
   --color-input-toggle-background: #dedede;
   --color-input-border: var(--color-secondary);
@@ -213,7 +213,7 @@
   --color-shadow: #00000026;
   --color-secondary-bg: #f4f4f4;
   --color-expand-button: #d8efff;
-  --color-placeholder-text: #aaa;
+  --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-6);
   --color-project-board-bg: var(--color-secondary-light-4);
   --color-project-board-dark-label: #111111;
@@ -227,7 +227,7 @@
   --color-nav-bg: #ffffff;
   --color-nav-hover-bg: #ebebeb;
   --color-nav-text: var(--color-text);
-  --color-label-text: #232323;
+  --color-label-text: var(--color-text);
   --color-label-bg: #cacaca5b;
   --color-label-hover-bg: #cacacaa0;
   --color-label-active-bg: #cacacaff;

From c7dcb58b1d96970959a5c8ac8d3955e4b7d027df Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Thu, 29 Feb 2024 22:16:02 +0800
Subject: [PATCH 212/679] Update FAQ about git hook problems (#29495)

Close
https://github.com/go-gitea/gitea/issues/29338#issuecomment-1970363817
---
 docs/content/help/faq.en-us.md | 6 ++++--
 docs/content/help/faq.zh-cn.md | 6 ++++--
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/docs/content/help/faq.en-us.md b/docs/content/help/faq.en-us.md
index 5ea2c10f5e..b3b0980125 100644
--- a/docs/content/help/faq.en-us.md
+++ b/docs/content/help/faq.en-us.md
@@ -221,9 +221,11 @@ Our translations are currently crowd-sourced on our [Crowdin project](https://cr
 
 Whether you want to change a translation or add a new one, it will need to be there as all translations are overwritten in our CI via the Crowdin integration.
 
-## Push Hook / Webhook aren't running
+## Push Hook / Webhook / Actions aren't running
 
-If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook, there are a few possibilities:
+If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook and Actions workflows, it's likely that the git hooks are not working.
+
+There are a few possibilities:
 
 1. The git hooks are out of sync: run "Resynchronize pre-receive, update and post-receive hooks of all repositories" on the site admin panel
 2. The git repositories (and hooks) are stored on some filesystems (ex: mounted by NAS) which don't support script execution, make sure the filesystem supports `chmod a+x any-script`
diff --git a/docs/content/help/faq.zh-cn.md b/docs/content/help/faq.zh-cn.md
index b8dd3cd180..25230df70b 100644
--- a/docs/content/help/faq.zh-cn.md
+++ b/docs/content/help/faq.zh-cn.md
@@ -225,9 +225,11 @@ Gitea还提供了自己的SSH服务器,用于在SSHD不可用时使用。
 
 无论您想要更改翻译还是添加新的翻译,都需要在Crowdin集成中进行,因为所有翻译都会被CI覆盖。
 
-## 推送钩子/ Webhook未运行
+## 推送钩子/ Webhook / Actions 未运行
 
-如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发Webhook,有几种可能性:
+如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发 Webhook 和 Actions,可能是 git 钩子不工作而导致的。
+
+这可能是由于以下原因:
 
 1. Git钩子不同步:在站点管理面板上运行“重新同步所有仓库的pre-receive、update和post-receive钩子”
 2. Git仓库(和钩子)存储在一些不支持脚本执行的文件系统上(例如由NAS挂载),请确保文件系统支持`chmod a+x any-script`

From f6656181e4a07d6c415927220efa2077d509f7c6 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Thu, 29 Feb 2024 19:52:49 +0100
Subject: [PATCH 213/679] migrate some more "OptionalBool" to "Option[bool]"
 (#29479)

just some refactoring bits towards replacing **util.OptionalBool** with
**optional.Option[bool]**

---------

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 models/repo/repo_list.go          | 52 +++++++++----------
 models/repo/repo_list_test.go     | 83 +++++++++++++++----------------
 models/user/email_address.go      | 19 +++----
 models/user/email_address_test.go |  6 +--
 models/user/search.go             | 34 ++++++-------
 models/user/user_test.go          | 18 +++----
 modules/indexer/issues/indexer.go |  4 +-
 modules/optional/option_test.go   | 10 ++++
 modules/util/util.go              | 10 ++--
 modules/util/util_test.go         | 20 ++++----
 routers/api/v1/repo/issue.go      |  5 +-
 routers/api/v1/repo/repo.go       | 26 +++++-----
 routers/web/admin/emails.go       |  6 +--
 routers/web/admin/users.go        |  2 +-
 routers/web/explore/user.go       |  4 +-
 routers/web/home.go               |  4 +-
 routers/web/repo/issue.go         |  5 +-
 routers/web/repo/repo.go          | 24 ++++-----
 routers/web/shared/user/header.go |  3 +-
 routers/web/user/home.go          |  9 ++--
 routers/web/user/notification.go  |  3 +-
 routers/web/user/profile.go       |  7 +--
 routers/web/user/search.go        |  3 +-
 23 files changed, 183 insertions(+), 174 deletions(-)

diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index 533ca5251f..6b452291ea 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -13,6 +13,7 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
@@ -125,11 +126,11 @@ type SearchRepoOptions struct {
 	// None -> include public and private
 	// True -> include just private
 	// False -> include just public
-	IsPrivate util.OptionalBool
+	IsPrivate optional.Option[bool]
 	// None -> include collaborative AND non-collaborative
 	// True -> include just collaborative
 	// False -> include just non-collaborative
-	Collaborate util.OptionalBool
+	Collaborate optional.Option[bool]
 	// What type of unit the user can be collaborative in,
 	// it is ignored if Collaborate is False.
 	// TypeInvalid means any unit type.
@@ -137,19 +138,19 @@ type SearchRepoOptions struct {
 	// None -> include forks AND non-forks
 	// True -> include just forks
 	// False -> include just non-forks
-	Fork util.OptionalBool
+	Fork optional.Option[bool]
 	// None -> include templates AND non-templates
 	// True -> include just templates
 	// False -> include just non-templates
-	Template util.OptionalBool
+	Template optional.Option[bool]
 	// None -> include mirrors AND non-mirrors
 	// True -> include just mirrors
 	// False -> include just non-mirrors
-	Mirror util.OptionalBool
+	Mirror optional.Option[bool]
 	// None -> include archived AND non-archived
 	// True -> include just archived
 	// False -> include just non-archived
-	Archived util.OptionalBool
+	Archived optional.Option[bool]
 	// only search topic name
 	TopicOnly bool
 	// only search repositories with specified primary language
@@ -159,7 +160,7 @@ type SearchRepoOptions struct {
 	// None -> include has milestones AND has no milestone
 	// True -> include just has milestones
 	// False -> include just has no milestone
-	HasMilestones util.OptionalBool
+	HasMilestones optional.Option[bool]
 	// LowerNames represents valid lower names to restrict to
 	LowerNames []string
 	// When specified true, apply some filters over the conditions:
@@ -359,12 +360,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 			)))
 	}
 
-	if opts.IsPrivate != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()})
+	if opts.IsPrivate.Has() {
+		cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()})
 	}
 
-	if opts.Template != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue})
+	if opts.Template.Has() {
+		cond = cond.And(builder.Eq{"is_template": opts.Template.Value()})
 	}
 
 	// Restrict to starred repositories
@@ -380,11 +381,11 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 	// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
 	if opts.OwnerID > 0 {
 		accessCond := builder.NewCond()
-		if opts.Collaborate != util.OptionalBoolTrue {
+		if !opts.Collaborate.Value() {
 			accessCond = builder.Eq{"owner_id": opts.OwnerID}
 		}
 
-		if opts.Collaborate != util.OptionalBoolFalse {
+		if opts.Collaborate.ValueOrDefault(true) {
 			// A Collaboration is:
 
 			collaborateCond := builder.NewCond()
@@ -472,31 +473,32 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 			Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true})))
 	}
 
-	if opts.Fork != util.OptionalBoolNone || opts.OnlyShowRelevant {
-		if opts.OnlyShowRelevant && opts.Fork == util.OptionalBoolNone {
+	if opts.Fork.Has() || opts.OnlyShowRelevant {
+		if opts.OnlyShowRelevant && !opts.Fork.Has() {
 			cond = cond.And(builder.Eq{"is_fork": false})
 		} else {
-			cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue})
+			cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
 		}
 	}
 
-	if opts.Mirror != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue})
+	if opts.Mirror.Has() {
+		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()})
 	}
 
 	if opts.Actor != nil && opts.Actor.IsRestricted {
 		cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
 	}
 
-	if opts.Archived != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue})
+	if opts.Archived.Has() {
+		cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()})
 	}
 
-	switch opts.HasMilestones {
-	case util.OptionalBoolTrue:
-		cond = cond.And(builder.Gt{"num_milestones": 0})
-	case util.OptionalBoolFalse:
-		cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
+	if opts.HasMilestones.Has() {
+		if opts.HasMilestones.Value() {
+			cond = cond.And(builder.Gt{"num_milestones": 0})
+		} else {
+			cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
+		}
 	}
 
 	if opts.OnlyShowRelevant {
diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go
index 83e37a27fd..88cfcde620 100644
--- a/models/repo/repo_list_test.go
+++ b/models/repo/repo_list_test.go
@@ -10,7 +10,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -27,62 +27,62 @@ func getTestCases() []struct {
 	}{
 		{
 			name:  "PublicRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)},
 			count: 7,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicRepositoriesOfUser",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)},
 			count: 2,
 		},
 		{
 			name:  "PublicRepositoriesOfUser2",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)},
 			count: 0,
 		},
 		{
 			name:  "PublicRepositoriesOfOrg3",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)},
 			count: 2,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfUser",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)},
 			count: 4,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfUser2",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)},
 			count: 0,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfOrg3",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)},
 			count: 4,
 		},
 		{
@@ -117,32 +117,32 @@ func getTestCases() []struct {
 		},
 		{
 			name:  "PublicRepositoriesOfOrganization",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)},
 			count: 1,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfOrganization",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)},
 			count: 2,
 		},
 		{
 			name:  "AllPublic/PublicRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)},
 			count: 7,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)},
 			count: 33,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)},
 			count: 38,
 		},
 		{
@@ -157,12 +157,12 @@ func getTestCases() []struct {
 		},
 		{
 			name:  "AllPublic/PublicRepositoriesOfOrganization",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)},
 			count: 33,
 		},
 		{
 			name:  "AllTemplates",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)},
 			count: 2,
 		},
 		{
@@ -190,7 +190,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:     "repo_12",
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -205,7 +205,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:     "test_repo",
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -220,7 +220,7 @@ func TestSearchRepository(t *testing.T) {
 		},
 		Keyword:     "repo_13",
 		Private:     true,
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -236,7 +236,7 @@ func TestSearchRepository(t *testing.T) {
 		},
 		Keyword:     "test_repo",
 		Private:     true,
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -257,7 +257,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:            "description_14",
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		IncludeDescription: true,
 	})
 
@@ -274,7 +274,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:            "description_14",
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		IncludeDescription: false,
 	})
 
@@ -327,30 +327,25 @@ func TestSearchRepository(t *testing.T) {
 						assert.False(t, repo.IsPrivate)
 					}
 
-					if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue {
-						assert.True(t, repo.IsFork || repo.IsMirror)
+					if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() {
+						assert.True(t, repo.IsFork && repo.IsMirror)
 					} else {
-						switch testCase.opts.Fork {
-						case util.OptionalBoolFalse:
-							assert.False(t, repo.IsFork)
-						case util.OptionalBoolTrue:
-							assert.True(t, repo.IsFork)
+						if testCase.opts.Fork.Has() {
+							assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork)
 						}
 
-						switch testCase.opts.Mirror {
-						case util.OptionalBoolFalse:
-							assert.False(t, repo.IsMirror)
-						case util.OptionalBoolTrue:
-							assert.True(t, repo.IsMirror)
+						if testCase.opts.Mirror.Has() {
+							assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror)
 						}
 					}
 
 					if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic {
-						switch testCase.opts.Collaborate {
-						case util.OptionalBoolFalse:
-							assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
-						case util.OptionalBoolTrue:
-							assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
+						if testCase.opts.Collaborate.Has() {
+							if testCase.opts.Collaborate.Value() {
+								assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
+							} else {
+								assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
+							}
 						}
 					}
 				}
diff --git a/models/user/email_address.go b/models/user/email_address.go
index 9ddd1838d5..5d67304691 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
@@ -416,8 +417,8 @@ type SearchEmailOptions struct {
 	db.ListOptions
 	Keyword     string
 	SortType    SearchEmailOrderBy
-	IsPrimary   util.OptionalBool
-	IsActivated util.OptionalBool
+	IsPrimary   optional.Option[bool]
+	IsActivated optional.Option[bool]
 }
 
 // SearchEmailResult is an e-mail address found in the user or email_address table
@@ -444,18 +445,12 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
 		))
 	}
 
-	switch {
-	case opts.IsPrimary.IsTrue():
-		cond = cond.And(builder.Eq{"email_address.is_primary": true})
-	case opts.IsPrimary.IsFalse():
-		cond = cond.And(builder.Eq{"email_address.is_primary": false})
+	if opts.IsPrimary.Has() {
+		cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()})
 	}
 
-	switch {
-	case opts.IsActivated.IsTrue():
-		cond = cond.And(builder.Eq{"email_address.is_activated": true})
-	case opts.IsActivated.IsFalse():
-		cond = cond.And(builder.Eq{"email_address.is_activated": false})
+	if opts.IsActivated.Has() {
+		cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()})
 	}
 
 	count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid").
diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go
index dc3073f98b..c2e010d95b 100644
--- a/models/user/email_address_test.go
+++ b/models/user/email_address_test.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -128,14 +128,14 @@ func TestListEmails(t *testing.T) {
 	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 27 }))
 
 	// Must find only primary addresses (i.e. from the `user` table)
-	opts = &user_model.SearchEmailOptions{IsPrimary: util.OptionalBoolTrue}
+	opts = &user_model.SearchEmailOptions{IsPrimary: optional.Some(true)}
 	emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
 	assert.NoError(t, err)
 	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.IsPrimary }))
 	assert.False(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsPrimary }))
 
 	// Must find only inactive addresses (i.e. not validated)
-	opts = &user_model.SearchEmailOptions{IsActivated: util.OptionalBoolFalse}
+	opts = &user_model.SearchEmailOptions{IsActivated: optional.Some(false)}
 	emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
 	assert.NoError(t, err)
 	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated }))
diff --git a/models/user/search.go b/models/user/search.go
index 9484bf4425..45b051187e 100644
--- a/models/user/search.go
+++ b/models/user/search.go
@@ -10,8 +10,8 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 	"xorm.io/xorm"
@@ -33,11 +33,11 @@ type SearchUserOptions struct {
 
 	SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set
 
-	IsActive           util.OptionalBool
-	IsAdmin            util.OptionalBool
-	IsRestricted       util.OptionalBool
-	IsTwoFactorEnabled util.OptionalBool
-	IsProhibitLogin    util.OptionalBool
+	IsActive           optional.Option[bool]
+	IsAdmin            optional.Option[bool]
+	IsRestricted       optional.Option[bool]
+	IsTwoFactorEnabled optional.Option[bool]
+	IsProhibitLogin    optional.Option[bool]
 	IncludeReserved    bool
 
 	ExtraParamStrings map[string]string
@@ -89,24 +89,24 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
 		cond = cond.And(builder.Eq{"login_name": opts.LoginName})
 	}
 
-	if !opts.IsActive.IsNone() {
-		cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
+	if opts.IsActive.Has() {
+		cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()})
 	}
 
-	if !opts.IsAdmin.IsNone() {
-		cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
+	if opts.IsAdmin.Has() {
+		cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
 	}
 
-	if !opts.IsRestricted.IsNone() {
-		cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()})
+	if opts.IsRestricted.Has() {
+		cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()})
 	}
 
-	if !opts.IsProhibitLogin.IsNone() {
-		cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()})
+	if opts.IsProhibitLogin.Has() {
+		cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()})
 	}
 
 	e := db.GetEngine(ctx)
-	if opts.IsTwoFactorEnabled.IsNone() {
+	if !opts.IsTwoFactorEnabled.Has() {
 		return e.Where(cond)
 	}
 
@@ -114,7 +114,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
 	// While using LEFT JOIN, sometimes the performance might not be good, but it won't be a problem now, such SQL is seldom executed.
 	// There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now):
 	// (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch)
-	if opts.IsTwoFactorEnabled.IsTrue() {
+	if opts.IsTwoFactorEnabled.Value() {
 		cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL"))
 	} else {
 		cond = cond.And(builder.Expr("two_factor.uid IS NULL"))
@@ -131,7 +131,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
 	defer sessCount.Close()
 	count, err := sessCount.Count(new(User))
 	if err != nil {
-		return nil, 0, fmt.Errorf("Count: %w", err)
+		return nil, 0, fmt.Errorf("count: %w", err)
 	}
 
 	if len(opts.OrderBy) == 0 {
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 68cee9cdbd..f522f743d5 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -16,10 +16,10 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password/hash"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -103,29 +103,29 @@ func TestSearchUsers(t *testing.T) {
 	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
 		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
 		[]int64{9})
 
-	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 
-	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
 
 	// order by name asc default
-	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)},
 		[]int64{1})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)},
 		[]int64{29})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
 		[]int64{37})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
 		[]int64{24})
 }
 
diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go
index 57037d2947..e3bc21b49d 100644
--- a/modules/indexer/issues/indexer.go
+++ b/modules/indexer/issues/indexer.go
@@ -20,10 +20,10 @@ import (
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
 	"code.gitea.io/gitea/modules/indexer/issues/meilisearch"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/queue"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // IndexerMetadata is used to send data to the queue, so it contains only the ids.
@@ -220,7 +220,7 @@ func PopulateIssueIndexer(ctx context.Context) error {
 			ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize},
 			OrderBy:     db_model.SearchOrderByID,
 			Private:     true,
-			Collaborate: util.OptionalBoolFalse,
+			Collaborate: optional.Some(false),
 		})
 		if err != nil {
 			log.Error("SearchRepositoryByName: %v", err)
diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go
index 410fd73577..4f55608004 100644
--- a/modules/optional/option_test.go
+++ b/modules/optional/option_test.go
@@ -27,6 +27,16 @@ func TestOption(t *testing.T) {
 	assert.Equal(t, int(1), some.Value())
 	assert.Equal(t, int(1), some.ValueOrDefault(2))
 
+	noneBool := optional.None[bool]()
+	assert.False(t, noneBool.Has())
+	assert.False(t, noneBool.Value())
+	assert.True(t, noneBool.ValueOrDefault(true))
+
+	someBool := optional.Some(true)
+	assert.True(t, someBool.Has())
+	assert.True(t, someBool.Value())
+	assert.True(t, someBool.ValueOrDefault(false))
+
 	var ptr *int
 	assert.False(t, optional.FromPtr(ptr).Has())
 
diff --git a/modules/util/util.go b/modules/util/util.go
index 28b549f405..615f654e47 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -68,13 +68,13 @@ func OptionalBoolOf(b bool) OptionalBool {
 	return OptionalBoolFalse
 }
 
-// OptionalBoolParse get the corresponding OptionalBool of a string using strconv.ParseBool
-func OptionalBoolParse(s string) OptionalBool {
-	b, e := strconv.ParseBool(s)
+// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
+func OptionalBoolParse(s string) optional.Option[bool] {
+	v, e := strconv.ParseBool(s)
 	if e != nil {
-		return OptionalBoolNone
+		return optional.None[bool]()
 	}
-	return OptionalBoolOf(b)
+	return optional.Some(v)
 }
 
 // IsEmptyString checks if the provided string is empty
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index c5830ce01c..819e12ee91 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/modules/optional"
+
 	"github.com/stretchr/testify/assert"
 )
 
@@ -173,17 +175,17 @@ func Test_RandomBytes(t *testing.T) {
 	assert.NotEqual(t, bytes3, bytes4)
 }
 
-func Test_OptionalBool(t *testing.T) {
-	assert.Equal(t, OptionalBoolNone, OptionalBoolParse(""))
-	assert.Equal(t, OptionalBoolNone, OptionalBoolParse("x"))
+func TestOptionalBoolParse(t *testing.T) {
+	assert.Equal(t, optional.None[bool](), OptionalBoolParse(""))
+	assert.Equal(t, optional.None[bool](), OptionalBoolParse("x"))
 
-	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("0"))
-	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("f"))
-	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("False"))
+	assert.Equal(t, optional.Some(false), OptionalBoolParse("0"))
+	assert.Equal(t, optional.Some(false), OptionalBoolParse("f"))
+	assert.Equal(t, optional.Some(false), OptionalBoolParse("False"))
 
-	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("1"))
-	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("t"))
-	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("True"))
+	assert.Equal(t, optional.Some(true), OptionalBoolParse("1"))
+	assert.Equal(t, optional.Some(true), OptionalBoolParse("t"))
+	assert.Equal(t, optional.Some(true), OptionalBoolParse("True"))
 }
 
 // Test case for any function which accepts and returns a single string.
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index efc1a08a05..227e0e725c 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -142,7 +143,7 @@ func SearchIssues(ctx *context.APIContext) {
 			Private:     false,
 			AllPublic:   true,
 			TopicOnly:   false,
-			Collaborate: util.OptionalBoolNone,
+			Collaborate: optional.None[bool](),
 			// This needs to be a column that is not nil in fixtures or
 			// MySQL will return different results when sorting by null in some cases
 			OrderBy: db.SearchOrderByAlphabetically,
@@ -165,7 +166,7 @@ func SearchIssues(ctx *context.APIContext) {
 			opts.OwnerID = owner.ID
 			opts.AllLimited = false
 			opts.AllPublic = false
-			opts.Collaborate = util.OptionalBoolFalse
+			opts.Collaborate = optional.Some(false)
 		}
 		if ctx.FormString("team") != "" {
 			if ctx.FormString("owner") == "" {
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index da443bbf18..6fde73a4e8 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -24,10 +24,10 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/label"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
@@ -135,33 +135,33 @@ func Search(ctx *context.APIContext) {
 		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"),
 		TeamID:             ctx.FormInt64("team_id"),
 		TopicOnly:          ctx.FormBool("topic"),
-		Collaborate:        util.OptionalBoolNone,
+		Collaborate:        optional.None[bool](),
 		Private:            ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
-		Template:           util.OptionalBoolNone,
+		Template:           optional.None[bool](),
 		StarredByID:        ctx.FormInt64("starredBy"),
 		IncludeDescription: ctx.FormBool("includeDesc"),
 	}
 
 	if ctx.FormString("template") != "" {
-		opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
+		opts.Template = optional.Some(ctx.FormBool("template"))
 	}
 
 	if ctx.FormBool("exclusive") {
-		opts.Collaborate = util.OptionalBoolFalse
+		opts.Collaborate = optional.Some(false)
 	}
 
 	mode := ctx.FormString("mode")
 	switch mode {
 	case "source":
-		opts.Fork = util.OptionalBoolFalse
-		opts.Mirror = util.OptionalBoolFalse
+		opts.Fork = optional.Some(false)
+		opts.Mirror = optional.Some(false)
 	case "fork":
-		opts.Fork = util.OptionalBoolTrue
+		opts.Fork = optional.Some(true)
 	case "mirror":
-		opts.Mirror = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(true)
 	case "collaborative":
-		opts.Mirror = util.OptionalBoolFalse
-		opts.Collaborate = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(false)
+		opts.Collaborate = optional.Some(true)
 	case "":
 	default:
 		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
@@ -169,11 +169,11 @@ func Search(ctx *context.APIContext) {
 	}
 
 	if ctx.FormString("archived") != "" {
-		opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
+		opts.Archived = optional.Some(ctx.FormBool("archived"))
 	}
 
 	if ctx.FormString("is_private") != "" {
-		opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
+		opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
 	}
 
 	sortMode := ctx.FormString("sort")
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
index 4296d70aee..2cf4035c6a 100644
--- a/routers/web/admin/emails.go
+++ b/routers/web/admin/emails.go
@@ -12,8 +12,8 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -68,10 +68,10 @@ func Emails(ctx *context.Context) {
 	opts.Keyword = ctx.FormTrim("q")
 	opts.SortType = orderBy
 	if len(ctx.FormString("is_activated")) != 0 {
-		opts.IsActivated = util.OptionalBoolOf(ctx.FormBool("activated"))
+		opts.IsActivated = optional.Some(ctx.FormBool("activated"))
 	}
 	if len(ctx.FormString("is_primary")) != 0 {
-		opts.IsPrimary = util.OptionalBoolOf(ctx.FormBool("primary"))
+		opts.IsPrimary = optional.Some(ctx.FormBool("primary"))
 	}
 
 	if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index cbca26eba8..bbdbc820d7 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -276,7 +276,7 @@ func ViewUser(ctx *context.Context) {
 		OwnerID:     u.ID,
 		OrderBy:     db.SearchOrderByAlphabetically,
 		Private:     true,
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index 41f440f9d9..b79a79fb2c 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -12,10 +12,10 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -155,7 +155,7 @@ func Users(ctx *context.Context) {
 		Actor:       ctx.Doer,
 		Type:        user_model.UserTypeIndividual,
 		ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
-		IsActive:    util.OptionalBoolTrue,
+		IsActive:    optional.Some(true),
 		Visible:     []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
 
 		SupportedSortOrders: supportedSortOrders,
diff --git a/routers/web/home.go b/routers/web/home.go
index 555f94c983..d4be0931e8 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -13,10 +13,10 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/web/auth"
 	"code.gitea.io/gitea/routers/web/user"
@@ -71,7 +71,7 @@ func HomeSitemap(ctx *context.Context) {
 		_, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
 			Type:        user_model.UserTypeIndividual,
 			ListOptions: db.ListOptions{PageSize: 1},
-			IsActive:    util.OptionalBoolTrue,
+			IsActive:    optional.Some(true),
 			Visible:     []structs.VisibleType{structs.VisibleTypePublic},
 		})
 		if err != nil {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 65e74a2d90..d13c658d05 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -38,6 +38,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
@@ -2519,7 +2520,7 @@ func SearchIssues(ctx *context.Context) {
 			Private:     false,
 			AllPublic:   true,
 			TopicOnly:   false,
-			Collaborate: util.OptionalBoolNone,
+			Collaborate: optional.None[bool](),
 			// This needs to be a column that is not nil in fixtures or
 			// MySQL will return different results when sorting by null in some cases
 			OrderBy: db.SearchOrderByAlphabetically,
@@ -2542,7 +2543,7 @@ func SearchIssues(ctx *context.Context) {
 			opts.OwnerID = owner.ID
 			opts.AllLimited = false
 			opts.AllPublic = false
-			opts.Collaborate = util.OptionalBoolFalse
+			opts.Collaborate = optional.Some(false)
 		}
 		if ctx.FormString("team") != "" {
 			if ctx.FormString("owner") == "" {
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 94e9d1267c..49779efa37 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -553,33 +553,33 @@ func SearchRepo(ctx *context.Context) {
 		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"),
 		TeamID:             ctx.FormInt64("team_id"),
 		TopicOnly:          ctx.FormBool("topic"),
-		Collaborate:        util.OptionalBoolNone,
+		Collaborate:        optional.None[bool](),
 		Private:            ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
-		Template:           util.OptionalBoolNone,
+		Template:           optional.None[bool](),
 		StarredByID:        ctx.FormInt64("starredBy"),
 		IncludeDescription: ctx.FormBool("includeDesc"),
 	}
 
 	if ctx.FormString("template") != "" {
-		opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
+		opts.Template = optional.Some(ctx.FormBool("template"))
 	}
 
 	if ctx.FormBool("exclusive") {
-		opts.Collaborate = util.OptionalBoolFalse
+		opts.Collaborate = optional.Some(false)
 	}
 
 	mode := ctx.FormString("mode")
 	switch mode {
 	case "source":
-		opts.Fork = util.OptionalBoolFalse
-		opts.Mirror = util.OptionalBoolFalse
+		opts.Fork = optional.Some(false)
+		opts.Mirror = optional.Some(false)
 	case "fork":
-		opts.Fork = util.OptionalBoolTrue
+		opts.Fork = optional.Some(true)
 	case "mirror":
-		opts.Mirror = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(true)
 	case "collaborative":
-		opts.Mirror = util.OptionalBoolFalse
-		opts.Collaborate = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(false)
+		opts.Collaborate = optional.Some(true)
 	case "":
 	default:
 		ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
@@ -587,11 +587,11 @@ func SearchRepo(ctx *context.Context) {
 	}
 
 	if ctx.FormString("archived") != "" {
-		opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
+		opts.Archived = optional.Some(ctx.FormBool("archived"))
 	}
 
 	if ctx.FormString("is_private") != "" {
-		opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
+		opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
 	}
 
 	sortMode := ctx.FormString("sort")
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 2253b8840d..51b04e0613 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
@@ -114,7 +115,7 @@ func LoadHeaderCount(ctx *context.Context) error {
 		Actor:              ctx.Doer,
 		OwnerID:            ctx.ContextUser.ID,
 		Private:            ctx.IsSigned,
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		IncludeDescription: setting.UI.SearchRepoDescription,
 	})
 	if err != nil {
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 78548e6df7..6f36806ff7 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -28,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
@@ -161,8 +162,8 @@ func Milestones(ctx *context.Context) {
 		Private:       true,
 		AllPublic:     false, // Include also all public repositories of users and public organisations
 		AllLimited:    false, // Include also all public repositories of limited organisations
-		Archived:      util.OptionalBoolFalse,
-		HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
+		Archived:      optional.Some(false),
+		HasMilestones: optional.Some(true), // Just needs display repos has milestones
 	}
 
 	if ctxUser.IsOrganization() && ctx.Org.Team != nil {
@@ -465,9 +466,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 		Private:     true,
 		AllPublic:   false,
 		AllLimited:  false,
-		Collaborate: util.OptionalBoolNone,
+		Collaborate: optional.None[bool](),
 		UnitType:    unitType,
-		Archived:    util.OptionalBoolFalse,
+		Archived:    optional.Some(false),
 	}
 	if team != nil {
 		repoOpts.TeamID = team.ID
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 05034f8efa..801e1cf95e 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -17,6 +17,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
@@ -399,7 +400,7 @@ func NotificationWatching(ctx *context.Context) {
 		OrderBy:            orderBy,
 		Private:            ctx.IsSigned,
 		WatchedByID:        ctx.Doer.ID,
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		TopicOnly:          ctx.FormBool("topic"),
 		IncludeDescription: setting.UI.SearchRepoDescription,
 	})
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index e7890b7c12..b9b069b0d4 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
@@ -203,7 +204,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
 			StarredByID:        ctx.ContextUser.ID,
-			Collaborate:        util.OptionalBoolFalse,
+			Collaborate:        optional.Some(false),
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
@@ -225,7 +226,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
 			WatchedByID:        ctx.ContextUser.ID,
-			Collaborate:        util.OptionalBoolFalse,
+			Collaborate:        optional.Some(false),
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
@@ -270,7 +271,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			OwnerID:            ctx.ContextUser.ID,
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
-			Collaborate:        util.OptionalBoolFalse,
+			Collaborate:        optional.Some(false),
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index 5ef61c88d4..fb7729bbe1 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -8,7 +8,6 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
@@ -25,7 +24,7 @@ func Search(ctx *context.Context) {
 		Keyword:     ctx.FormTrim("q"),
 		UID:         ctx.FormInt64("uid"),
 		Type:        user_model.UserTypeIndividual,
-		IsActive:    util.OptionalBoolFromGeneric(ctx.FormOptionalBool("active")),
+		IsActive:    ctx.FormOptionalBool("active"),
 		ListOptions: listOptions,
 	})
 	if err != nil {

From a6eb298098983b7aae028fff4d80d15d5510f10b Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Fri, 1 Mar 2024 00:27:12 +0000
Subject: [PATCH 214/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_cs-CZ.ini | 36 +++++++++++++++++++++++++++++++++
 options/locale/locale_zh-CN.ini |  4 +++-
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index d30103a8eb..2a421b1172 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -123,6 +123,7 @@ pin=Připnout
 unpin=Odepnout
 
 artifacts=Artefakty
+confirm_delete_artifact=Jste si jisti, že chcete odstranit artefakt „%s“?
 
 archived=Archivováno
 
@@ -423,6 +424,7 @@ authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný
 sspi_auth_failed=SSPI autentizace selhala
 password_pwned=Heslo, které jste zvolili, je na <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">seznamu odcizených hesel</a>, která byla dříve odhalena při narušení veřejných dat. Zkuste to prosím znovu s jiným heslem.
 password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned
+last_admin=Nelze odstranit posledního správce. Musí existovat alespoň jeden správce.
 
 [mail]
 view_it_on=Zobrazit na %s
@@ -588,6 +590,7 @@ org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. N
 
 target_branch_not_exist=Cílová větev neexistuje.
 
+admin_cannot_delete_self=Nemůžete se smazat, dokud jste správce. Nejdříve prosím odeberte svá administrátorská oprávnění.
 
 [user]
 change_avatar=Změnit váš avatar…
@@ -967,6 +970,8 @@ issue_labels_helper=Vyberte sadu štítků úkolů.
 license=Licence
 license_helper=Vyberte licenční soubor.
 license_helper_desc=Licence řídí, co ostatní mohou a nemohou dělat s vaším kódem. Nejste si jisti, která je pro váš projekt správná? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">Zvolte licenci</a>
+object_format=Formát objektu
+object_format_helper=Objektový formát repozitáře. Nelze později změnit. SHA1 je nejvíce kompatibilní.
 readme=README
 readme_helper=Vyberte šablonu souboru README.
 readme_helper_desc=Toto je místo, kde můžete napsat úplný popis vašeho projektu.
@@ -1033,6 +1038,7 @@ desc.public=Veřejný
 desc.template=Šablona
 desc.internal=Interní
 desc.archived=Archivováno
+desc.sha256=SHA256
 
 template.items=Položky šablony
 template.git_content=Obsah gitu (výchozí větev)
@@ -1183,6 +1189,8 @@ audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5
 stored_lfs=Uloženo pomocí Git LFS
 symbolic_link=Symbolický odkaz
 executable_file=Spustitelný soubor
+vendored=Vendorováno
+generated=Generováno
 commit_graph=Graf commitů
 commit_graph.select=Vybrat větve
 commit_graph.hide_pr_refs=Skrýt požadavky na natažení
@@ -1518,7 +1526,11 @@ issues.label_title=Název štítku
 issues.label_description=Popis štítku
 issues.label_color=Barva štítku
 issues.label_exclusive=Exkluzivní
+issues.label_archive=Archivovat štítek
 issues.label_archived_filter=Zobrazit archivované popisky
+issues.label_archive_tooltip=Archivované štítky jsou ve výchozím nastavení vyloučeny z návrhů při hledání podle popisku.
+issues.label_exclusive_desc=Pojmenujte štítek <code>rozsah/položka</code>, aby se stal vzájemně exkluzivním s jinými štítky <code>rozsah/</code>.
+issues.label_exclusive_warning=Jakékoliv protichůdné rozsahy štítků budou odstraněny při úpravě štítků u úkolů nebo u požadavku na natažení.
 issues.label_count=%d štítků
 issues.label_open_issues=%d otevřených úkolů
 issues.label_edit=Upravit
@@ -1619,6 +1631,7 @@ issues.dependency.issue_closing_blockedby=Uzavření tohoto úkolu je blokováno
 issues.dependency.issue_close_blocks=Tento úkol blokuje uzavření následujících úkolů
 issues.dependency.pr_close_blocks=Tento požadavek na natažení blokuje uzavření následujících úkolů
 issues.dependency.issue_close_blocked=Musíte zavřít všechny úkoly, které blokují tento úkol, aby jej bylo možné zavřít.
+issues.dependency.issue_batch_close_blocked=Nelze uzavřít úkoly, které jste vybrali, protože úkol #%d má stále otevřené závislosti
 issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento požadavek na natažení, aby jej bylo možné sloučit.
 issues.dependency.blocks_short=Blokuje
 issues.dependency.blocked_by_short=Závisí na
@@ -1700,6 +1713,7 @@ pulls.select_commit_hold_shift_for_range=Vyberte commit. Podržte klávesu shift
 pulls.review_only_possible_for_full_diff=Posouzení je možné pouze při zobrazení plného rozlišení
 pulls.filter_changes_by_commit=Filtrovat podle commitu
 pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení.
+pulls.nothing_to_compare_have_tag=Vybraná větev/značka je stejná.
 pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný.
 pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
 pulls.create=Vytvořit požadavek na natažení
@@ -1822,6 +1836,7 @@ milestones.update_ago=Aktualizováno %s
 milestones.no_due_date=Bez lhůty dokončení
 milestones.open=Otevřít
 milestones.close=Zavřít
+milestones.new_subheader=Milníky vám pomohou organizovat úkoly a sledovat jejich pokrok.
 milestones.completeness=%d%% Dokončeno
 milestones.create=Vytvořit milník
 milestones.title=Název
@@ -1955,6 +1970,7 @@ activity.git_stats_and_deletions=a
 activity.git_stats_deletion_1=%d odebrání
 activity.git_stats_deletion_n=%d odebrání
 
+contributors.contribution_type.filter_label=Typ příspěvku:
 contributors.contribution_type.commits=Commity
 
 search=Vyhledat
@@ -2341,6 +2357,7 @@ settings.matrix.room_id=ID místnosti
 settings.matrix.message_type=Typ zprávy
 settings.archive.button=Archivovat repozitář
 settings.archive.header=Archivovat tento repozitář
+settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo žádosti o natažení.
 settings.archive.success=Repozitář byl úspěšně archivován.
 settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů.
 settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář.
@@ -2545,6 +2562,11 @@ error.csv.unexpected=Tento soubor nelze vykreslit, protože obsahuje neočekáva
 error.csv.invalid_field_count=Soubor nelze vykreslit, protože má nesprávný počet polí na řádku %d.
 
 [graphs]
+component_loading=Načítání %s...
+component_loading_failed=Nelze načíst %s
+component_loading_info=Může to chvíli trvat…
+component_failed_to_load=Došlo k neočekávané chybě.
+contributors.what=příspěvky
 
 [org]
 org_name_holder=Název organizace
@@ -2715,6 +2737,7 @@ dashboard.delete_repo_archives.started=Spuštěna úloha smazání všech archiv
 dashboard.delete_missing_repos=Smazat všechny repozitáře, které nemají Git soubory
 dashboard.delete_missing_repos.started=Spuštěna úloha mazání všech repozitářů, které nemají Git soubory.
 dashboard.delete_generated_repository_avatars=Odstranit vygenerované avatary repozitářů
+dashboard.sync_repo_tags=Synchronizovat značky z git dat do databáze
 dashboard.update_mirrors=Aktualizovat zrcadla
 dashboard.repo_health_check=Kontrola stavu všech repozitářů
 dashboard.check_repo_stats=Zkontrolovat všechny statistiky repositáře
@@ -2762,11 +2785,14 @@ dashboard.delete_old_actions=Odstranit všechny staré akce z databáze
 dashboard.delete_old_actions.started=Začalo odstraňování všech starých akcí z databáze.
 dashboard.update_checker=Kontrola aktualizací
 dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze
+dashboard.gc_lfs=Úklid LFS meta objektů
 dashboard.stop_zombie_tasks=Zastavit zombie úlohy
 dashboard.stop_endless_tasks=Zastavit nekonečné úlohy
 dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy
 dashboard.start_schedule_tasks=Spustit naplánované úlohy
 dashboard.sync_branch.started=Synchronizace větví byla spuštěna
+dashboard.sync_tag.started=Synchronizace značek spuštěna
+dashboard.rebuild_issue_indexer=Znovu sestavit index úkolů
 
 users.user_manage_panel=Správa uživatelských účtů
 users.new_account=Vytvořit uživatelský účet
@@ -3184,6 +3210,12 @@ notices.desc=Popis
 notices.op=Akce
 notices.delete_success=Systémové upozornění bylo smazáno.
 
+self_check.no_problem_found=Zatím nebyl nalezen žádný problém.
+self_check.database_collation_mismatch=Očekávejte, že databáze použije collation: %s
+self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Gitea může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání.
+self_check.database_inconsistent_collation_columns=Databáze používá collation %s, ale tyto sloupce používají chybné collation. To může způsobit neočekávané problémy.
+self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít příkaz "gitea doctor convert", který opraví problémy s collation, nebo můžete také problém vyřešit příkazem "ALTER ... COLLATE ..." SQL ručně.
+self_check.database_fix_mssql=Uživatelé MSSQL mohou problém vyřešit pouze pomocí příkazu "ALTER ... COLLATE ..." SQL ručně.
 
 [action]
 create_repo=vytvořil/a repozitář <a href="%s">%s</a>
@@ -3371,6 +3403,7 @@ rpm.distros.suse=na distribuce založené na SUSE
 rpm.install=Pro instalaci balíčku spusťte následující příkaz:
 rpm.repository=Informace o repozitáři
 rpm.repository.architectures=Architektury
+rpm.repository.multiple_groups=Tento balíček je k dispozici ve více skupinách.
 rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz:
 rubygems.install2=nebo ho přidejte do Gemfie:
 rubygems.dependencies.runtime=Běhové závislosti
@@ -3498,6 +3531,8 @@ runs.actors_no_select=Všichni aktéři
 runs.status_no_select=Všechny stavy
 runs.no_results=Nebyly nalezeny žádné výsledky.
 runs.no_workflows=Zatím neexistují žádné pracovní postupy.
+runs.no_workflows.quick_start=Nevíte jak začít s Gitea Actions? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">průvodce rychlým startem</a>.
+runs.no_workflows.documentation=Další informace o Gitea Actions naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
 runs.no_runs=Pracovní postup zatím nebyl spuštěn.
 runs.empty_commit_message=(prázdná zpráva commitu)
 
@@ -3515,6 +3550,7 @@ variables.none=Zatím nejsou žádné proměnné.
 variables.deletion=Odstranit proměnnou
 variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat?
 variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak.
+variables.id_not_exist=Proměnná s ID %d neexistuje.
 variables.edit=Upravit proměnnou
 variables.deletion.failed=Nepodařilo se odstranit proměnnou.
 variables.deletion.success=Proměnná byla odstraněna.
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 7c8153cbb1..89f237a117 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -2119,7 +2119,7 @@ settings.trust_model.collaborator.long=协作者:信任协作者的签名
 settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。
 settings.trust_model.committer=提交者
 settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub,这会强制采用 Gitea 作为提交者和签名者)
-settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须撇撇数据库种的一名用户。
+settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须匹配数据库中的一名用户。
 settings.trust_model.collaboratorcommitter=协作者+提交者
 settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名
 settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。
@@ -3250,7 +3250,9 @@ notices.delete_success=系统通知已被删除。
 self_check.no_problem_found=尚未发现问题。
 self_check.database_collation_mismatch=期望数据库使用的校验方式:%s
 self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。
+self_check.database_inconsistent_collation_columns=数据库正在使用%s的排序规则,但是这些列使用了不匹配的排序规则。这可能会造成一些意外问题。
 self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。
+self_check.database_fix_mssql=对于MSSQL用户,您现在只能通过"ALTER ... COLLATE ..."SQLs手动解决这个问题。
 
 [action]
 create_repo=创建了仓库 <a href="%s">%s</a>

From 5e32cd6beb1a4f11bd19e6d44ba2a50828b686ef Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Thu, 29 Feb 2024 20:43:42 -0500
Subject: [PATCH 215/679] =?UTF-8?q?Don=E2=80=99t=20comment=20when=20lockin?=
 =?UTF-8?q?g=20(#29508)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reduces the number of emails/notifications on outdated issues.

Co-authored-by: John Olheiser <john.olheiser@gmail.com>
---
 .github/workflows/cron-lock.yml | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml
index d4172687d5..665313135b 100644
--- a/.github/workflows/cron-lock.yml
+++ b/.github/workflows/cron-lock.yml
@@ -20,8 +20,4 @@ jobs:
       - uses: dessant/lock-threads@v5
         with:
           issue-inactive-days: 10
-          issue-comment: |
-            Automatically locked because of our [CONTRIBUTING guidelines](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#issue-locking)
           pr-inactive-days: 7
-          pr-comment: |
-            Automatically locked because of our [CONTRIBUTING guidelines](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#issue-locking)

From 58ce1de994c2a036ebf7137c143ce193694d740d Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 1 Mar 2024 10:23:00 +0800
Subject: [PATCH 216/679] Move migration functions to services layer (#29497)

---
 modules/repository/repo.go            | 243 ------------------------
 routers/web/repo/setting/setting.go   |   3 +-
 services/migrations/gitea_uploader.go |   2 +-
 services/repository/migrate.go        | 264 ++++++++++++++++++++++++++
 tests/integration/mirror_pull_test.go |   3 +-
 5 files changed, 267 insertions(+), 248 deletions(-)
 create mode 100644 services/repository/migrate.go

diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index 39bdc6adcf..cb926084ba 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -5,16 +5,13 @@ package repository
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"io"
-	"net/http"
 	"strings"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
@@ -22,10 +19,8 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/migration"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 )
 
 /*
@@ -47,244 +42,6 @@ func WikiRemoteURL(ctx context.Context, remote string) string {
 	return ""
 }
 
-// MigrateRepositoryGitData starts migrating git related data after created migrating repository
-func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
-	repo *repo_model.Repository, opts migration.MigrateOptions,
-	httpTransport *http.Transport,
-) (*repo_model.Repository, error) {
-	repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
-
-	if u.IsOrganization() {
-		t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
-		if err != nil {
-			return nil, err
-		}
-		repo.NumWatches = t.NumMembers
-	} else {
-		repo.NumWatches = 1
-	}
-
-	migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
-
-	var err error
-	if err = util.RemoveAll(repoPath); err != nil {
-		return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err)
-	}
-
-	if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
-		Mirror:        true,
-		Quiet:         true,
-		Timeout:       migrateTimeout,
-		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
-	}); err != nil {
-		if errors.Is(err, context.DeadlineExceeded) {
-			return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err)
-		}
-		return repo, fmt.Errorf("Clone: %w", err)
-	}
-
-	if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
-		return repo, err
-	}
-
-	if opts.Wiki {
-		wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
-		wikiRemotePath := WikiRemoteURL(ctx, opts.CloneAddr)
-		if len(wikiRemotePath) > 0 {
-			if err := util.RemoveAll(wikiPath); err != nil {
-				return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
-			}
-
-			if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
-				Mirror:        true,
-				Quiet:         true,
-				Timeout:       migrateTimeout,
-				Branch:        "master",
-				SkipTLSVerify: setting.Migrations.SkipTLSVerify,
-			}); err != nil {
-				log.Warn("Clone wiki: %v", err)
-				if err := util.RemoveAll(wikiPath); err != nil {
-					return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
-				}
-			} else {
-				if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
-					return repo, err
-				}
-			}
-		}
-	}
-
-	if repo.OwnerID == u.ID {
-		repo.Owner = u
-	}
-
-	if err = CheckDaemonExportOK(ctx, repo); err != nil {
-		return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
-	}
-
-	if stdout, _, err := git.NewCommand(ctx, "update-server-info").
-		SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
-		RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
-		log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
-		return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
-	}
-
-	gitRepo, err := git.OpenRepository(ctx, repoPath)
-	if err != nil {
-		return repo, fmt.Errorf("OpenRepository: %w", err)
-	}
-	defer gitRepo.Close()
-
-	repo.IsEmpty, err = gitRepo.IsEmpty()
-	if err != nil {
-		return repo, fmt.Errorf("git.IsEmpty: %w", err)
-	}
-
-	if !repo.IsEmpty {
-		if len(repo.DefaultBranch) == 0 {
-			// Try to get HEAD branch and set it as default branch.
-			headBranch, err := gitRepo.GetHEADBranch()
-			if err != nil {
-				return repo, fmt.Errorf("GetHEADBranch: %w", err)
-			}
-			if headBranch != nil {
-				repo.DefaultBranch = headBranch.Name
-			}
-		}
-
-		if _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
-			return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err)
-		}
-
-		if !opts.Releases {
-			// note: this will greatly improve release (tag) sync
-			// for pull-mirrors with many tags
-			repo.IsMirror = opts.Mirror
-			if err = SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
-				log.Error("Failed to synchronize tags to releases for repository: %v", err)
-			}
-		}
-
-		if opts.LFS {
-			endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
-			lfsClient := lfs.NewClient(endpoint, httpTransport)
-			if err = StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
-				log.Error("Failed to store missing LFS objects for repository: %v", err)
-			}
-		}
-	}
-
-	ctx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return nil, err
-	}
-	defer committer.Close()
-
-	if opts.Mirror {
-		remoteAddress, err := util.SanitizeURL(opts.CloneAddr)
-		if err != nil {
-			return repo, err
-		}
-		mirrorModel := repo_model.Mirror{
-			RepoID:         repo.ID,
-			Interval:       setting.Mirror.DefaultInterval,
-			EnablePrune:    true,
-			NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
-			LFS:            opts.LFS,
-			RemoteAddress:  remoteAddress,
-		}
-		if opts.LFS {
-			mirrorModel.LFSEndpoint = opts.LFSEndpoint
-		}
-
-		if opts.MirrorInterval != "" {
-			parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
-			if err != nil {
-				log.Error("Failed to set Interval: %v", err)
-				return repo, err
-			}
-			if parsedInterval == 0 {
-				mirrorModel.Interval = 0
-				mirrorModel.NextUpdateUnix = 0
-			} else if parsedInterval < setting.Mirror.MinInterval {
-				err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
-				log.Error("Interval: %s is too frequent", opts.MirrorInterval)
-				return repo, err
-			} else {
-				mirrorModel.Interval = parsedInterval
-				mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
-			}
-		}
-
-		if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
-			return repo, fmt.Errorf("InsertOne: %w", err)
-		}
-
-		repo.IsMirror = true
-		if err = UpdateRepository(ctx, repo, false); err != nil {
-			return nil, err
-		}
-
-		// this is necessary for sync local tags from remote
-		configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName())
-		if stdout, _, err := git.NewCommand(ctx, "config").
-			AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`).
-			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
-			log.Error("MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err)
-			return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*): %w", err)
-		}
-	} else {
-		if err = UpdateRepoSize(ctx, repo); err != nil {
-			log.Error("Failed to update size for repository: %v", err)
-		}
-		if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
-			return nil, err
-		}
-	}
-
-	return repo, committer.Commit()
-}
-
-// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
-// This also removes possible user credentials.
-func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
-	cmd := git.NewCommand(ctx, "remote", "rm", "origin")
-	// if the origin does not exist
-	_, stderr, err := cmd.RunStdString(&git.RunOpts{
-		Dir: repoPath,
-	})
-	if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
-		return err
-	}
-	return nil
-}
-
-// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
-func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
-	repoPath := repo.RepoPath()
-	if err := CreateDelegateHooks(repoPath); err != nil {
-		return repo, fmt.Errorf("createDelegateHooks: %w", err)
-	}
-	if repo.HasWiki() {
-		if err := CreateDelegateHooks(repo.WikiPath()); err != nil {
-			return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
-		}
-	}
-
-	_, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
-	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
-		return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
-	}
-
-	if repo.HasWiki() {
-		if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil {
-			return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
-		}
-	}
-
-	return repo, UpdateRepository(ctx, repo, false)
-}
-
 // SyncRepoTags synchronizes releases table with repository tags
 func SyncRepoTags(ctx context.Context, repoID int64) error {
 	repo, err := repo_model.GetRepositoryByID(ctx, repoID)
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index b13c4c2ddb..0f649acba3 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -23,7 +23,6 @@ import (
 	"code.gitea.io/gitea/modules/indexer/stats"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
-	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
@@ -693,7 +692,7 @@ func SettingsPost(ctx *context.Context) {
 		}
 		repo.IsMirror = false
 
-		if _, err := repo_module.CleanUpMigrateInfo(ctx, repo); err != nil {
+		if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
 			ctx.ServerError("CleanUpMigrateInfo", err)
 			return
 		} else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 1c9824fe3a..87691bf729 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -120,7 +120,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
 	r.DefaultBranch = repo.DefaultBranch
 	r.Description = repo.Description
 
-	r, err = repo_module.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
+	r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
 		RepoName:       g.repoName,
 		Description:    repo.Description,
 		OriginalURL:    repo.OriginalURL,
diff --git a/services/repository/migrate.go b/services/repository/migrate.go
new file mode 100644
index 0000000000..51fdd90f54
--- /dev/null
+++ b/services/repository/migrate.go
@@ -0,0 +1,264 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/lfs"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/migration"
+	repo_module "code.gitea.io/gitea/modules/repository"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
+)
+
+// MigrateRepositoryGitData starts migrating git related data after created migrating repository
+func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
+	repo *repo_model.Repository, opts migration.MigrateOptions,
+	httpTransport *http.Transport,
+) (*repo_model.Repository, error) {
+	repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
+
+	if u.IsOrganization() {
+		t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
+		if err != nil {
+			return nil, err
+		}
+		repo.NumWatches = t.NumMembers
+	} else {
+		repo.NumWatches = 1
+	}
+
+	migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
+
+	var err error
+	if err = util.RemoveAll(repoPath); err != nil {
+		return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err)
+	}
+
+	if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
+		Mirror:        true,
+		Quiet:         true,
+		Timeout:       migrateTimeout,
+		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
+	}); err != nil {
+		if errors.Is(err, context.DeadlineExceeded) {
+			return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err)
+		}
+		return repo, fmt.Errorf("Clone: %w", err)
+	}
+
+	if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
+		return repo, err
+	}
+
+	if opts.Wiki {
+		wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
+		wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
+		if len(wikiRemotePath) > 0 {
+			if err := util.RemoveAll(wikiPath); err != nil {
+				return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
+			}
+
+			if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
+				Mirror:        true,
+				Quiet:         true,
+				Timeout:       migrateTimeout,
+				Branch:        "master",
+				SkipTLSVerify: setting.Migrations.SkipTLSVerify,
+			}); err != nil {
+				log.Warn("Clone wiki: %v", err)
+				if err := util.RemoveAll(wikiPath); err != nil {
+					return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
+				}
+			} else {
+				if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
+					return repo, err
+				}
+			}
+		}
+	}
+
+	if repo.OwnerID == u.ID {
+		repo.Owner = u
+	}
+
+	if err = repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
+		return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
+	}
+
+	if stdout, _, err := git.NewCommand(ctx, "update-server-info").
+		SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
+		RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
+		log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+		return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
+	}
+
+	gitRepo, err := git.OpenRepository(ctx, repoPath)
+	if err != nil {
+		return repo, fmt.Errorf("OpenRepository: %w", err)
+	}
+	defer gitRepo.Close()
+
+	repo.IsEmpty, err = gitRepo.IsEmpty()
+	if err != nil {
+		return repo, fmt.Errorf("git.IsEmpty: %w", err)
+	}
+
+	if !repo.IsEmpty {
+		if len(repo.DefaultBranch) == 0 {
+			// Try to get HEAD branch and set it as default branch.
+			headBranch, err := gitRepo.GetHEADBranch()
+			if err != nil {
+				return repo, fmt.Errorf("GetHEADBranch: %w", err)
+			}
+			if headBranch != nil {
+				repo.DefaultBranch = headBranch.Name
+			}
+		}
+
+		if _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
+			return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err)
+		}
+
+		if !opts.Releases {
+			// note: this will greatly improve release (tag) sync
+			// for pull-mirrors with many tags
+			repo.IsMirror = opts.Mirror
+			if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
+				log.Error("Failed to synchronize tags to releases for repository: %v", err)
+			}
+		}
+
+		if opts.LFS {
+			endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
+			lfsClient := lfs.NewClient(endpoint, httpTransport)
+			if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
+				log.Error("Failed to store missing LFS objects for repository: %v", err)
+			}
+		}
+	}
+
+	ctx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	defer committer.Close()
+
+	if opts.Mirror {
+		remoteAddress, err := util.SanitizeURL(opts.CloneAddr)
+		if err != nil {
+			return repo, err
+		}
+		mirrorModel := repo_model.Mirror{
+			RepoID:         repo.ID,
+			Interval:       setting.Mirror.DefaultInterval,
+			EnablePrune:    true,
+			NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
+			LFS:            opts.LFS,
+			RemoteAddress:  remoteAddress,
+		}
+		if opts.LFS {
+			mirrorModel.LFSEndpoint = opts.LFSEndpoint
+		}
+
+		if opts.MirrorInterval != "" {
+			parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
+			if err != nil {
+				log.Error("Failed to set Interval: %v", err)
+				return repo, err
+			}
+			if parsedInterval == 0 {
+				mirrorModel.Interval = 0
+				mirrorModel.NextUpdateUnix = 0
+			} else if parsedInterval < setting.Mirror.MinInterval {
+				err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
+				log.Error("Interval: %s is too frequent", opts.MirrorInterval)
+				return repo, err
+			} else {
+				mirrorModel.Interval = parsedInterval
+				mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
+			}
+		}
+
+		if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
+			return repo, fmt.Errorf("InsertOne: %w", err)
+		}
+
+		repo.IsMirror = true
+		if err = UpdateRepository(ctx, repo, false); err != nil {
+			return nil, err
+		}
+
+		// this is necessary for sync local tags from remote
+		configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName())
+		if stdout, _, err := git.NewCommand(ctx, "config").
+			AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`).
+			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
+			log.Error("MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+			return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*): %w", err)
+		}
+	} else {
+		if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
+			log.Error("Failed to update size for repository: %v", err)
+		}
+		if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
+			return nil, err
+		}
+	}
+
+	return repo, committer.Commit()
+}
+
+// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
+// This also removes possible user credentials.
+func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
+	cmd := git.NewCommand(ctx, "remote", "rm", "origin")
+	// if the origin does not exist
+	_, stderr, err := cmd.RunStdString(&git.RunOpts{
+		Dir: repoPath,
+	})
+	if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
+		return err
+	}
+	return nil
+}
+
+// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
+func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
+	repoPath := repo.RepoPath()
+	if err := repo_module.CreateDelegateHooks(repoPath); err != nil {
+		return repo, fmt.Errorf("createDelegateHooks: %w", err)
+	}
+	if repo.HasWiki() {
+		if err := repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
+			return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
+		}
+	}
+
+	_, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+		return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
+	}
+
+	if repo.HasWiki() {
+		if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil {
+			return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
+		}
+	}
+
+	return repo, UpdateRepository(ctx, repo, false)
+}
diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go
index 2e71b80fbb..77050c4bbc 100644
--- a/tests/integration/mirror_pull_test.go
+++ b/tests/integration/mirror_pull_test.go
@@ -14,7 +14,6 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/migration"
-	"code.gitea.io/gitea/modules/repository"
 	mirror_service "code.gitea.io/gitea/services/mirror"
 	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -52,7 +51,7 @@ func TestMirrorPull(t *testing.T) {
 
 	ctx := context.Background()
 
-	mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
+	mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
 	assert.NoError(t, err)
 
 	gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)

From e71eb8930a5d0f60874b038c223498b41ad65592 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 1 Mar 2024 15:11:51 +0800
Subject: [PATCH 217/679] Refactor some Str2html code (#29397)

This PR touches the most interesting part of the "template refactoring".

1. Unclear variable type. Especially for "web/feed/convert.go":
sometimes it uses text, sometimes it uses HTML.
2. Assign text content to "RenderedContent" field, for example: `
project.RenderedContent = project.Description` in web/org/projects.go
3. Assign rendered content to text field, for example: `r.Note =
rendered content` in web/repo/release.go
4. (possible) Incorrectly calling `{{Str2html
.PackageDescriptor.Metadata.ReleaseNotes}}` in
package/content/nuget.tmpl, I guess the name Str2html misleads
developers to use it to "render string to html", but it only sanitizes.
if ReleaseNotes really contains HTML, then this is not a problem.
---
 models/issues/comment.go                      |  5 ++--
 models/issues/issue.go                        |  3 +-
 models/issues/milestone.go                    |  5 ++--
 models/project/project.go                     |  3 +-
 models/repo/release.go                        |  3 +-
 modules/markup/html_test.go                   |  8 ++---
 modules/markup/markdown/markdown.go           |  5 ++--
 modules/markup/markdown/markdown_test.go      | 30 ++++++++++---------
 modules/templates/util_render.go              |  2 +-
 modules/templates/util_string.go              | 15 ++++++++++
 routers/web/feed/convert.go                   | 21 ++++++++-----
 routers/web/feed/profile.go                   |  2 +-
 routers/web/org/projects.go                   |  5 ++--
 routers/web/repo/issue.go                     |  3 +-
 routers/web/repo/release.go                   |  2 +-
 templates/mail/release.tmpl                   |  2 +-
 templates/org/header.tmpl                     |  2 +-
 templates/org/home.tmpl                       |  2 +-
 templates/package/content/nuget.tmpl          |  2 +-
 templates/projects/list.tmpl                  |  2 +-
 templates/projects/view.tmpl                  |  2 +-
 templates/repo/diff/comments.tmpl             |  2 +-
 templates/repo/issue/milestone_issues.tmpl    |  2 +-
 templates/repo/issue/milestones.tmpl          |  2 +-
 templates/repo/issue/view_content.tmpl        |  2 +-
 .../repo/issue/view_content/attachments.tmpl  |  4 +--
 .../repo/issue/view_content/comments.tmpl     |  6 ++--
 .../repo/issue/view_content/conversation.tmpl |  2 +-
 templates/repo/release/list.tmpl              |  2 +-
 templates/shared/user/profile_big_avatar.tmpl |  2 +-
 templates/user/dashboard/milestones.tmpl      |  2 +-
 templates/user/profile.tmpl                   |  2 +-
 32 files changed, 91 insertions(+), 61 deletions(-)

diff --git a/models/issues/comment.go b/models/issues/comment.go
index c7b22f3cca..da91a83384 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -8,6 +8,7 @@ package issues
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"strconv"
 	"unicode/utf8"
 
@@ -259,8 +260,8 @@ type Comment struct {
 	CommitID        int64
 	Line            int64 // - previous line / + proposed line
 	TreePath        string
-	Content         string `xorm:"LONGTEXT"`
-	RenderedContent string `xorm:"-"`
+	Content         string        `xorm:"LONGTEXT"`
+	RenderedContent template.HTML `xorm:"-"`
 
 	// Path represents the 4 lines of code cemented by this comment
 	Patch       string `xorm:"-"`
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 90aad10bb9..563a780dcb 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -7,6 +7,7 @@ package issues
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"regexp"
 	"slices"
 
@@ -105,7 +106,7 @@ type Issue struct {
 	OriginalAuthorID int64                  `xorm:"index"`
 	Title            string                 `xorm:"name"`
 	Content          string                 `xorm:"LONGTEXT"`
-	RenderedContent  string                 `xorm:"-"`
+	RenderedContent  template.HTML          `xorm:"-"`
 	Labels           []*Label               `xorm:"-"`
 	MilestoneID      int64                  `xorm:"INDEX"`
 	Milestone        *Milestone             `xorm:"-"`
diff --git a/models/issues/milestone.go b/models/issues/milestone.go
index f663d42fe9..ea52a64c81 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -6,6 +6,7 @@ package issues
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
@@ -47,8 +48,8 @@ type Milestone struct {
 	RepoID          int64                  `xorm:"INDEX"`
 	Repo            *repo_model.Repository `xorm:"-"`
 	Name            string
-	Content         string `xorm:"TEXT"`
-	RenderedContent string `xorm:"-"`
+	Content         string        `xorm:"TEXT"`
+	RenderedContent template.HTML `xorm:"-"`
 	IsClosed        bool
 	NumIssues       int
 	NumClosedIssues int
diff --git a/models/project/project.go b/models/project/project.go
index d2fca6cdc8..42b06e58c9 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -6,6 +6,7 @@ package project
 import (
 	"context"
 	"fmt"
+	"html/template"
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -100,7 +101,7 @@ type Project struct {
 	CardType    CardType
 	Type        Type
 
-	RenderedContent string `xorm:"-"`
+	RenderedContent template.HTML `xorm:"-"`
 
 	CreatedUnix    timeutil.TimeStamp `xorm:"INDEX created"`
 	UpdatedUnix    timeutil.TimeStamp `xorm:"INDEX updated"`
diff --git a/models/repo/release.go b/models/repo/release.go
index 9287931dd5..a9f65f6c3e 100644
--- a/models/repo/release.go
+++ b/models/repo/release.go
@@ -7,6 +7,7 @@ package repo
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"net/url"
 	"sort"
 	"strconv"
@@ -80,7 +81,7 @@ type Release struct {
 	NumCommits       int64
 	NumCommitsBehind int64              `xorm:"-"`
 	Note             string             `xorm:"TEXT"`
-	RenderedNote     string             `xorm:"-"`
+	RenderedNote     template.HTML      `xorm:"-"`
 	IsDraft          bool               `xorm:"NOT NULL DEFAULT false"`
 	IsPrerelease     bool               `xorm:"NOT NULL DEFAULT false"`
 	IsTag            bool               `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index cb29431d4b..ccb63c6bab 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -397,7 +397,7 @@ func TestRender_ShortLinks(t *testing.T) {
 			},
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 		buffer, err = markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
@@ -407,7 +407,7 @@ func TestRender_ShortLinks(t *testing.T) {
 			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 	}
 
 	mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
@@ -510,7 +510,7 @@ func TestRender_RelativeImages(t *testing.T) {
 			Metas: localMetas,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 		buffer, err = markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
@@ -520,7 +520,7 @@ func TestRender_RelativeImages(t *testing.T) {
 			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 	}
 
 	rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 771162b9a3..f0b1afa27e 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -6,6 +6,7 @@ package markdown
 
 import (
 	"fmt"
+	"html/template"
 	"io"
 	"strings"
 	"sync"
@@ -262,12 +263,12 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 }
 
 // RenderString renders Markdown string to HTML with all specific handling stuff and return string
-func RenderString(ctx *markup.RenderContext, content string) (string, error) {
+func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
 	var buf strings.Builder
 	if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
 		return "", err
 	}
-	return buf.String(), nil
+	return template.HTML(buf.String()), nil
 }
 
 // RenderRaw renders Markdown to HTML without handling special links.
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index 398efedb98..dbf95e5e62 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -5,6 +5,7 @@ package markdown_test
 
 import (
 	"context"
+	"html/template"
 	"os"
 	"strings"
 	"testing"
@@ -56,7 +57,7 @@ func TestRender_StandardLinks(t *testing.T) {
 			},
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 
 		buffer, err = markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
@@ -66,7 +67,7 @@ func TestRender_StandardLinks(t *testing.T) {
 			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 	}
 
 	googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
@@ -90,7 +91,7 @@ func TestRender_Images(t *testing.T) {
 			},
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 	}
 
 	url := "../../.images/src/02/train.jpg"
@@ -299,7 +300,7 @@ func TestTotal_RenderWiki(t *testing.T) {
 			IsWiki: true,
 		}, sameCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, answers[i], line)
+		assert.Equal(t, template.HTML(answers[i]), line)
 	}
 
 	testCases := []string{
@@ -324,7 +325,7 @@ func TestTotal_RenderWiki(t *testing.T) {
 			IsWiki: true,
 		}, testCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, testCases[i+1], line)
+		assert.Equal(t, template.HTML(testCases[i+1]), line)
 	}
 }
 
@@ -343,7 +344,7 @@ func TestTotal_RenderString(t *testing.T) {
 			Metas: localMetas,
 		}, sameCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, answers[i], line)
+		assert.Equal(t, template.HTML(answers[i]), line)
 	}
 
 	testCases := []string{}
@@ -356,7 +357,7 @@ func TestTotal_RenderString(t *testing.T) {
 			},
 		}, testCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, testCases[i+1], line)
+		assert.Equal(t, template.HTML(testCases[i+1]), line)
 	}
 }
 
@@ -423,7 +424,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
 `
 	res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
 	assert.NoError(t, err)
-	assert.Equal(t, expected, res)
+	assert.Equal(t, template.HTML(expected), res)
 }
 
 func TestColorPreview(t *testing.T) {
@@ -457,7 +458,7 @@ func TestColorPreview(t *testing.T) {
 	for _, test := range positiveTests {
 		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
-		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 
 	}
 
@@ -524,7 +525,7 @@ func TestMathBlock(t *testing.T) {
 	for _, test := range testcases {
 		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
-		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 
 	}
 }
@@ -562,12 +563,12 @@ foo: bar
 	for _, test := range testcases {
 		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
-		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 	}
 }
 
 func TestRenderLinks(t *testing.T) {
-	input := `  space @mention-user  
+	input := `  space @mention-user${SPACE}${SPACE}
 /just/a/path.bin
 https://example.com/file.bin
 [local link](file.bin)
@@ -588,8 +589,9 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 mail@domain.com
 @mention-user test
 #123
-  space  
+  space${SPACE}${SPACE}
 `
+	input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming
 	cases := []struct {
 		Links    markup.Links
 		IsWiki   bool
@@ -952,6 +954,6 @@ space</p>
 	for i, c := range cases {
 		result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
 		assert.NoError(t, err, "Unexpected error in testcase: %v", i)
-		assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i)
+		assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
 	}
 }
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 1d9635410b..cdff31698c 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -208,7 +208,7 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n
 	if err != nil {
 		log.Error("RenderString: %v", err)
 	}
-	return template.HTML(output)
+	return output
 }
 
 func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 2771b1e223..479b755da1 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -4,6 +4,8 @@
 package templates
 
 import (
+	"fmt"
+	"html/template"
 	"strings"
 
 	"code.gitea.io/gitea/modules/base"
@@ -17,6 +19,19 @@ func NewStringUtils() *StringUtils {
 	return &stringUtils
 }
 
+func (su *StringUtils) ToString(v any) string {
+	switch v := v.(type) {
+	case string:
+		return v
+	case template.HTML:
+		return string(v)
+	case fmt.Stringer:
+		return v.String()
+	default:
+		return fmt.Sprint(v)
+	}
+}
+
 func (su *StringUtils) HasPrefix(s, prefix string) bool {
 	return strings.HasPrefix(s, prefix)
 }
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 298fe0bb39..3a2de1d9a1 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -50,7 +50,7 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
 
 // renderMarkdown creates a minimal markdown render context from an action.
 // If rendering fails, the original markdown text is returned
-func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
+func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML {
 	markdownCtx := &markup.RenderContext{
 		Ctx: ctx,
 		Links: markup.Links{
@@ -64,7 +64,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content
 	}
 	markdown, err := markdown.RenderString(markdownCtx, content)
 	if err != nil {
-		return content
+		return templates.Str2html(content) // old code did so: use Str2html to render in tmpl
 	}
 	return markdown
 }
@@ -74,7 +74,11 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 	for _, act := range actions {
 		act.LoadActUser(ctx)
 
-		var content, desc, title string
+		// TODO: the code seems quite strange (maybe not right)
+		// sometimes it uses text content but sometimes it uses HTML content
+		// it should clearly defines which kind of content it should use for the feed items: plan text or rich HTML
+		var title, desc string
+		var content template.HTML
 
 		link := &feeds.Link{Href: act.GetCommentHTMLURL(ctx)}
 
@@ -228,7 +232,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 				desc = act.GetIssueTitle(ctx)
 				comment := act.GetIssueInfos()[1]
 				if len(comment) != 0 {
-					desc += "\n\n" + renderMarkdown(ctx, act, comment)
+					desc += "\n\n" + string(renderMarkdown(ctx, act, comment))
 				}
 			case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
 				desc = act.GetIssueInfos()[1]
@@ -239,7 +243,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 			}
 		}
 		if len(content) == 0 {
-			content = desc
+			content = templates.Str2html(desc)
 		}
 
 		items = append(items, &feeds.Item{
@@ -253,7 +257,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 			},
 			Id:      fmt.Sprintf("%v: %v", strconv.FormatInt(act.ID, 10), link.Href),
 			Created: act.CreatedUnix.AsTime(),
-			Content: content,
+			Content: string(content),
 		})
 	}
 	return items, err
@@ -282,7 +286,8 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
 			return nil, err
 		}
 
-		var title, content string
+		var title string
+		var content template.HTML
 
 		if rel.IsTag {
 			title = rel.TagName
@@ -311,7 +316,7 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
 				Email: rel.Publisher.GetEmail(),
 			},
 			Id:      fmt.Sprintf("%v: %v", strconv.FormatInt(rel.ID, 10), link.Href),
-			Content: content,
+			Content: string(content),
 		})
 	}
 
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 2b70aad17b..08cbcd9e12 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -58,7 +58,7 @@ func showUserFeed(ctx *context.Context, formatType string) {
 	feed := &feeds.Feed{
 		Title:       ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()),
 		Link:        &feeds.Link{Href: ctx.ContextUser.HTMLURL()},
-		Description: ctxUserDescription,
+		Description: string(ctxUserDescription),
 		Created:     time.Now(),
 	}
 
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 338558fa23..f2db4a4579 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
@@ -104,7 +105,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	for _, project := range projects {
-		project.RenderedContent = project.Description
+		project.RenderedContent = templates.Str2html(project.Description) // FIXME: is it right? why not render?
 	}
 
 	err = shared_user.LoadHeaderCount(ctx)
@@ -395,7 +396,7 @@ func ViewProject(ctx *context.Context) {
 		}
 	}
 
-	project.RenderedContent = project.Description
+	project.RenderedContent = templates.Str2html(project.Description) // FIXME: is it right? why not render?
 	ctx.Data["LinkedPRs"] = linkedPrsMap
 	ctx.Data["PageIsViewProjects"] = true
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index d13c658d05..702aa7201b 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -42,6 +42,7 @@ import (
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/templates/vars"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -1760,7 +1761,7 @@ func ViewIssue(ctx *context.Context) {
 				// so "|" is used as delimeter to mark the new format
 				if comment.Content[0] != '|' {
 					// handle old time comments that have formatted text stored
-					comment.RenderedContent = comment.Content
+					comment.RenderedContent = templates.Str2html(comment.Content)
 					comment.Content = ""
 				} else {
 					// else it's just a duration in seconds to pass on to the frontend
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index a730c2d3b7..c6d8c45af1 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -113,7 +113,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
 			cacheUsers[r.PublisherID] = r.Publisher
 		}
 
-		r.Note, err = markdown.RenderString(&markup.RenderContext{
+		r.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
 			Links: markup.Links{
 				Base: ctx.Repo.RepoLink,
 			},
diff --git a/templates/mail/release.tmpl b/templates/mail/release.tmpl
index 62a16573c6..90a3caa4c5 100644
--- a/templates/mail/release.tmpl
+++ b/templates/mail/release.tmpl
@@ -22,7 +22,7 @@
 		{{.locale.Tr "mail.release.note"}}<br>
 		{{- if eq .Release.RenderedNote ""}}
 		{{else}}
-			{{.Release.RenderedNote | Str2html}}
+			{{.Release.RenderedNote}}
 		{{end -}}
 	</p>
 	<br><br>
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 8423fd7d3b..efbbc43b1d 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -18,7 +18,7 @@
 				{{end}}
 			</span>
 		</div>
-		{{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription | Str2html}}</div>{{end}}
+		{{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription}}</div>{{end}}
 		<div class="text light meta gt-mt-2">
 			{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
 			{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index 4e33b1af72..892ba0da5b 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -6,7 +6,7 @@
 		<div class="ui mobile reversed stackable grid">
 			<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
 				{{if .ProfileReadme}}
-					<div id="readme_profile" class="markup">{{.ProfileReadme | Str2html}}</div>
+					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{end}}
 				{{template "explore/repo_search" .}}
 				{{template "explore/repo_list" .}}
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl
index f84288629d..2008cf4cc8 100644
--- a/templates/package/content/nuget.tmpl
+++ b/templates/package/content/nuget.tmpl
@@ -20,7 +20,7 @@
 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
 		<div class="ui attached segment">
 			{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
-			{{if .PackageDescriptor.Metadata.ReleaseNotes}}{{Str2html .PackageDescriptor.Metadata.ReleaseNotes}}{{end}}
+			{{if .PackageDescriptor.Metadata.ReleaseNotes}}{{.PackageDescriptor.Metadata.ReleaseNotes}}{{end}}
 		</div>
 	{{end}}
 
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index cbff82dd70..30fbd498a4 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -75,7 +75,7 @@
 			</div>
 			{{if .Description}}
 			<div class="content">
-				{{.RenderedContent|Str2html}}
+				{{.RenderedContent}}
 			</div>
 			{{end}}
 		</li>
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index b3ad03c354..3792ccca0e 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -58,7 +58,7 @@
 		{{end}}
 	</div>
 
-	<div class="content">{{$.Project.RenderedContent|Str2html}}</div>
+	<div class="content">{{$.Project.RenderedContent}}</div>
 
 	<div class="divider"></div>
 </div>
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index e567417fa6..e00487a22c 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -55,7 +55,7 @@
 		<div class="ui attached segment comment-body">
 			<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
 			{{if .RenderedContent}}
-				{{.RenderedContent|Str2html}}
+				{{.RenderedContent}}
 			{{else}}
 				<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 			{{end}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index d9495d9b77..35a8a77680 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -22,7 +22,7 @@
 		</div>
 		{{if .Milestone.RenderedContent}}
 		<div class="markup content gt-mb-4">
-				{{.Milestone.RenderedContent|Str2html}}
+				{{.Milestone.RenderedContent}}
 		</div>
 		{{end}}
 		<div class="gt-df gt-fc gt-gap-3">
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 698e3fffba..363ba7e3a2 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -82,7 +82,7 @@
 					</div>
 					{{if .Content}}
 						<div class="markup content">
-							{{.RenderedContent|Str2html}}
+							{{.RenderedContent}}
 						</div>
 					{{end}}
 				</li>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index b5441872a3..ee824b76b1 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -54,7 +54,7 @@
 					<div class="ui attached segment comment-body" role="article">
 						<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission $.IsIssuePoster}}data-can-edit="true"{{end}}>
 							{{if .Issue.RenderedContent}}
-								{{.Issue.RenderedContent|Str2html}}
+								{{.Issue.RenderedContent}}
 							{{else}}
 								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 							{{end}}
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
index 1fb6f2f2c2..58f4c702b3 100644
--- a/templates/repo/issue/view_content/attachments.tmpl
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -8,7 +8,7 @@
 			<div class="gt-f1 gt-p-3">
 				<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					{{if FilenameIsImage .Name}}
-						{{if not (StringUtils.Contains $.Content .UUID)}}
+						{{if not (StringUtils.Contains (StringUtils.ToString $.Content) .UUID)}}
 							{{$hasThumbnails = true}}
 						{{end}}
 						{{svg "octicon-file"}}
@@ -29,7 +29,7 @@
 		<div class="ui small thumbnails">
 			{{- range .Attachments -}}
 				{{if FilenameIsImage .Name}}
-					{{if not (StringUtils.Contains $.Content .UUID)}}
+					{{if not (StringUtils.Contains (StringUtils.ToString $.Content) .UUID)}}
 					<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
 						<img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					</a>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 562e44c791..36ef5751ae 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -61,7 +61,7 @@
 					<div class="ui attached segment comment-body" role="article">
 						<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
 							{{if .RenderedContent}}
-								{{.RenderedContent|Str2html}}
+								{{.RenderedContent}}
 							{{else}}
 								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 							{{end}}
@@ -432,7 +432,7 @@
 						<div class="ui attached segment comment-body">
 							<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
 								{{if .RenderedContent}}
-									{{.RenderedContent|Str2html}}
+									{{.RenderedContent}}
 								{{else}}
 									<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 								{{end}}
@@ -631,7 +631,7 @@
 							<div class="ui attached segment">
 								<div class="render-content markup">
 									{{if .RenderedContent}}
-										{{.RenderedContent|Str2html}}
+										{{.RenderedContent}}
 									{{else}}
 										<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 									{{end}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 1bad0e9b55..1afc744aee 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -87,7 +87,7 @@
 						<div class="text comment-content">
 							<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
 							{{if .RenderedContent}}
-								{{.RenderedContent|Str2html}}
+								{{.RenderedContent}}
 							{{else}}
 								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 							{{end}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 5b747c2bf9..7f38fa3ff0 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -58,7 +58,7 @@
 							{{end}}
 						</p>
 						<div class="markup desc">
-							{{Str2html $release.Note}}
+							{{$release.RenderedNote}}
 						</div>
 						<div class="divider"></div>
 						<details class="download" {{if eq $idx 0}}open{{end}}>
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 19a3b25cc5..88d3b9a6e5 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -70,7 +70,7 @@
 			{{end}}
 			{{if $.RenderedDescription}}
 				<li>
-					<div class="render-content markup">{{$.RenderedDescription|Str2html}}</div>
+					<div class="render-content markup">{{$.RenderedDescription}}</div>
 				</li>
 			{{end}}
 			{{range .OpenIDs}}
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 1829021ff4..737a0f7e2b 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -141,7 +141,7 @@
 							</div>
 							{{if .Content}}
 								<div class="markup content">
-									{{.RenderedContent|Str2html}}
+									{{.RenderedContent}}
 								</div>
 							{{end}}
 						</li>
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index 426b5f042a..37590fc2fa 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -29,7 +29,7 @@
 				{{else if eq .TabName "followers"}}
 					{{template "repo/user_cards" .}}
 				{{else if eq .TabName "overview"}}
-					<div id="readme_profile" class="markup">{{.ProfileReadme | Str2html}}</div>
+					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{else}}
 					{{template "explore/repo_search" .}}
 					{{template "explore/repo_list" .}}

From cb52b17f92e2d2293f7c003649743464492bca48 Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Fri, 1 Mar 2024 03:23:28 -0500
Subject: [PATCH 218/679] Add admin API route for managing user's badges
 (#23106)

Fix #22785

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/migrations/migrations.go      |   2 +
 models/migrations/v1_22/v287.go      |  46 +++++++
 models/migrations/v1_22/v287_test.go |  57 +++++++++
 models/user/badge.go                 |  85 ++++++++++++-
 modules/structs/user.go              |  31 +++++
 routers/api/v1/admin/user_badge.go   | 124 +++++++++++++++++++
 routers/api/v1/api.go                |   3 +
 routers/api/v1/swagger/options.go    |   6 +
 templates/swagger/v1_json.tmpl       | 171 ++++++++++++++++++++++++++-
 9 files changed, 523 insertions(+), 2 deletions(-)
 create mode 100644 models/migrations/v1_22/v287.go
 create mode 100644 models/migrations/v1_22/v287_test.go
 create mode 100644 routers/api/v1/admin/user_badge.go

diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index beb1f3bb96..516eb53f62 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -558,6 +558,8 @@ var migrations = []Migration{
 	NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun),
 	// v286 -> v287
 	NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
+	// v287 -> v288
+	NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go
new file mode 100644
index 0000000000..c8b1593286
--- /dev/null
+++ b/models/migrations/v1_22/v287.go
@@ -0,0 +1,46 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+type BadgeUnique struct {
+	ID   int64  `xorm:"pk autoincr"`
+	Slug string `xorm:"UNIQUE"`
+}
+
+func (BadgeUnique) TableName() string {
+	return "badge"
+}
+
+func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error {
+	type Badge struct {
+		Slug string
+	}
+
+	err := x.Sync(new(Badge))
+	if err != nil {
+		return err
+	}
+
+	sess := x.NewSession()
+	defer sess.Close()
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+
+	_, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL")
+	if err != nil {
+		return err
+	}
+
+	err = sess.Sync(new(BadgeUnique))
+	if err != nil {
+		return err
+	}
+
+	return sess.Commit()
+}
diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go
new file mode 100644
index 0000000000..19c7ae3b91
--- /dev/null
+++ b/models/migrations/v1_22/v287_test.go
@@ -0,0 +1,57 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"fmt"
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_UpdateBadgeColName(t *testing.T) {
+	type Badge struct {
+		ID          int64 `xorm:"pk autoincr"`
+		Description string
+		ImageURL    string
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(BadgeUnique), new(Badge))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	oldBadges := []Badge{
+		{ID: 1, Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
+		{ID: 2, Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
+		{ID: 3, Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
+	}
+
+	for _, badge := range oldBadges {
+		_, err := x.Insert(&badge)
+		assert.NoError(t, err)
+	}
+
+	if err := UseSlugInsteadOfIDForBadges(x); err != nil {
+		assert.NoError(t, err)
+		return
+	}
+
+	got := []BadgeUnique{}
+	if err := x.Table("badge").Asc("id").Find(&got); !assert.NoError(t, err) {
+		return
+	}
+
+	for i, e := range oldBadges {
+		got := got[i]
+		assert.Equal(t, e.ID, got.ID)
+		assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
+	}
+
+	// TODO: check if badges have been updated
+}
diff --git a/models/user/badge.go b/models/user/badge.go
index ee52b44cf5..3ff3530a36 100644
--- a/models/user/badge.go
+++ b/models/user/badge.go
@@ -5,13 +5,15 @@ package user
 
 import (
 	"context"
+	"fmt"
 
 	"code.gitea.io/gitea/models/db"
 )
 
 // Badge represents a user badge
 type Badge struct {
-	ID          int64 `xorm:"pk autoincr"`
+	ID          int64  `xorm:"pk autoincr"`
+	Slug        string `xorm:"UNIQUE"`
 	Description string
 	ImageURL    string
 }
@@ -39,3 +41,84 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
 	count, err := sess.FindAndCount(&badges)
 	return badges, count, err
 }
+
+// CreateBadge creates a new badge.
+func CreateBadge(ctx context.Context, badge *Badge) error {
+	_, err := db.GetEngine(ctx).Insert(badge)
+	return err
+}
+
+// GetBadge returns a badge
+func GetBadge(ctx context.Context, slug string) (*Badge, error) {
+	badge := new(Badge)
+	has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
+	if !has {
+		return nil, err
+	}
+	return badge, err
+}
+
+// UpdateBadge updates a badge based on its slug.
+func UpdateBadge(ctx context.Context, badge *Badge) error {
+	_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
+	return err
+}
+
+// DeleteBadge deletes a badge.
+func DeleteBadge(ctx context.Context, badge *Badge) error {
+	_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
+	return err
+}
+
+// AddUserBadge adds a badge to a user.
+func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
+	return AddUserBadges(ctx, u, []*Badge{badge})
+}
+
+// AddUserBadges adds badges to a user.
+func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		for _, badge := range badges {
+			// hydrate badge and check if it exists
+			has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
+			if err != nil {
+				return err
+			} else if !has {
+				return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
+			}
+			if err := db.Insert(ctx, &UserBadge{
+				BadgeID: badge.ID,
+				UserID:  u.ID,
+			}); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+// RemoveUserBadge removes a badge from a user.
+func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
+	return RemoveUserBadges(ctx, u, []*Badge{badge})
+}
+
+// RemoveUserBadges removes badges from a user.
+func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		for _, badge := range badges {
+			if _, err := db.GetEngine(ctx).
+				Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
+				Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
+				Delete(&UserBadge{}); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+// RemoveAllUserBadges removes all badges from a user.
+func RemoveAllUserBadges(ctx context.Context, u *User) error {
+	_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
+	return err
+}
diff --git a/modules/structs/user.go b/modules/structs/user.go
index 0df67894b0..c43558be5d 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -1,4 +1,5 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2023 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package structs
@@ -108,3 +109,33 @@ type UpdateUserAvatarOption struct {
 	// image must be base64 encoded
 	Image string `json:"image" binding:"Required"`
 }
+
+// Badge represents a user badge
+// swagger:model
+type Badge struct {
+	ID          int64  `json:"id"`
+	Slug        string `json:"slug"`
+	Description string `json:"description"`
+	ImageURL    string `json:"image_url"`
+}
+
+// UserBadge represents a user badge
+// swagger:model
+type UserBadge struct {
+	ID      int64 `json:"id"`
+	BadgeID int64 `json:"badge_id"`
+	UserID  int64 `json:"user_id"`
+}
+
+// UserBadgeOption options for link between users and badges
+type UserBadgeOption struct {
+	// example: ["badge1","badge2"]
+	BadgeSlugs []string `json:"badge_slugs" binding:"Required"`
+}
+
+// BadgeList
+// swagger:response BadgeList
+type BadgeList struct {
+	// in:body
+	Body []Badge `json:"body"`
+}
diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go
new file mode 100644
index 0000000000..bacd1f809b
--- /dev/null
+++ b/routers/api/v1/admin/user_badge.go
@@ -0,0 +1,124 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+	"net/http"
+
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+)
+
+// ListUserBadges lists all badges belonging to a user
+func ListUserBadges(ctx *context.APIContext) {
+	// swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
+	// ---
+	// summary: List a user's badges
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: username of user
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/BadgeList"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetUserBadges", err)
+		return
+	}
+
+	ctx.SetTotalCountHeader(maxResults)
+	ctx.JSON(http.StatusOK, &badges)
+}
+
+// AddUserBadges add badges to a user
+func AddUserBadges(ctx *context.APIContext) {
+	// swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
+	// ---
+	// summary: Add a badge to a user
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: username of user
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UserBadgeOption"
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+
+	form := web.GetForm(ctx).(*api.UserBadgeOption)
+	badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+	if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+		ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// DeleteUserBadges delete a badge from a user
+func DeleteUserBadges(ctx *context.APIContext) {
+	// swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
+	// ---
+	// summary: Remove a badge from a user
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: username of user
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UserBadgeOption"
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	form := web.GetForm(ctx).(*api.UserBadgeOption)
+	badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+	if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+		ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge {
+	badges := make([]*user_model.Badge, len(form.BadgeSlugs))
+	for i, badge := range form.BadgeSlugs {
+		badges[i] = &user_model.Badge{
+			Slug: badge,
+		}
+	}
+	return badges
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 0913571c27..1587d413f5 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1519,6 +1519,9 @@ func Routes() *web.Route {
 					m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
 					m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
 					m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
+					m.Get("/badges", admin.ListUserBadges)
+					m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
+					m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
 				}, context.UserAssignmentAPI())
 			})
 			m.Group("/emails", func() {
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 6f7859df62..e03862d7b9 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -190,4 +190,10 @@ type swaggerParameterBodies struct {
 
 	// in:body
 	CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption
+
+	// in:body
+	UserBadgeOption api.UserBadgeOption
+
+	// in:body
+	UserBadgeList api.BadgeList
 }
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index b2bd1bf174..d4c5d9a7ee 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -689,6 +689,109 @@
         }
       }
     },
+    "/admin/users/{username}/badges": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "List a user's badges",
+        "operationId": "adminListUserBadges",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "username of user",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/BadgeList"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Add a badge to a user",
+        "operationId": "adminAddUserBadges",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "username of user",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UserBadgeOption"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Remove a badge from a user",
+        "operationId": "adminDeleteUserBadges",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "username of user",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UserBadgeOption"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/admin/users/{username}/keys": {
       "post": {
         "consumes": [
@@ -17003,6 +17106,45 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "Badge": {
+      "description": "Badge represents a user badge",
+      "type": "object",
+      "properties": {
+        "description": {
+          "type": "string",
+          "x-go-name": "Description"
+        },
+        "id": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "ID"
+        },
+        "image_url": {
+          "type": "string",
+          "x-go-name": "ImageURL"
+        },
+        "slug": {
+          "type": "string",
+          "x-go-name": "Slug"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
+    "BadgeList": {
+      "description": "BadgeList",
+      "type": "object",
+      "properties": {
+        "body": {
+          "description": "in:body",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Badge"
+          },
+          "x-go-name": "Body"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "Branch": {
       "description": "Branch represents a repository branch",
       "type": "object",
@@ -23047,6 +23189,24 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "UserBadgeOption": {
+      "description": "UserBadgeOption options for link between users and badges",
+      "type": "object",
+      "properties": {
+        "badge_slugs": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "BadgeSlugs",
+          "example": [
+            "badge1",
+            "badge2"
+          ]
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "UserHeatmapData": {
       "description": "UserHeatmapData represents the data needed to create a heatmap",
       "type": "object",
@@ -23336,6 +23496,15 @@
         }
       }
     },
+    "BadgeList": {
+      "description": "BadgeList",
+      "schema": {
+        "type": "array",
+        "items": {
+          "$ref": "#/definitions/Badge"
+        }
+      }
+    },
     "Branch": {
       "description": "Branch",
       "schema": {
@@ -24249,7 +24418,7 @@
     "parameterBodies": {
       "description": "parameterBodies",
       "schema": {
-        "$ref": "#/definitions/CreateOrUpdateSecretOption"
+        "$ref": "#/definitions/BadgeList"
       }
     },
     "redirect": {

From fb42972c057364a1dc99dfb528554e7a94415be7 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 1 Mar 2024 18:16:19 +0800
Subject: [PATCH 219/679] Rename Str2html to SanitizeHTML and clarify its
 behavior (#29516)

Str2html was abused a lot. So use a proper name for it: SanitizeHTML

And add some tests to show its behavior.
---
 .../administration/mail-templates.en-us.md    | 10 ++++----
 .../administration/mail-templates.zh-cn.md    | 22 ++++++++---------
 modules/templates/helper.go                   | 24 +++++++++----------
 modules/templates/helper_test.go              |  5 ++++
 routers/web/feed/convert.go                   |  4 ++--
 routers/web/org/projects.go                   |  4 ++--
 routers/web/repo/issue.go                     |  2 +-
 templates/base/alert.tmpl                     |  8 +++----
 templates/base/alert_details.tmpl             |  2 +-
 templates/mail/issue/default.tmpl             |  2 +-
 templates/repo/commit_page.tmpl               |  2 +-
 .../repo/issue/view_content/comments.tmpl     |  2 +-
 .../repo/settings/webhook/base_list.tmpl      |  2 +-
 templates/status/500.tmpl                     |  2 +-
 14 files changed, 48 insertions(+), 43 deletions(-)

diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index b642ff4aa7..9077f97aea 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -224,7 +224,7 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages
         {{if not (eq .Body "")}}
             <h3>Message content</h3>
             <hr>
-            {{.Body | Str2html}}
+            {{.Body | SanitizeHTML}}
         {{end}}
     </p>
     <hr>
@@ -260,19 +260,19 @@ The template system contains several functions that can be used to further proce
 the messages. Here's a list of some of them:
 
 | Name             | Parameters  | Available | Usage                                                                       |
-| ---------------- | ----------- | --------- | --------------------------------------------------------------------------- |
+| ---------------- | ----------- | --------- |-----------------------------------------------------------------------------|
 | `AppUrl`         | -           | Any       | Gitea's URL                                                                 |
 | `AppName`        | -           | Any       | Set from `app.ini`, usually "Gitea"                                         |
 | `AppDomain`      | -           | Any       | Gitea's host name                                                           |
 | `EllipsisString` | string, int | Any       | Truncates a string to the specified length; adds ellipsis as needed         |
-| `Str2html`       | string      | Body only | Sanitizes text by removing any HTML tags from it.                           |
+| `SanitizeHTML`   | string      | Body only | Sanitizes text by removing any dangerous HTML tags from it.                 |
 | `SafeHTML`       | string      | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. |
 
 These are _functions_, not metadata, so they have to be used:
 
 ```html
-Like this:         {{Str2html "Escape<my>text"}}
-Or this:           {{"Escape<my>text" | Str2html}}
+Like this:         {{SanitizeHTML "Escape<my>text"}}
+Or this:           {{"Escape<my>text" | SanitizeHTML}}
 Or this:           {{AppUrl}}
 But not like this: {{.AppUrl}}
 ```
diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md
index fd455ef3a8..d58f9dc176 100644
--- a/docs/content/administration/mail-templates.zh-cn.md
+++ b/docs/content/administration/mail-templates.zh-cn.md
@@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
         {{if not (eq .Body "")}}
             <h3>消息内容:</h3>
             <hr>
-            {{.Body | Str2html}}
+            {{.Body | SanitizeHTML}}
         {{end}}
     </p>
     <hr>
@@ -242,20 +242,20 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 
 模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
 
-| 函数名              | 参数        | 可用于       | 用法                                                                              |
-|------------------| ----------- | ------------ | --------------------------------------------------------------------------------- |
-| `AppUrl`         | -           | 任何地方     | Gitea 的 URL                                                                     |
-| `AppName`        | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"                                               |
-| `AppDomain`      | -           | 任何地方     | Gitea 的主机名                                                                   |
-| `EllipsisString` | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号                                        |
-| `Str2html`       | string      | 仅正文部分   | 通过删除其中的 HTML 标签对文本进行清理                                              |
-| `SafeHTML`       | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段               |
+| 函数名              | 参数        | 可用于       | 用法                                                      |
+|------------------| ----------- | ------------ |---------------------------------------------------------|
+| `AppUrl`         | -           | 任何地方     | Gitea 的 URL                                             |
+| `AppName`        | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"                             |
+| `AppDomain`      | -           | 任何地方     | Gitea 的主机名                                              |
+| `EllipsisString` | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号                                   |
+| `SanitizeHTML`   | string      | 仅正文部分   | 通过删除其中的危险 HTML 标签对文本进行清理                                |
+| `SafeHTML`       | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段 |
 
 这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
 
 ```html
-像这样使用:         {{Str2html "Escape<my>text"}}
-或者这样使用:       {{"Escape<my>text" | Str2html}}
+像这样使用:         {{SanitizeHTML "Escape<my>text"}}
+或者这样使用:       {{"Escape<my>text" | SanitizeHTML}}
 或者这样使用:       {{AppUrl}}
 但不要像这样使用:   {{.AppUrl}}
 ```
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 0f39767586..1487fce69d 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -33,16 +33,16 @@ func NewFuncMap() template.FuncMap {
 
 		// -----------------------------------------------------------------
 		// html/template related functions
-		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
-		"Eval":        Eval,
-		"SafeHTML":    SafeHTML,
-		"HTMLFormat":  HTMLFormat,
-		"HTMLEscape":  HTMLEscape,
-		"QueryEscape": url.QueryEscape,
-		"JSEscape":    JSEscapeSafe,
-		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
-		"URLJoin":     util.URLJoin,
-		"DotEscape":   DotEscape,
+		"dict":         dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
+		"Eval":         Eval,
+		"SafeHTML":     SafeHTML,
+		"HTMLFormat":   HTMLFormat,
+		"HTMLEscape":   HTMLEscape,
+		"QueryEscape":  url.QueryEscape,
+		"JSEscape":     JSEscapeSafe,
+		"SanitizeHTML": SanitizeHTML,
+		"URLJoin":      util.URLJoin,
+		"DotEscape":    DotEscape,
 
 		"PathEscape":         url.PathEscape,
 		"PathEscapeSegments": util.PathEscapeSegments,
@@ -207,8 +207,8 @@ func SafeHTML(s any) template.HTML {
 	panic(fmt.Sprintf("unexpected type %T", s))
 }
 
-// Str2html sanitizes the input by pre-defined markdown rules
-func Str2html(s any) template.HTML {
+// SanitizeHTML sanitizes the input by pre-defined markdown rules
+func SanitizeHTML(s any) template.HTML {
 	switch v := s.(type) {
 	case string:
 		return template.HTML(markup.Sanitize(v))
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index 8f5d633d4f..3365278ac2 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -61,3 +61,8 @@ func TestJSEscapeSafe(t *testing.T) {
 func TestHTMLFormat(t *testing.T) {
 	assert.Equal(t, template.HTML("<a>&lt; < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1))
 }
+
+func TestSanitizeHTML(t *testing.T) {
+	assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
+	assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(template.HTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`)))
+}
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 3a2de1d9a1..3defa436a7 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -64,7 +64,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content
 	}
 	markdown, err := markdown.RenderString(markdownCtx, content)
 	if err != nil {
-		return templates.Str2html(content) // old code did so: use Str2html to render in tmpl
+		return templates.SanitizeHTML(content) // old code did so: use SanitizeHTML to render in tmpl
 	}
 	return markdown
 }
@@ -243,7 +243,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 			}
 		}
 		if len(content) == 0 {
-			content = templates.Str2html(desc)
+			content = templates.SanitizeHTML(desc)
 		}
 
 		items = append(items, &feeds.Item{
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index f2db4a4579..82cd91997a 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -105,7 +105,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	for _, project := range projects {
-		project.RenderedContent = templates.Str2html(project.Description) // FIXME: is it right? why not render?
+		project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
 	}
 
 	err = shared_user.LoadHeaderCount(ctx)
@@ -396,7 +396,7 @@ func ViewProject(ctx *context.Context) {
 		}
 	}
 
-	project.RenderedContent = templates.Str2html(project.Description) // FIXME: is it right? why not render?
+	project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
 	ctx.Data["LinkedPRs"] = linkedPrsMap
 	ctx.Data["PageIsViewProjects"] = true
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 702aa7201b..ebaa955ac8 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1761,7 +1761,7 @@ func ViewIssue(ctx *context.Context) {
 				// so "|" is used as delimeter to mark the new format
 				if comment.Content[0] != '|' {
 					// handle old time comments that have formatted text stored
-					comment.RenderedContent = templates.Str2html(comment.Content)
+					comment.RenderedContent = templates.SanitizeHTML(comment.Content)
 					comment.Content = ""
 				} else {
 					// else it's just a duration in seconds to pass on to the frontend
diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl
index 160584f769..760d3bfa2c 100644
--- a/templates/base/alert.tmpl
+++ b/templates/base/alert.tmpl
@@ -1,20 +1,20 @@
 {{if .Flash.ErrorMsg}}
 	<div class="ui negative message flash-message flash-error">
-		<p>{{.Flash.ErrorMsg | Str2html}}</p>
+		<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
 {{if .Flash.SuccessMsg}}
 	<div class="ui positive message flash-message flash-success">
-		<p>{{.Flash.SuccessMsg | Str2html}}</p>
+		<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
 {{if .Flash.InfoMsg}}
 	<div class="ui info message flash-message flash-info">
-		<p>{{.Flash.InfoMsg | Str2html}}</p>
+		<p>{{.Flash.InfoMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
 {{if .Flash.WarningMsg}}
 	<div class="ui warning message flash-message flash-warning">
-		<p>{{.Flash.WarningMsg | Str2html}}</p>
+		<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl
index 1d7ec15dc0..6801c8240f 100644
--- a/templates/base/alert_details.tmpl
+++ b/templates/base/alert_details.tmpl
@@ -2,6 +2,6 @@
 <details>
 	<summary>{{.Summary}}</summary>
 	<code>
-		{{.Details | Str2html}}
+		{{.Details | SanitizeHTML}}
 	</code>
 </details>
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index 79dbe897cc..10fa0f1ffc 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -58,7 +58,7 @@
 				{{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}}
 			{{end}}
 		{{else}}
-			{{.Body | Str2html}}
+			{{.Body | SanitizeHTML}}
 		{{end -}}
 		{{- range .ReviewComments}}
 			<hr>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 115ee92955..7892a57163 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -276,7 +276,7 @@
 				<span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span>
 			</div>
 			<div class="ui bottom attached info segment git-notes">
-				<pre class="commit-body">{{.NoteRendered | Str2html}}</pre>
+				<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
 			</div>
 		{{end}}
 		{{template "repo/diff/box" .}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 36ef5751ae..66ecc544d2 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -162,7 +162,7 @@
 				</span>
 				<div class="detail">
 					{{svg "octicon-git-commit"}}
-					<span class="text grey muted-links">{{.Content | Str2html}}</span>
+					<span class="text grey muted-links">{{.Content | SanitizeHTML}}</span>
 				</div>
 			</div>
 		{{else if eq .Type 7}}
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index 5a3fc0e7b8..00f9a48ba7 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -10,7 +10,7 @@
 <div class="ui attached segment">
 	<div class="ui list">
 		<div class="item">
-			{{.Description | Str2html}}
+			{{.Description | SanitizeHTML}}
 		</div>
 		{{range .Webhooks}}
 			<div class="item truncated-item-container">
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index d6cff28174..a92933c153 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -1,5 +1,5 @@
 {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics.
-* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName, Str2html
+* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName, SanitizeHTML
 * ctx.Locale
 * .Flash
 * .ErrorMsg

From b8a598e6a43fa65db23b88d3b3b281b5f2f7c2e0 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 1 Mar 2024 18:56:29 +0800
Subject: [PATCH 220/679] Refactor the "attachments" sub-template data key to
 RenderedContent (#29517)

The value passed into "attachments" sub-template is from
"RedneredContent", so use the same name for consistent. And it makes
readers easy to know its data type.
---
 templates/repo/diff/comments.tmpl                   | 2 +-
 templates/repo/issue/view_content.tmpl              | 2 +-
 templates/repo/issue/view_content/attachments.tmpl  | 4 ++--
 templates/repo/issue/view_content/comments.tmpl     | 4 ++--
 templates/repo/issue/view_content/conversation.tmpl | 2 +-
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index e00487a22c..0bb577deba 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -63,7 +63,7 @@
 			<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 			<div class="edit-content-zone gt-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
 			{{if .Attachments}}
-				{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+				{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 			{{end}}
 		</div>
 		{{$reactions := .Reactions.GroupByType}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index ee824b76b1..aa91764be4 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -62,7 +62,7 @@
 						<div id="issue-{{.Issue.ID}}-raw" class="raw-content gt-hidden">{{.Issue.Content}}</div>
 						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
 						{{if .Issue.Attachments}}
-							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Issue.Attachments "Content" .Issue.RenderedContent}}
+							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
 						{{end}}
 					</div>
 					{{$reactions := .Issue.Reactions.GroupByType}}
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
index 58f4c702b3..2c3a47d670 100644
--- a/templates/repo/issue/view_content/attachments.tmpl
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -8,7 +8,7 @@
 			<div class="gt-f1 gt-p-3">
 				<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					{{if FilenameIsImage .Name}}
-						{{if not (StringUtils.Contains (StringUtils.ToString $.Content) .UUID)}}
+						{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
 							{{$hasThumbnails = true}}
 						{{end}}
 						{{svg "octicon-file"}}
@@ -29,7 +29,7 @@
 		<div class="ui small thumbnails">
 			{{- range .Attachments -}}
 				{{if FilenameIsImage .Name}}
-					{{if not (StringUtils.Contains (StringUtils.ToString $.Content) .UUID)}}
+					{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
 					<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
 						<img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					</a>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 66ecc544d2..b500cec91c 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -69,7 +69,7 @@
 						<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 						{{if .Attachments}}
-							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 						{{end}}
 					</div>
 					{{$reactions := .Reactions.GroupByType}}
@@ -440,7 +440,7 @@
 							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 							{{if .Attachments}}
-								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 							{{end}}
 						</div>
 						{{$reactions := .Reactions.GroupByType}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 1afc744aee..5bb99d1db6 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -95,7 +95,7 @@
 							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 							{{if .Attachments}}
-								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 							{{end}}
 						</div>
 						{{$reactions := .Reactions.GroupByType}}

From 2ca5daf07e5816343c1018f5773e7a2c671f3777 Mon Sep 17 00:00:00 2001
From: Origami404 <Origami404g@gmail.com>
Date: Fri, 1 Mar 2024 20:01:24 +0800
Subject: [PATCH 221/679] Adding back missing options to app.example.ini
 (#29511)

In the refactoring of the configuration file #15807,
some lines were accidentally deleted:

DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
ENABLE_PUSH_CREATE_USER = false
ENABLE_PUSH_CREATE_ORG = false

Fix #29510

---------

Co-authored-by: techknowlogick <matti@mdranta.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 custom/conf/app.example.ini | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 5451537d02..f7f5cac9c8 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -956,6 +956,12 @@ LEVEL = Info
 ;GO_GET_CLONE_URL_PROTOCOL = https
 ;;
 ;; Close issues as long as a commit on any branch marks it as fixed
+;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
+;;
+;; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
+;ENABLE_PUSH_CREATE_USER = false
+;ENABLE_PUSH_CREATE_ORG = false
+;;
 ;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions.
 ;DISABLED_REPO_UNITS =
 ;;

From 194479a7416cffdc151e6b75a895d566e5970dca Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 1 Mar 2024 20:52:30 +0800
Subject: [PATCH 222/679] Use a predictiable fork URL to allow forking
 repositories without providing a repo ID (#29519)

Close #29512

The "fork" URL:

* Before: `/repo/fork/{RepoID}`
* After: `/{OwnerName}/{RepoName}/fork`
---
 routers/web/repo/pull.go            |  2 +-
 routers/web/web.go                  |  6 ++----
 services/context/repo.go            | 20 --------------------
 templates/repo/header.tmpl          |  4 ++--
 tests/integration/repo_fork_test.go |  6 +++---
 5 files changed, 8 insertions(+), 30 deletions(-)

diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index b1521a2112..c97edd8720 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -112,7 +112,7 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
 }
 
 func getForkRepository(ctx *context.Context) *repo_model.Repository {
-	forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
+	forkRepo := ctx.Repo.Repository
 	if ctx.Written() {
 		return nil
 	}
diff --git a/routers/web/web.go b/routers/web/web.go
index b6dd9500c8..9de652fba5 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -956,10 +956,6 @@ func registerRoutes(m *web.Route) {
 		m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
 		m.Get("/migrate", repo.Migrate)
 		m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
-		m.Group("/fork", func() {
-			m.Combo("/{repoid}").Get(repo.Fork).
-				Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
-		}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
 		m.Get("/search", repo.SearchRepo)
 	}, reqSignIn)
 
@@ -1255,6 +1251,8 @@ func registerRoutes(m *web.Route) {
 			m.Post("/delete", repo.DeleteBranchPost)
 			m.Post("/restore", repo.RestoreBranchPost)
 		}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+
+		m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
 	}, reqSignIn, context.RepoAssignment, context.UnitTypes())
 
 	// Tags
diff --git a/services/context/repo.go b/services/context/repo.go
index d6a68c0c1a..0b15c95e59 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -408,26 +408,6 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
 	ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
 }
 
-// RepoIDAssignment returns a handler which assigns the repo to the context.
-func RepoIDAssignment() func(ctx *Context) {
-	return func(ctx *Context) {
-		repoID := ctx.ParamsInt64(":repoid")
-
-		// Get repository.
-		repo, err := repo_model.GetRepositoryByID(ctx, repoID)
-		if err != nil {
-			if repo_model.IsErrRepoNotExist(err) {
-				ctx.NotFound("GetRepositoryByID", nil)
-			} else {
-				ctx.ServerError("GetRepositoryByID", err)
-			}
-			return
-		}
-
-		repoAssignment(ctx, repo)
-	}
-}
-
 // RepoAssignment returns a middleware to handle repository assignment
 func RepoAssignment(ctx *Context) context.CancelFunc {
 	if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce {
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 3e27d963bb..ee46af4236 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -82,7 +82,7 @@
 									{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
 									{{end}}
 								{{else if not $.UserAndOrgForks}}
-									href="{{AppSubUrl}}/repo/fork/{{.ID}}"
+									href="{{$.RepoLink}}/fork"
 								{{else}}
 									data-modal="#fork-repo-modal"
 								{{end}}
@@ -103,7 +103,7 @@
 									</div>
 									{{if $.CanSignedUserFork}}
 									<div class="divider"></div>
-									<a href="{{AppSubUrl}}/repo/fork/{{.ID}}">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
+									<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
 									{{end}}
 								</div>
 							</div>
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index 594fba6796..ca5d61ecc2 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -29,14 +29,14 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO
 
 	// Step2: click the fork button
 	htmlDoc := NewHTMLParser(t, resp.Body)
-	link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href")
+	link, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href")
 	assert.True(t, exists, "The template has changed")
 	req = NewRequest(t, "GET", link)
 	resp = session.MakeRequest(t, req, http.StatusOK)
 
 	// Step3: fill the form of the forking
 	htmlDoc = NewHTMLParser(t, resp.Body)
-	link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/fork/\"]").Attr("action")
+	link, exists = htmlDoc.doc.Find(`form.ui.form[action*="/fork"]`).Attr("action")
 	assert.True(t, exists, "The template has changed")
 	_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value")
 	assert.True(t, exists, fmt.Sprintf("Fork owner '%s' is not present in select box", forkOwnerName))
@@ -70,6 +70,6 @@ func TestRepoForkToOrg(t *testing.T) {
 	req := NewRequest(t, "GET", "/user2/repo1")
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	htmlDoc := NewHTMLParser(t, resp.Body)
-	_, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href")
+	_, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href")
 	assert.False(t, exists, "Forking should not be allowed anymore")
 }

From cee08f634746d65ce3fb05a32dd2439f241a92e6 Mon Sep 17 00:00:00 2001
From: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Date: Fri, 1 Mar 2024 21:23:53 +0800
Subject: [PATCH 223/679] Set pre-step status to `skipped` if job is skipped
 (#29489)

close #27496
1. Set pre-step (Set up job) status to `skipped` if job is skipped.
2. Apart from pre-step, the other steps should also be set to `skipped`.
The status of other steps are reported from the runner side. This will
be completed by this PR: https://gitea.com/gitea/act_runner/pulls/500

before:

![image](https://github.com/go-gitea/gitea/assets/33891828/4bac2ba9-66de-4679-b7ed-fbae459c0c54)

after:

![image](https://github.com/go-gitea/gitea/assets/33891828/ead4871a-4e0f-4bb1-9fb4-37f4fdb78dfc)
---
 modules/actions/task_state.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go
index cbbc0b357d..fe925bbb5d 100644
--- a/modules/actions/task_state.go
+++ b/modules/actions/task_state.go
@@ -35,6 +35,9 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
 	} else if task.Status.IsDone() {
 		preStep.Stopped = task.Stopped
 		preStep.Status = actions_model.StatusFailure
+		if task.Status.IsSkipped() {
+			preStep.Status = actions_model.StatusSkipped
+		}
 	}
 	logIndex += preStep.LogLength
 

From 6841e58d1fae33311fa0239823def9dd8fba4c1f Mon Sep 17 00:00:00 2001
From: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Date: Fri, 1 Mar 2024 22:18:35 +0800
Subject: [PATCH 224/679] Ignore `__debug_bin*` which is generated by vscode
 when debugging (#29524)

When debugging in VSCode now, the executable file generated will come
with a random string attached.
---
 .gitignore | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 814d910315..8f2544866a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,7 @@ _test
 
 # MS VSCode
 .vscode
-__debug_bin
+__debug_bin*
 
 *.cgo1.go
 *.cgo2.c

From 3b99066aa866e51e6a610716eaddfd1ea3645a67 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 1 Mar 2024 17:12:21 +0100
Subject: [PATCH 225/679] Fix incorrect diff expander for deletion of last
 lines in a file (#29501)

Fixes: https://github.com/go-gitea/gitea/issues/29498

I don't quite understand this code, but this change does seem to fix the
issue and I tested a number of diffs with it and saw no issue. The
function gets such value if last line is an addition:

```
  LastLeftIdx: (int) 0,
  LastRightIdx: (int) 47,
  LeftIdx: (int) 47,
  RightIdx: (int) 48,
```

If it's a deletion, it gets:

```
  LastLeftIdx: (int) 47,
  LastRightIdx: (int) 0,
  LeftIdx: (int) 48,
  RightIdx: (int) 47,
```

So I think it's correct to make this check respect both left and right
side.
---
 services/gitdiff/gitdiff.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 740c748347..b05c210a0c 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -154,7 +154,7 @@ func (d *DiffLine) GetBlobExcerptQuery() string {
 
 // GetExpandDirection gets DiffLineExpandDirection
 func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
-	if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
+	if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
 		return DiffLineExpandNone
 	}
 	if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 {

From 4b8293aa094e725b372329a19da687a6d1550069 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 2 Mar 2024 00:46:02 +0800
Subject: [PATCH 226/679] Fix issue & comment history bugs (#29525)

* Follow #17746: `HasIssueContentHistory` should use expr builder to
make sure zero value (0) be respected.
* Add "doer" check to make sure `canSoftDeleteContentHistory` only be
called by sign-in users.
---
 models/issues/content_history.go          |  8 ++------
 models/issues/content_history_test.go     | 19 +++++++++++++++++++
 routers/web/repo/issue_content_history.go |  6 +++++-
 3 files changed, 26 insertions(+), 7 deletions(-)

diff --git a/models/issues/content_history.go b/models/issues/content_history.go
index 8b00adda99..31c80d2cea 100644
--- a/models/issues/content_history.go
+++ b/models/issues/content_history.go
@@ -172,13 +172,9 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6
 
 // HasIssueContentHistory check if a ContentHistory entry exists
 func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) {
-	exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{
-		IssueID:   issueID,
-		CommentID: commentID,
-	})
+	exists, err := db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).Exist(&ContentHistory{})
 	if err != nil {
-		log.Error("can not fetch issue content history. err=%v", err)
-		return false, err
+		return false, fmt.Errorf("can not check issue content history. err: %w", err)
 	}
 	return exists, err
 }
diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go
index 0ea1d0f7b2..1caa73a948 100644
--- a/models/issues/content_history_test.go
+++ b/models/issues/content_history_test.go
@@ -78,3 +78,22 @@ func TestContentHistory(t *testing.T) {
 	assert.EqualValues(t, 7, list2[1].HistoryID)
 	assert.EqualValues(t, 4, list2[2].HistoryID)
 }
+
+func TestHasIssueContentHistoryForCommentOnly(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	_ = db.TruncateBeans(db.DefaultContext, &issues_model.ContentHistory{})
+
+	hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
+	assert.False(t, hasHistory1)
+	hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
+	assert.False(t, hasHistory2)
+
+	_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow(), "c-a", true)
+	_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false)
+
+	hasHistory1, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
+	assert.False(t, hasHistory1)
+	hasHistory2, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
+	assert.True(t, hasHistory2)
+}
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index fce0eccc7b..1ec497658f 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -94,7 +94,7 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue
 	// CanWrite means the doer can manage the issue/PR list
 	if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
 		canSoftDelete = true
-	} else {
+	} else if ctx.Doer != nil {
 		// for read-only users, they could still post issues or comments,
 		// they should be able to delete the history related to their own issue/comment, a case is:
 		// 1. the user posts some sensitive data
@@ -186,6 +186,10 @@ func SoftDeleteContentHistory(ctx *context.Context) {
 	if ctx.Written() {
 		return
 	}
+	if ctx.Doer == nil {
+		ctx.NotFound("Require SignIn", nil)
+		return
+	}
 
 	commentID := ctx.FormInt64("comment_id")
 	historyID := ctx.FormInt64("history_id")

From 85ad4a0f7d3468a2e79270fd36f544202560143b Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Sat, 2 Mar 2024 00:22:27 +0000
Subject: [PATCH 227/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_fr-FR.ini | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 7fd79446cc..20ef954cd2 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -424,6 +424,7 @@ authorization_failed_desc=L'autorisation a échoué car nous avons détecté une
 sspi_auth_failed=Échec de l'authentification SSPI
 password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">des mots de passe ayant fuité</a> sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs.
 password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
+last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis.
 
 [mail]
 view_it_on=Voir sur %s
@@ -1714,6 +1715,7 @@ pulls.select_commit_hold_shift_for_range=Maintenir Maj et cliquer sur des révis
 pulls.review_only_possible_for_full_diff=Une évaluation n'est possible que lorsque vous affichez le différentiel complet.
 pulls.filter_changes_by_commit=Filtrer par révision
 pulls.nothing_to_compare=Ces branches sont identiques. Il n’y a pas besoin de créer une demande d'ajout.
+pulls.nothing_to_compare_have_tag=Les branches/étiquettes sélectionnées sont équivalentes.
 pulls.nothing_to_compare_and_allow_empty_pr=Ces branches sont égales. Cette demande d'ajout sera vide.
 pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux branches : <a href="%[1]s">%[2]s#%[3]d</a>'
 pulls.create=Créer une demande d'ajout

From 9de5e39e25009bacc5ca201ed97e9cbb623e56e9 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 2 Mar 2024 09:21:01 +0800
Subject: [PATCH 228/679] Allow options to disable user gpg keys configuration
 from the interface on app.ini (#29486)

Follow #29447
Fix #29454
Extract from #20549
---
 custom/conf/app.example.ini                           |  3 ++-
 .../administration/config-cheat-sheet.en-us.md        |  3 ++-
 .../administration/config-cheat-sheet.zh-cn.md        |  3 ++-
 modules/setting/admin.go                              |  3 ++-
 routers/api/v1/user/gpg_key.go                        | 11 +++++++++++
 routers/web/user/setting/keys.go                      | 10 ++++++++++
 templates/user/settings/keys.tmpl                     |  2 ++
 7 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index f7f5cac9c8..dc5aa691ee 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1480,8 +1480,9 @@ LEVEL = Info
 ;;
 ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 ;DEFAULT_EMAIL_NOTIFICATIONS = enabled
-;; Disabled features for users, could be "deletion", more features can be disabled in future
+;; Disabled features for users, could be "deletion","manage_gpg_keys" more features can be disabled in future
 ;; - deletion: a user cannot delete their own account
+;; - manage_gpg_keys: a user cannot configure gpg keys
 ;USER_DISABLED_FEATURES =
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 643932de6c..ea6e1eb1a4 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -518,8 +518,9 @@ And the following unique queues:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
-- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion` and more features can be added in future.
+- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_gpg_keys` and more features can be added in future.
   - `deletion`: User cannot delete their own account.
+  - `manage_gpg_keys`: User cannot configure gpg keys
 
 ## Security (`security`)
 
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 5fe0a62215..5cc5734359 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -497,8 +497,9 @@ Gitea 创建以下非唯一队列:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
-- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`, 未来可以增加更多设置。
+- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。
   - `deletion`: 用户不能通过界面或者API删除他自己。
+  - `manage_gpg_keys`: 用户不能配置 GPG 密钥
 
 ## 安全性 (`security`)
 
diff --git a/modules/setting/admin.go b/modules/setting/admin.go
index 48a2ea9744..29bb947bc4 100644
--- a/modules/setting/admin.go
+++ b/modules/setting/admin.go
@@ -20,5 +20,6 @@ func loadAdminFrom(rootCfg ConfigProvider) {
 }
 
 const (
-	UserFeatureDeletion = "deletion"
+	UserFeatureDeletion      = "deletion"
+	UserFeatureManageGPGKeys = "manage_gpg_keys"
 )
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index b8438cd2aa..dcf5da0b2e 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -10,6 +10,7 @@ import (
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
@@ -132,6 +133,11 @@ func GetGPGKey(ctx *context.APIContext) {
 
 // CreateUserGPGKey creates new GPG key to given user by ID.
 func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
+	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+		return
+	}
+
 	token := asymkey_model.VerificationToken(ctx.Doer, 1)
 	lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
 
@@ -268,6 +274,11 @@ func DeleteGPGKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
+	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+		return
+	}
+
 	if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.ParamsInt64(":id")); err != nil {
 		if asymkey_model.IsErrGPGKeyAccessDenied(err) {
 			ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 0a12777e5e..cb01913bda 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -5,6 +5,7 @@
 package setting
 
 import (
+	"fmt"
 	"net/http"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -77,6 +78,11 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "gpg":
+		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+			return
+		}
+
 		token := asymkey_model.VerificationToken(ctx.Doer, 1)
 		lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
 
@@ -224,6 +230,10 @@ func KeysPost(ctx *context.Context) {
 func DeleteKey(ctx *context.Context) {
 	switch ctx.FormString("type") {
 	case "gpg":
+		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+			return
+		}
 		if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
 			ctx.Flash.Error("DeleteGPGKey: " + err.Error())
 		} else {
diff --git a/templates/user/settings/keys.tmpl b/templates/user/settings/keys.tmpl
index 93037e7e28..a44bf50048 100644
--- a/templates/user/settings/keys.tmpl
+++ b/templates/user/settings/keys.tmpl
@@ -2,6 +2,8 @@
 	<div class="user-setting-content">
 		{{template "user/settings/keys_ssh" .}}
 		{{template "user/settings/keys_principal" .}}
+		{{if not ($.UserDisabledFeatures.Contains "manage_gpg_keys")}}
 		{{template "user/settings/keys_gpg" .}}
+		{{end}}
 	</div>
 {{template "user/settings/layout_footer" .}}

From 8a0a83a1b53f55bcc710c3b229cba1c1bcf471c6 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 2 Mar 2024 10:48:14 +0200
Subject: [PATCH 229/679] Remove jQuery AJAX from common global functions
 (#29528)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the locale change functionality and it works as before
- Tested the delete button functionality and it works as before

# Demo using `fetch` instead of jQuery AJAX

![action](https://github.com/go-gitea/gitea/assets/20454870/8a024f75-c2a5-4bff-898d-ca751d2489f1)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/common-global.js | 26 ++++++++++++--------------
 1 file changed, 12 insertions(+), 14 deletions(-)

diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index cd0fc6d6a9..f90591aff3 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -11,7 +11,7 @@ import {htmlEscape} from 'escape-goat';
 import {showTemporaryTooltip} from '../modules/tippy.js';
 import {confirmModal} from './comp/ConfirmModal.js';
 import {showErrorToast} from '../modules/toast.js';
-import {request, POST} from '../modules/fetch.js';
+import {request, POST, GET} from '../modules/fetch.js';
 import '../htmx.js';
 
 const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
@@ -37,11 +37,10 @@ export function initHeadNavbarContentToggle() {
 }
 
 export function initFootLanguageMenu() {
-  function linkLanguageAction() {
+  async function linkLanguageAction() {
     const $this = $(this);
-    $.get($this.data('url')).always(() => {
-      window.location.reload();
-    });
+    await GET($this.data('url'));
+    window.location.reload();
   }
 
   $('.language-menu a[lang]').on('click', linkLanguageAction);
@@ -309,27 +308,26 @@ export function initGlobalLinkActions() {
 
     dialog.modal({
       closable: false,
-      onApprove() {
+      onApprove: async () => {
         if ($this.data('type') === 'form') {
           $($this.data('form')).trigger('submit');
           return;
         }
-
-        const postData = {
-          _csrf: csrfToken,
-        };
+        const postData = new FormData();
         for (const [key, value] of Object.entries(dataArray)) {
           if (key && key.startsWith('data')) {
-            postData[key.slice(4)] = value;
+            postData.append(key.slice(4), value);
           }
           if (key === 'id') {
-            postData['id'] = value;
+            postData.append('id', value);
           }
         }
 
-        $.post($this.data('url'), postData).done((data) => {
+        const response = await POST($this.data('url'), {data: postData});
+        if (response.ok) {
+          const data = await response.json();
           window.location.href = data.redirect;
-        });
+        }
       }
     }).modal('show');
   }

From a53d268aca87a281aadc2246541f8749eddcebed Mon Sep 17 00:00:00 2001
From: ChristopherHX <christopher.homberger@web.de>
Date: Sat, 2 Mar 2024 10:12:17 +0100
Subject: [PATCH 230/679] Actions Artifacts v4 backend (#28965)

Fixes #28853

Needs both https://gitea.com/gitea/act_runner/pulls/473 and
https://gitea.com/gitea/act_runner/pulls/471 on the runner side and
patched `actions/upload-artifact@v4` / `actions/download-artifact@v4`,
like `christopherhx/gitea-upload-artifact@v4` and
`christopherhx/gitea-download-artifact@v4`, to not return errors due to
GHES not beeing supported yet.
---
 models/fixtures/action_run.yml                |   19 +
 models/fixtures/action_run_job.yml            |   14 +
 models/fixtures/action_task.yml               |   20 +
 routers/api/actions/artifact.pb.go            | 1058 +++++++++++++++++
 routers/api/actions/artifact.proto            |   73 ++
 routers/api/actions/artifacts_chunks.go       |  107 +-
 routers/api/actions/artifacts_utils.go        |   11 +
 routers/api/actions/artifactsv4.go            |  512 ++++++++
 routers/init.go                               |    2 +
 routers/web/repo/actions/view.go              |   23 +
 .../api_actions_artifact_v4_test.go           |  224 ++++
 11 files changed, 2034 insertions(+), 29 deletions(-)
 create mode 100644 routers/api/actions/artifact.pb.go
 create mode 100644 routers/api/actions/artifact.proto
 create mode 100644 routers/api/actions/artifactsv4.go
 create mode 100644 tests/integration/api_actions_artifact_v4_test.go

diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml
index 2c2151f354..a42ab77ca5 100644
--- a/models/fixtures/action_run.yml
+++ b/models/fixtures/action_run.yml
@@ -17,3 +17,22 @@
   updated: 1683636626
   need_approval: 0
   approved_by: 0
+-
+  id: 792
+  title: "update actions"
+  repo_id: 4
+  owner_id: 1
+  workflow_id: "artifact.yaml"
+  index: 188
+  trigger_user_id: 1
+  ref: "refs/heads/master"
+  commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
+  event: "push"
+  is_fork_pull_request: 0
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
+  created: 1683636108
+  updated: 1683636626
+  need_approval: 0
+  approved_by: 0
diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml
index 071998b979..fd90f4fd5d 100644
--- a/models/fixtures/action_run_job.yml
+++ b/models/fixtures/action_run_job.yml
@@ -12,3 +12,17 @@
   status: 1
   started: 1683636528
   stopped: 1683636626
+-
+  id: 193
+  run_id: 792
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  name: job_2
+  attempt: 1
+  job_id: job_2
+  task_id: 48
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml
index c78fb3c5d6..443effe08c 100644
--- a/models/fixtures/action_task.yml
+++ b/models/fixtures/action_task.yml
@@ -18,3 +18,23 @@
   log_length: 707
   log_size: 90179
   log_expired: 0
+-
+  id: 48
+  job_id: 193
+  attempt: 1
+  runner_id: 1
+  status: 6 # 6 is the status code for "running", running task can upload artifacts
+  started: 1683636528
+  stopped: 1683636626
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  token_hash: ffffcfffffffbffffffffffffffffefffffffafffffffffffffffffffffffffffffdffffffffffffffffffffffffffffffff
+  token_salt: ffffffffff
+  token_last_eight: ffffffff
+  log_filename: artifact-test2/2f/47.log
+  log_in_storage: 1
+  log_length: 707
+  log_size: 90179
+  log_expired: 0
diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go
new file mode 100644
index 0000000000..590eda9fb9
--- /dev/null
+++ b/routers/api/actions/artifact.pb.go
@@ -0,0 +1,1058 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.32.0
+// 	protoc        v4.25.2
+// source: artifact.proto
+
+package actions
+
+import (
+	reflect "reflect"
+	sync "sync"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type CreateArtifactRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                 `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                 `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string                 `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	ExpiresAt               *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
+	Version                 int32                  `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"`
+}
+
+func (x *CreateArtifactRequest) Reset() {
+	*x = CreateArtifactRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CreateArtifactRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateArtifactRequest) ProtoMessage() {}
+
+func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateArtifactRequest.ProtoReflect.Descriptor instead.
+func (*CreateArtifactRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CreateArtifactRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *CreateArtifactRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *CreateArtifactRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *CreateArtifactRequest) GetExpiresAt() *timestamppb.Timestamp {
+	if x != nil {
+		return x.ExpiresAt
+	}
+	return nil
+}
+
+func (x *CreateArtifactRequest) GetVersion() int32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+type CreateArtifactResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ok              bool   `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+	SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"`
+}
+
+func (x *CreateArtifactResponse) Reset() {
+	*x = CreateArtifactResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CreateArtifactResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateArtifactResponse) ProtoMessage() {}
+
+func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateArtifactResponse.ProtoReflect.Descriptor instead.
+func (*CreateArtifactResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CreateArtifactResponse) GetOk() bool {
+	if x != nil {
+		return x.Ok
+	}
+	return false
+}
+
+func (x *CreateArtifactResponse) GetSignedUploadUrl() string {
+	if x != nil {
+		return x.SignedUploadUrl
+	}
+	return ""
+}
+
+type FinalizeArtifactRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                  `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                  `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string                  `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	Size                    int64                   `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
+	Hash                    *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"`
+}
+
+func (x *FinalizeArtifactRequest) Reset() {
+	*x = FinalizeArtifactRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FinalizeArtifactRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FinalizeArtifactRequest) ProtoMessage() {}
+
+func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FinalizeArtifactRequest.ProtoReflect.Descriptor instead.
+func (*FinalizeArtifactRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *FinalizeArtifactRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *FinalizeArtifactRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *FinalizeArtifactRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *FinalizeArtifactRequest) GetSize() int64 {
+	if x != nil {
+		return x.Size
+	}
+	return 0
+}
+
+func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue {
+	if x != nil {
+		return x.Hash
+	}
+	return nil
+}
+
+type FinalizeArtifactResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ok         bool  `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+	ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
+}
+
+func (x *FinalizeArtifactResponse) Reset() {
+	*x = FinalizeArtifactResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FinalizeArtifactResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FinalizeArtifactResponse) ProtoMessage() {}
+
+func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FinalizeArtifactResponse.ProtoReflect.Descriptor instead.
+func (*FinalizeArtifactResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *FinalizeArtifactResponse) GetOk() bool {
+	if x != nil {
+		return x.Ok
+	}
+	return false
+}
+
+func (x *FinalizeArtifactResponse) GetArtifactId() int64 {
+	if x != nil {
+		return x.ArtifactId
+	}
+	return 0
+}
+
+type ListArtifactsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                  `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                  `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	NameFilter              *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"`
+	IdFilter                *wrapperspb.Int64Value  `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"`
+}
+
+func (x *ListArtifactsRequest) Reset() {
+	*x = ListArtifactsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListArtifactsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsRequest) ProtoMessage() {}
+
+func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[4]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsRequest.ProtoReflect.Descriptor instead.
+func (*ListArtifactsRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *ListArtifactsRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsRequest) GetNameFilter() *wrapperspb.StringValue {
+	if x != nil {
+		return x.NameFilter
+	}
+	return nil
+}
+
+func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value {
+	if x != nil {
+		return x.IdFilter
+	}
+	return nil
+}
+
+type ListArtifactsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"`
+}
+
+func (x *ListArtifactsResponse) Reset() {
+	*x = ListArtifactsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[5]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListArtifactsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsResponse) ProtoMessage() {}
+
+func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[5]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsResponse.ProtoReflect.Descriptor instead.
+func (*ListArtifactsResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_MonolithArtifact {
+	if x != nil {
+		return x.Artifacts
+	}
+	return nil
+}
+
+type ListArtifactsResponse_MonolithArtifact struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                 `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                 `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	DatabaseId              int64                  `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"`
+	Name                    string                 `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
+	Size                    int64                  `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
+	CreatedAt               *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) Reset() {
+	*x = ListArtifactsResponse_MonolithArtifact{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[6]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {}
+
+func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[6]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsResponse_MonolithArtifact.ProtoReflect.Descriptor instead.
+func (*ListArtifactsResponse_MonolithArtifact) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetDatabaseId() int64 {
+	if x != nil {
+		return x.DatabaseId
+	}
+	return 0
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetSize() int64 {
+	if x != nil {
+		return x.Size
+	}
+	return 0
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreatedAt
+	}
+	return nil
+}
+
+type GetSignedArtifactURLRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *GetSignedArtifactURLRequest) Reset() {
+	*x = GetSignedArtifactURLRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[7]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetSignedArtifactURLRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSignedArtifactURLRequest) ProtoMessage() {}
+
+func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[7]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSignedArtifactURLRequest.ProtoReflect.Descriptor instead.
+func (*GetSignedArtifactURLRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetSignedArtifactURLRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *GetSignedArtifactURLRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *GetSignedArtifactURLRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+type GetSignedArtifactURLResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"`
+}
+
+func (x *GetSignedArtifactURLResponse) Reset() {
+	*x = GetSignedArtifactURLResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[8]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetSignedArtifactURLResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSignedArtifactURLResponse) ProtoMessage() {}
+
+func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[8]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSignedArtifactURLResponse.ProtoReflect.Descriptor instead.
+func (*GetSignedArtifactURLResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *GetSignedArtifactURLResponse) GetSignedUrl() string {
+	if x != nil {
+		return x.SignedUrl
+	}
+	return ""
+}
+
+type DeleteArtifactRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *DeleteArtifactRequest) Reset() {
+	*x = DeleteArtifactRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DeleteArtifactRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteArtifactRequest) ProtoMessage() {}
+
+func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[9]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteArtifactRequest.ProtoReflect.Descriptor instead.
+func (*DeleteArtifactRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *DeleteArtifactRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *DeleteArtifactRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *DeleteArtifactRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+type DeleteArtifactResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ok         bool  `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+	ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
+}
+
+func (x *DeleteArtifactResponse) Reset() {
+	*x = DeleteArtifactResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[10]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DeleteArtifactResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteArtifactResponse) ProtoMessage() {}
+
+func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[10]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteArtifactResponse.ProtoReflect.Descriptor instead.
+func (*DeleteArtifactResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *DeleteArtifactResponse) GetOk() bool {
+	if x != nil {
+		return x.Ok
+	}
+	return false
+}
+
+func (x *DeleteArtifactResponse) GetArtifactId() int64 {
+	if x != nil {
+		return x.ArtifactId
+	}
+	return 0
+}
+
+var File_artifact_proto protoreflect.FileDescriptor
+
+var file_artifact_proto_rawDesc = []byte{
+	0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a,
+	0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
+	0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+	0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
+	0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
+	0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
+	0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
+	0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
+	0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
+	0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+	0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61,
+	0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
+	0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18,
+	0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52,
+	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61,
+	0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02,
+	0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c,
+	0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73,
+	0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8,
+	0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
+	0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
+	0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
+	0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
+	0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
+	0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
+	0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+	0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
+	0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
+	0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e,
+	0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63,
+	0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69,
+	0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41,
+	0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+	0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
+	0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+	0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c,
+	0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
+	0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c,
+	0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61,
+	0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a,
+	0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61,
+	0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68,
+	0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c,
+	0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72,
+	0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f,
+	0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
+	0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26,
+	0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72,
+	0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+	0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69,
+	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f,
+	0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a,
+	0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75,
+	0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52,
+	0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
+	0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
+	0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
+	0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f,
+	0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
+	0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22,
+	0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74,
+	0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+	0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
+	0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+	0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53,
+	0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e,
+	0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69,
+	0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65,
+	0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75,
+	0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42,
+	0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b,
+	0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77,
+	0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65,
+	0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
+	0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
+	0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66,
+	0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_artifact_proto_rawDescOnce sync.Once
+	file_artifact_proto_rawDescData = file_artifact_proto_rawDesc
+)
+
+func file_artifact_proto_rawDescGZIP() []byte {
+	file_artifact_proto_rawDescOnce.Do(func() {
+		file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData)
+	})
+	return file_artifact_proto_rawDescData
+}
+
+var (
+	file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+	file_artifact_proto_goTypes  = []interface{}{
+		(*CreateArtifactRequest)(nil),                  // 0: github.actions.results.api.v1.CreateArtifactRequest
+		(*CreateArtifactResponse)(nil),                 // 1: github.actions.results.api.v1.CreateArtifactResponse
+		(*FinalizeArtifactRequest)(nil),                // 2: github.actions.results.api.v1.FinalizeArtifactRequest
+		(*FinalizeArtifactResponse)(nil),               // 3: github.actions.results.api.v1.FinalizeArtifactResponse
+		(*ListArtifactsRequest)(nil),                   // 4: github.actions.results.api.v1.ListArtifactsRequest
+		(*ListArtifactsResponse)(nil),                  // 5: github.actions.results.api.v1.ListArtifactsResponse
+		(*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
+		(*GetSignedArtifactURLRequest)(nil),            // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest
+		(*GetSignedArtifactURLResponse)(nil),           // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse
+		(*DeleteArtifactRequest)(nil),                  // 9: github.actions.results.api.v1.DeleteArtifactRequest
+		(*DeleteArtifactResponse)(nil),                 // 10: github.actions.results.api.v1.DeleteArtifactResponse
+		(*timestamppb.Timestamp)(nil),                  // 11: google.protobuf.Timestamp
+		(*wrapperspb.StringValue)(nil),                 // 12: google.protobuf.StringValue
+		(*wrapperspb.Int64Value)(nil),                  // 13: google.protobuf.Int64Value
+	}
+)
+
+var file_artifact_proto_depIdxs = []int32{
+	11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp
+	12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue
+	12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue
+	13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value
+	6,  // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
+	11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp
+	6,  // [6:6] is the sub-list for method output_type
+	6,  // [6:6] is the sub-list for method input_type
+	6,  // [6:6] is the sub-list for extension type_name
+	6,  // [6:6] is the sub-list for extension extendee
+	0,  // [0:6] is the sub-list for field type_name
+}
+
+func init() { file_artifact_proto_init() }
+func file_artifact_proto_init() {
+	if File_artifact_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CreateArtifactRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CreateArtifactResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FinalizeArtifactRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FinalizeArtifactResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListArtifactsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListArtifactsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListArtifactsResponse_MonolithArtifact); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetSignedArtifactURLRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetSignedArtifactURLResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*DeleteArtifactRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*DeleteArtifactResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_artifact_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   11,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_artifact_proto_goTypes,
+		DependencyIndexes: file_artifact_proto_depIdxs,
+		MessageInfos:      file_artifact_proto_msgTypes,
+	}.Build()
+	File_artifact_proto = out.File
+	file_artifact_proto_rawDesc = nil
+	file_artifact_proto_goTypes = nil
+	file_artifact_proto_depIdxs = nil
+}
diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto
new file mode 100644
index 0000000000..c68e5d030d
--- /dev/null
+++ b/routers/api/actions/artifact.proto
@@ -0,0 +1,73 @@
+syntax = "proto3";
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+package github.actions.results.api.v1;
+
+message CreateArtifactRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+    google.protobuf.Timestamp expires_at = 4;
+    int32 version = 5;
+}
+
+message CreateArtifactResponse {
+    bool ok = 1;
+    string signed_upload_url = 2;
+}
+
+message FinalizeArtifactRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+    int64 size = 4;
+    google.protobuf.StringValue hash = 5;
+}
+
+message FinalizeArtifactResponse {
+  bool ok = 1;
+  int64 artifact_id = 2;
+}
+
+message ListArtifactsRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    google.protobuf.StringValue name_filter = 3;
+    google.protobuf.Int64Value id_filter = 4;
+}
+
+message ListArtifactsResponse {
+    repeated ListArtifactsResponse_MonolithArtifact artifacts = 1;
+}
+
+message ListArtifactsResponse_MonolithArtifact {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    int64 database_id = 3;
+    string name = 4;
+    int64 size = 5;
+    google.protobuf.Timestamp created_at = 6;
+}
+
+message GetSignedArtifactURLRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+}
+
+message GetSignedArtifactURLResponse {
+    string signed_url = 1;
+}
+
+message DeleteArtifactRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+}
+
+message DeleteArtifactResponse {
+    bool ok = 1;
+    int64 artifact_id = 2;
+}
diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go
index 0713c8bba8..3a81724b3a 100644
--- a/routers/api/actions/artifacts_chunks.go
+++ b/routers/api/actions/artifacts_chunks.go
@@ -5,11 +5,16 @@ package actions
 
 import (
 	"crypto/md5"
+	"crypto/sha256"
 	"encoding/base64"
+	"encoding/hex"
+	"errors"
 	"fmt"
+	"hash"
 	"io"
 	"path/filepath"
 	"sort"
+	"strings"
 	"time"
 
 	"code.gitea.io/gitea/models/actions"
@@ -18,6 +23,52 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 )
 
+func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
+	artifact *actions.ActionArtifact,
+	contentSize, runID, start, end, length int64, checkMd5 bool,
+) (int64, error) {
+	// build chunk store path
+	storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
+	var r io.Reader = ctx.Req.Body
+	var hasher hash.Hash
+	if checkMd5 {
+		// use io.TeeReader to avoid reading all body to md5 sum.
+		// it writes data to hasher after reading end
+		// if hash is not matched, delete the read-end result
+		hasher = md5.New()
+		r = io.TeeReader(r, hasher)
+	}
+	// save chunk to storage
+	writtenSize, err := st.Save(storagePath, r, -1)
+	if err != nil {
+		return -1, fmt.Errorf("save chunk to storage error: %v", err)
+	}
+	var checkErr error
+	if checkMd5 {
+		// check md5
+		reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
+		chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
+		log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
+		// if md5 not match, delete the chunk
+		if reqMd5String != chunkMd5String {
+			checkErr = fmt.Errorf("md5 not match")
+		}
+	}
+	if writtenSize != contentSize {
+		checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size"))
+	}
+	if checkErr != nil {
+		if err := st.Delete(storagePath); err != nil {
+			log.Error("Error deleting chunk: %s, %v", storagePath, err)
+		}
+		return -1, checkErr
+	}
+	log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
+		storagePath, contentSize, artifact.ID, start, end)
+	// return chunk total size
+	return length, nil
+}
+
 func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
 	artifact *actions.ActionArtifact,
 	contentSize, runID int64,
@@ -29,33 +80,15 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
 		log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
 		return -1, fmt.Errorf("parse content range error: %v", err)
 	}
-	// build chunk store path
-	storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
-	// use io.TeeReader to avoid reading all body to md5 sum.
-	// it writes data to hasher after reading end
-	// if hash is not matched, delete the read-end result
-	hasher := md5.New()
-	r := io.TeeReader(ctx.Req.Body, hasher)
-	// save chunk to storage
-	writtenSize, err := st.Save(storagePath, r, -1)
-	if err != nil {
-		return -1, fmt.Errorf("save chunk to storage error: %v", err)
-	}
-	// check md5
-	reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
-	chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
-	log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
-	// if md5 not match, delete the chunk
-	if reqMd5String != chunkMd5String || writtenSize != contentSize {
-		if err := st.Delete(storagePath); err != nil {
-			log.Error("Error deleting chunk: %s, %v", storagePath, err)
-		}
-		return -1, fmt.Errorf("md5 not match")
-	}
-	log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
-		storagePath, contentSize, artifact.ID, start, end)
-	// return chunk total size
-	return length, nil
+	return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
+}
+
+func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
+	artifact *actions.ActionArtifact,
+	start, contentSize, runID int64,
+) (int64, error) {
+	end := start + contentSize - 1
+	return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
 }
 
 type chunkFileItem struct {
@@ -111,14 +144,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
 			log.Debug("artifact %d chunks not found", art.ID)
 			continue
 		}
-		if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil {
+		if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error {
+func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error {
 	sort.Slice(chunks, func(i, j int) bool {
 		return chunks[i].Start < chunks[j].Start
 	})
@@ -157,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
 		readers = append(readers, readCloser)
 	}
 	mergedReader := io.MultiReader(readers...)
+	shaPrefix := "sha256:"
+	var hash hash.Hash
+	if strings.HasPrefix(checksum, shaPrefix) {
+		hash = sha256.New()
+	}
+	if hash != nil {
+		mergedReader = io.TeeReader(mergedReader, hash)
+	}
 
 	// if chunk is gzip, use gz as extension
 	// download-artifact action will use content-encoding header to decide if it should decompress the file
@@ -185,6 +226,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
 		}
 	}()
 
+	if hash != nil {
+		rawChecksum := hash.Sum(nil)
+		actualChecksum := hex.EncodeToString(rawChecksum)
+		if !strings.HasSuffix(checksum, actualChecksum) {
+			return fmt.Errorf("update artifact error checksum is invalid")
+		}
+	}
+
 	// save storage path to artifact
 	log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath)
 	// if artifact is already uploaded, delete the old file
diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go
index 381e7eb16e..aaf89ef40e 100644
--- a/routers/api/actions/artifacts_utils.go
+++ b/routers/api/actions/artifacts_utils.go
@@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
 	return task, runID, true
 }
 
+func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
+	task := ctx.ActionTask
+	runID, err := strconv.ParseInt(rawRunID, 10, 64)
+	if err != nil || task.Job.RunID != runID {
+		log.Error("Error runID not match")
+		ctx.Error(http.StatusBadRequest, "run-id does not match")
+		return nil, 0, false
+	}
+	return task, runID, true
+}
+
 func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
 	paramHash := ctx.Params("artifact_hash")
 	// use artifact name to create upload url
diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go
new file mode 100644
index 0000000000..8300989c75
--- /dev/null
+++ b/routers/api/actions/artifactsv4.go
@@ -0,0 +1,512 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+// GitHub Actions Artifacts V4 API Simple Description
+//
+// 1. Upload artifact
+// 1.1. CreateArtifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact
+// Request:
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name": "test",
+//     "version": 4
+// }
+// Response:
+// {
+//     "ok": true,
+//     "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75"
+// }
+// 1.2. Upload Zip Content to Blobstorage (unauthenticated request)
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block
+// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
+// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
+// 1.5. FinalizeArtifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact
+// Request
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name": "test",
+//     "size": "2097",
+//     "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4"
+// }
+// Response
+// {
+//     "ok": true,
+//     "artifactId": "4"
+// }
+// 2. Download artifact
+// 2.1. ListArtifacts and optionally filter by artifact exact name or id
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts
+// Request
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name_filter": "test"
+// }
+// Response
+// {
+//     "artifacts": [
+//         {
+//             "workflowRunBackendId": "21",
+//             "workflowJobRunBackendId": "49",
+//             "databaseId": "4",
+//             "name": "test",
+//             "size": "2093",
+//             "createdAt": "2024-01-23T00:13:28Z"
+//         }
+//     ]
+// }
+// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL
+// Request
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name": "test"
+// }
+// Response
+// {
+//     "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76"
+// }
+// 2.3. Download Zip from Blobstorage (unauthenticated request)
+// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/storage"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+
+	"google.golang.org/protobuf/encoding/protojson"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/types/known/timestamppb"
+)
+
+const (
+	ArtifactV4RouteBase       = "/twirp/github.actions.results.api.v1.ArtifactService"
+	ArtifactV4ContentEncoding = "application/zip"
+)
+
+type artifactV4Routes struct {
+	prefix string
+	fs     storage.ObjectStorage
+}
+
+func ArtifactV4Contexter() func(next http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+			base, baseCleanUp := context.NewBaseContext(resp, req)
+			defer baseCleanUp()
+
+			ctx := &ArtifactContext{Base: base}
+			ctx.AppendContextValue(artifactContextKey, ctx)
+
+			next.ServeHTTP(ctx.Resp, ctx.Req)
+		})
+	}
+}
+
+func ArtifactsV4Routes(prefix string) *web.Route {
+	m := web.NewRoute()
+
+	r := artifactV4Routes{
+		prefix: prefix,
+		fs:     storage.ActionsArtifacts,
+	}
+
+	m.Group("", func() {
+		m.Post("CreateArtifact", r.createArtifact)
+		m.Post("FinalizeArtifact", r.finalizeArtifact)
+		m.Post("ListArtifacts", r.listArtifacts)
+		m.Post("GetSignedArtifactURL", r.getSignedArtifactURL)
+		m.Post("DeleteArtifact", r.deleteArtifact)
+	}, ArtifactContexter())
+	m.Group("", func() {
+		m.Put("UploadArtifact", r.uploadArtifact)
+		m.Get("DownloadArtifact", r.downloadArtifact)
+	}, ArtifactV4Contexter())
+
+	return m
+}
+
+func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte {
+	mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
+	mac.Write([]byte(endp))
+	mac.Write([]byte(expires))
+	mac.Write([]byte(artifactName))
+	mac.Write([]byte(fmt.Sprint(taskID)))
+	return mac.Sum(nil)
+}
+
+func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string {
+	expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
+	uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") +
+		"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
+	return uploadURL
+}
+
+func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) {
+	rawTaskID := ctx.Req.URL.Query().Get("taskID")
+	sig := ctx.Req.URL.Query().Get("sig")
+	expires := ctx.Req.URL.Query().Get("expires")
+	artifactName := ctx.Req.URL.Query().Get("artifactName")
+	dsig, _ := base64.URLEncoding.DecodeString(sig)
+	taskID, _ := strconv.ParseInt(rawTaskID, 10, 64)
+
+	expecedsig := r.buildSignature(endp, expires, artifactName, taskID)
+	if !hmac.Equal(dsig, expecedsig) {
+		log.Error("Error unauthorized")
+		ctx.Error(http.StatusUnauthorized, "Error unauthorized")
+		return nil, "", false
+	}
+	t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires)
+	if err != nil || t.Before(time.Now()) {
+		log.Error("Error link expired")
+		ctx.Error(http.StatusUnauthorized, "Error link expired")
+		return nil, "", false
+	}
+	task, err := actions.GetTaskByID(ctx, taskID)
+	if err != nil {
+		log.Error("Error runner api getting task by ID: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
+		return nil, "", false
+	}
+	if task.Status != actions.StatusRunning {
+		log.Error("Error runner api getting task: task is not running")
+		ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
+		return nil, "", false
+	}
+	if err := task.LoadJob(ctx); err != nil {
+		log.Error("Error runner api getting job: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error runner api getting job")
+		return nil, "", false
+	}
+	return task, artifactName, true
+}
+
+func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) {
+	var art actions.ActionArtifact
+	has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, util.ErrNotExist
+	}
+	return &art, nil
+}
+
+func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
+	body, err := io.ReadAll(ctx.Req.Body)
+	if err != nil {
+		log.Error("Error decode request body: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error decode request body")
+		return false
+	}
+	err = protojson.Unmarshal(body, req)
+	if err != nil {
+		log.Error("Error decode request body: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error decode request body")
+		return false
+	}
+	return true
+}
+
+func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
+	resp, err := protojson.Marshal(req)
+	if err != nil {
+		log.Error("Error encode response body: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error encode response body")
+		return
+	}
+	ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
+	ctx.Resp.WriteHeader(http.StatusOK)
+	_, _ = ctx.Resp.Write(resp)
+}
+
+func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
+	var req CreateArtifactRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	artifactName := req.Name
+
+	rententionDays := setting.Actions.ArtifactRetentionDays
+	if req.ExpiresAt != nil {
+		rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24)
+	}
+	// create or get artifact with name and path
+	artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays)
+	if err != nil {
+		log.Error("Error create or get artifact: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error create or get artifact")
+		return
+	}
+	artifact.ContentEncoding = ArtifactV4ContentEncoding
+	if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+		log.Error("Error UpdateArtifactByID: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
+		return
+	}
+
+	respData := CreateArtifactResponse{
+		Ok:              true,
+		SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID),
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
+	task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact")
+	if !ok {
+		return
+	}
+
+	comp := ctx.Req.URL.Query().Get("comp")
+	switch comp {
+	case "block", "appendBlock":
+		// get artifact by name
+		artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
+		if err != nil {
+			log.Error("Error artifact not found: %v", err)
+			ctx.Error(http.StatusNotFound, "Error artifact not found")
+			return
+		}
+
+		if comp == "block" {
+			artifact.FileSize = 0
+			artifact.FileCompressedSize = 0
+		}
+
+		_, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID)
+		if err != nil {
+			log.Error("Error runner api getting task: task is not running")
+			ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
+			return
+		}
+		artifact.FileCompressedSize += ctx.Req.ContentLength
+		artifact.FileSize += ctx.Req.ContentLength
+		if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+			log.Error("Error UpdateArtifactByID: %v", err)
+			ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
+			return
+		}
+		ctx.JSON(http.StatusCreated, "appended")
+	case "blocklist":
+		ctx.JSON(http.StatusCreated, "created")
+	}
+}
+
+func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
+	var req FinalizeArtifactRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, runID, req.Name)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+	chunkMap, err := listChunksByRunID(r.fs, runID)
+	if err != nil {
+		log.Error("Error merge chunks: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+		return
+	}
+	chunks, ok := chunkMap[artifact.ID]
+	if !ok {
+		log.Error("Error merge chunks")
+		ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+		return
+	}
+	checksum := ""
+	if req.Hash != nil {
+		checksum = req.Hash.Value
+	}
+	if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil {
+		log.Error("Error merge chunks: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+		return
+	}
+
+	respData := FinalizeArtifactResponse{
+		Ok:         true,
+		ArtifactId: artifact.ID,
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
+	var req ListArtifactsRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID})
+	if err != nil {
+		log.Error("Error getting artifacts: %v", err)
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+	if len(artifacts) == 0 {
+		log.Debug("[artifact] handleListArtifacts, no artifacts")
+		ctx.Error(http.StatusNotFound)
+		return
+	}
+
+	list := []*ListArtifactsResponse_MonolithArtifact{}
+
+	table := map[string]*ListArtifactsResponse_MonolithArtifact{}
+	for _, artifact := range artifacts {
+		if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding {
+			table[artifact.ArtifactName] = nil
+			continue
+		}
+
+		table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{
+			Name:                    artifact.ArtifactName,
+			CreatedAt:               timestamppb.New(artifact.CreatedUnix.AsTime()),
+			DatabaseId:              artifact.ID,
+			WorkflowRunBackendId:    req.WorkflowRunBackendId,
+			WorkflowJobRunBackendId: req.WorkflowJobRunBackendId,
+			Size:                    artifact.FileSize,
+		}
+	}
+	for _, artifact := range table {
+		if artifact != nil {
+			list = append(list, artifact)
+		}
+	}
+
+	respData := ListArtifactsResponse{
+		Artifacts: list,
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
+	var req GetSignedArtifactURLRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	artifactName := req.Name
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, runID, artifactName)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+
+	respData := GetSignedArtifactURLResponse{}
+
+	if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+		u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath)
+		if u != nil && err == nil {
+			respData.SignedUrl = u.String()
+		}
+	}
+	if respData.SignedUrl == "" {
+		respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID)
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
+	task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact")
+	if !ok {
+		return
+	}
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+
+	file, _ := r.fs.Open(artifact.StoragePath)
+
+	_, _ = io.Copy(ctx.Resp, file)
+}
+
+func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
+	var req DeleteArtifactRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, runID, req.Name)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+
+	err = actions.SetArtifactNeedDelete(ctx, runID, req.Name)
+	if err != nil {
+		log.Error("Error deleting artifacts: %v", err)
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	respData := DeleteArtifactResponse{
+		Ok:         true,
+		ArtifactId: artifact.ID,
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
diff --git a/routers/init.go b/routers/init.go
index e0a7150ba3..1dedbebeb5 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -198,6 +198,8 @@ func NormalRoutes() *web.Route {
 		// TODO: this prefix should be generated with a token string with runner ?
 		prefix = "/api/actions_pipeline"
 		r.Mount(prefix, actions_router.ArtifactsRoutes(prefix))
+		prefix = actions_router.ArtifactV4RouteBase
+		r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix))
 	}
 
 	return r
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 52c3cf1d07..3f8030e40d 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -22,6 +22,7 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -602,6 +603,28 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
 
 	ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
 
+	// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
+	// The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
+	if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
+		art := artifacts[0]
+		if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+			u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath)
+			if u != nil && err == nil {
+				ctx.Redirect(u.String())
+				return
+			}
+		}
+		f, err := storage.ActionsArtifacts.Open(art.StoragePath)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, err.Error())
+			return
+		}
+		_, _ = io.Copy(ctx.Resp, f)
+		return
+	}
+
+	// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
+	// Those need to be zipped for download
 	writer := zip.NewWriter(ctx.Resp)
 	defer writer.Close()
 	for _, art := range artifacts {
diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go
new file mode 100644
index 0000000000..f58f876849
--- /dev/null
+++ b/tests/integration/api_actions_artifact_v4_test.go
@@ -0,0 +1,224 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/hex"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/routers/api/actions"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/types/known/timestamppb"
+	"google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+func toProtoJSON(m protoreflect.ProtoMessage) io.Reader {
+	resp, _ := protojson.Marshal(m)
+	buf := bytes.Buffer{}
+	buf.Write(resp)
+	return &buf
+}
+
+func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
+		Version:                 4,
+		Name:                    "artifact",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var uploadResp actions.CreateArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
+	assert.True(t, uploadResp.Ok)
+	assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
+
+	// get upload url
+	idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
+	url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
+
+	// upload artifact chunk
+	body := strings.Repeat("A", 1024)
+	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
+	MakeRequest(t, req, http.StatusCreated)
+
+	t.Logf("Create artifact confirm")
+
+	sha := sha256.Sum256([]byte(body))
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
+		Name:                    "artifact",
+		Size:                    1024,
+		Hash:                    wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	resp = MakeRequest(t, req, http.StatusOK)
+	var finalizeResp actions.FinalizeArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
+	assert.True(t, finalizeResp.Ok)
+}
+
+func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
+		Version:                 4,
+		Name:                    "artifact-invalid-checksum",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var uploadResp actions.CreateArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
+	assert.True(t, uploadResp.Ok)
+	assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
+
+	// get upload url
+	idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
+	url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
+
+	// upload artifact chunk
+	body := strings.Repeat("B", 1024)
+	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
+	MakeRequest(t, req, http.StatusCreated)
+
+	t.Logf("Create artifact confirm")
+
+	sha := sha256.Sum256([]byte(strings.Repeat("A", 1024)))
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
+		Name:                    "artifact-invalid-checksum",
+		Size:                    1024,
+		Hash:                    wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusInternalServerError)
+}
+
+func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
+		Version:                 4,
+		ExpiresAt:               timestamppb.New(time.Now().Add(5 * 24 * time.Hour)),
+		Name:                    "artifactWithRetentionDays",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var uploadResp actions.CreateArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
+	assert.True(t, uploadResp.Ok)
+	assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
+
+	// get upload url
+	idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
+	url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
+
+	// upload artifact chunk
+	body := strings.Repeat("A", 1024)
+	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
+	MakeRequest(t, req, http.StatusCreated)
+
+	t.Logf("Create artifact confirm")
+
+	sha := sha256.Sum256([]byte(body))
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
+		Name:                    "artifactWithRetentionDays",
+		Size:                    1024,
+		Hash:                    wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	resp = MakeRequest(t, req, http.StatusOK)
+	var finalizeResp actions.FinalizeArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
+	assert.True(t, finalizeResp.Ok)
+}
+
+func TestActionsArtifactV4DownloadSingle(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
+		NameFilter:              wrapperspb.String("artifact"),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var listResp actions.ListArtifactsResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &listResp)
+	assert.Len(t, listResp.Artifacts, 1)
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
+		Name:                    "artifact",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	resp = MakeRequest(t, req, http.StatusOK)
+	var finalizeResp actions.GetSignedArtifactURLResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
+	assert.NotEmpty(t, finalizeResp.SignedUrl)
+
+	req = NewRequest(t, "GET", finalizeResp.SignedUrl)
+	resp = MakeRequest(t, req, http.StatusOK)
+	body := strings.Repeat("A", 1024)
+	assert.Equal(t, resp.Body.String(), body)
+}
+
+func TestActionsArtifactV4Delete(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// delete artifact by name
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/DeleteArtifact", toProtoJSON(&actions.DeleteArtifactRequest{
+		Name:                    "artifact",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var deleteResp actions.DeleteArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &deleteResp)
+	assert.True(t, deleteResp.Ok)
+}

From 2089b974c8bf670de5c6801fa48c229fd9291a7b Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 2 Mar 2024 11:29:04 +0200
Subject: [PATCH 231/679] Remove jQuery AJAX from the repo tag edit form
 (#29526)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the repo tag edit form functionality and it works as before

# Demo using `fetch` instead of jQuery AJAX

![action](https://github.com/go-gitea/gitea/assets/20454870/11126bc4-1666-44ae-8644-a6351da43514)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-home.js | 57 ++++++++++++++++----------------
 1 file changed, 29 insertions(+), 28 deletions(-)

diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index 3603fae2e9..bbba7b103e 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -1,8 +1,9 @@
 import $ from 'jquery';
 import {stripTags} from '../utils.js';
 import {hideElem, showElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 
 export function initRepoTopicBar() {
   const mgrBtn = $('#manage_topic');
@@ -30,50 +31,50 @@ export function initRepoTopicBar() {
     mgrBtn.focus();
   });
 
-  saveBtn.on('click', () => {
+  saveBtn.on('click', async () => {
     const topics = $('input[name=topics]').val();
 
-    $.post(saveBtn.attr('data-link'), {
-      _csrf: csrfToken,
-      topics
-    }, (_data, _textStatus, xhr) => {
-      if (xhr.responseJSON.status === 'ok') {
+    const data = new FormData();
+    data.append('topics', topics);
+
+    const response = await POST(saveBtn.attr('data-link'), {data});
+
+    if (response.ok) {
+      const responseData = await response.json();
+      if (responseData.status === 'ok') {
         viewDiv.children('.topic').remove();
         if (topics.length) {
           const topicArray = topics.split(',');
           topicArray.sort();
-          for (let i = 0; i < topicArray.length; i++) {
+          for (const topic of topicArray) {
             const link = $('<a class="ui repo-topic large label topic gt-m-0"></a>');
-            link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topicArray[i])}&topic=1`);
-            link.text(topicArray[i]);
+            link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`);
+            link.text(topic);
             link.insertBefore(mgrBtn); // insert all new topics before manage button
           }
         }
         hideElem(editDiv);
         showElem(viewDiv);
       }
-    }).fail((xhr) => {
-      if (xhr.status === 422) {
-        if (xhr.responseJSON.invalidTopics.length > 0) {
-          topicPrompts.formatPrompt = xhr.responseJSON.message;
+    } else if (response.status === 422) {
+      const responseData = await response.json();
+      if (responseData.invalidTopics.length > 0) {
+        topicPrompts.formatPrompt = responseData.message;
 
-          const {invalidTopics} = xhr.responseJSON;
-          const topicLabels = topicDropdown.children('a.ui.label');
-
-          for (const [index, value] of topics.split(',').entries()) {
-            for (let i = 0; i < invalidTopics.length; i++) {
-              if (invalidTopics[i] === value) {
-                topicLabels.eq(index).removeClass('green').addClass('red');
-              }
-            }
+        const {invalidTopics} = responseData;
+        const topicLabels = topicDropdown.children('a.ui.label');
+        for (const [index, value] of topics.split(',').entries()) {
+          if (invalidTopics.includes(value)) {
+            topicLabels.eq(index).removeClass('green').addClass('red');
           }
-        } else {
-          topicPrompts.countPrompt = xhr.responseJSON.message;
         }
+      } else {
+        topicPrompts.countPrompt = responseData.message;
       }
-    }).always(() => {
-      topicForm.form('validate form');
-    });
+    }
+
+    // Always validate the form
+    topicForm.form('validate form');
   });
 
   topicDropdown.dropdown({

From 90435847792d26ac3f23f1a8479706afadec6b15 Mon Sep 17 00:00:00 2001
From: charles <30816317+charles7668@users.noreply.github.com>
Date: Sat, 2 Mar 2024 17:54:46 +0800
Subject: [PATCH 232/679] Fix issue link does not support quotes (#29484)
 (#29487)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Close #29484

![圖片](https://github.com/go-gitea/gitea/assets/30816317/b27e6e16-67e0-469c-8e04-30180c585890)
---
 modules/references/references.go      | 4 ++--
 modules/references/references_test.go | 4 ++++
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/modules/references/references.go b/modules/references/references.go
index 7758312564..761d6ee3d1 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -31,9 +31,9 @@ var (
 	// mentionPattern matches all mentions in the form of "@user" or "@org/team"
 	mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
 	// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
-	issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\')([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
+	issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`)
 	// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
-	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
+	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`)
 	// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
 	// e.g. org/repo#12345
 	crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
diff --git a/modules/references/references_test.go b/modules/references/references_test.go
index ba7dda80cc..0c32933619 100644
--- a/modules/references/references_test.go
+++ b/modules/references/references_test.go
@@ -429,6 +429,8 @@ func TestRegExp_issueNumericPattern(t *testing.T) {
 		"  #12",
 		"#12:",
 		"ref: #12: msg",
+		"\"#1234\"",
+		"'#1234'",
 	}
 	falseTestCases := []string{
 		"# 1234",
@@ -459,6 +461,8 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
 		"(ABC-123)",
 		"[ABC-123]",
 		"ABC-123:",
+		"\"ABC-123\"",
+		"'ABC-123'",
 	}
 	falseTestCases := []string{
 		"RC-08",

From c0c2cb933bc59c5c9b2558c9bf53b495504f35f3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 2 Mar 2024 20:02:34 +0800
Subject: [PATCH 233/679] Fix incorrect subpath in links (#29535)

* `$referenceUrl`: it is constructed by "Issue.Link", which already has
the "AppSubURL"
* `window.location.href`: AppSubURL could be empty string, so it needs
the trailing slash
---
 templates/repo/diff/comments.tmpl     | 2 +-
 templates/repo/diff/conversation.tmpl | 2 +-
 web_src/js/features/notification.js   | 2 +-
 web_src/js/features/stopwatch.js      | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 0bb577deba..99974ecf6a 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -33,7 +33,7 @@
 			<div class="comment-header-right actions gt-df gt-ac">
 				{{if .Invalidated}}
 					{{$referenceUrl := printf "%s#%s" $.root.Issue.Link .HashTag}}
-					<a href="{{AppSubUrl}}{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+					<a href="{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
 						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
 					</a>
 				{{end}}
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index feca7b6c0b..aaeac3c550 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -14,7 +14,7 @@
 					We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above).
 					The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl
 					-->
-					<a href="{{AppSubUrl}}{{$referenceUrl}}" class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+					<a href="{{$referenceUrl}}" class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
 						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
 					</a>
 				{{end}}
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 4dcf02d2dc..a9236247c6 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -112,7 +112,7 @@ export function initNotificationCount() {
           type: 'close',
         });
         worker.port.close();
-        window.location.href = appSubUrl;
+        window.location.href = `${appSubUrl}/`;
       } else if (event.data.type === 'close') {
         worker.port.postMessage({
           type: 'close',
diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
index e7e20e5212..2ec74344fc 100644
--- a/web_src/js/features/stopwatch.js
+++ b/web_src/js/features/stopwatch.js
@@ -75,7 +75,7 @@ export function initStopwatch() {
           type: 'close',
         });
         worker.port.close();
-        window.location.href = appSubUrl;
+        window.location.href = `${appSubUrl}/`;
       } else if (event.data.type === 'close') {
         worker.port.postMessage({
           type: 'close',

From e650f64d812f5ebeb4a11d2ec20f2376c6d963bc Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 2 Mar 2024 20:45:14 +0800
Subject: [PATCH 234/679] Fix incorrect redirection when creating a PR fails
 (#29537)

This is only a quick fix to make it easier to backport.

After this PR gets merged, I will propose a new PR to fix the FIXME.

<details>

![image](https://github.com/go-gitea/gitea/assets/2114189/98d1d5c4-2e79-4a75-80e9-76fd898986e0)

</details>
---
 options/locale/locale_en-US.ini    | 4 ++--
 routers/web/repo/pull.go           | 2 +-
 templates/repo/diff/compare.tmpl   | 8 --------
 templates/repo/issue/new.tmpl      | 8 --------
 templates/repo/issue/new_form.tmpl | 8 +++-----
 5 files changed, 6 insertions(+), 24 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6d4e109e1d..beda02603e 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1792,9 +1792,9 @@ pulls.unrelated_histories = Merge Failed: The merge head and base do not share a
 pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again.
 pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again.
 pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch.
-pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository.
+pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this repository.
 pulls.push_rejected_summary = Full Rejection Message
-pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the Git Hooks for this repository
+pulls.push_rejected_no_message = Push Failed: The push was rejected but there was no remote message. Review the Git Hooks for this repository
 pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.`
 pulls.status_checking = Some checks are pending
 pulls.status_checks_success = All checks were successful
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index c97edd8720..428fe23156 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1501,7 +1501,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 				return
 			}
 			ctx.Flash.Error(flashError)
-			ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost
+			ctx.JSONRedirect(ctx.Link + "?" + ctx.Req.URL.RawQuery) // FIXME: it's unfriendly, and will make the content lost
 			return
 		}
 		ctx.ServerError("NewPullRequest", err)
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index 819bd8a2f0..e460d4da3b 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -11,14 +11,6 @@
 			{{ctx.Locale.Tr "action.compare_commits_general"}}
 		{{end}}
 	</h2>
-	{{if .Flash.WarningMsg}}
-		{{/*
-			There's already an importing of alert.tmpl in new_form.tmpl,
-			but only the negative message will be displayed within forms for some reasons, see semantic.css:10659.
-			To avoid repeated negative messages, the importing here if for .Flash.WarningMsg only.
-		*/}}
-		{{template "base/alert" .}}
-	{{end}}
 	{{$BaseCompareName := $.BaseName -}}
 	{{- $HeadCompareName := $.HeadRepo.OwnerName -}}
 	{{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}}
diff --git a/templates/repo/issue/new.tmpl b/templates/repo/issue/new.tmpl
index 780e874bc6..ccd45fdebe 100644
--- a/templates/repo/issue/new.tmpl
+++ b/templates/repo/issue/new.tmpl
@@ -2,14 +2,6 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository new issue">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		{{if .Flash.WarningMsg}}
-			{{/*
-			There's already an importing of alert.tmpl in new_form.tmpl,
-			but only the negative message will be displayed within forms for some reasons, see semantic.css:10659.
-			To avoid repeated negative messages, the importing here if for .Flash.WarningMsg only.
-			 */}}
-			{{template "base/alert" .}}
-		{{end}}
 		{{template "repo/issue/new_form" .}}
 	</div>
 </div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 8e4310f0bb..e67314bfd5 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -1,10 +1,8 @@
+{{if .Flash}}
+{{template "base/alert" .}}
+{{end}}
 <form class="issue-content ui comment form form-fetch-action" id="new-issue" action="{{.Link}}" method="post">
 	{{.CsrfTokenHtml}}
-	{{if .Flash}}
-		<div class="sixteen wide column">
-			{{template "base/alert" .}}
-		</div>
-	{{end}}
 	<div class="issue-content-left">
 		<div class="ui comments">
 			<div class="comment">

From 423372d84ab3d885e47d4a00cd69d6040b61cc4c Mon Sep 17 00:00:00 2001
From: charles <30816317+charles7668@users.noreply.github.com>
Date: Sat, 2 Mar 2024 21:38:34 +0800
Subject: [PATCH 235/679] =?UTF-8?q?Add=20a=20check=20for=20when=20the=20co?=
 =?UTF-8?q?mmand=20is=20canceled=20by=20the=20program=20on=20Window?=
 =?UTF-8?q?=E2=80=A6=20(#29538)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Close #29509

Windows, unlike Linux, does not have signal-specified exit codes.
Therefore, we should add a Windows-specific check for Windows. If we
don't do this, the logs will always show a failed status, even though
the command actually works correctly.

If you check the Go source code in exec_windows.go, you will see that it
always returns exit code 1.

![image](https://github.com/go-gitea/gitea/assets/30816317/9dfd7c70-9995-47d9-9641-db793f58770c)

The exit code 1 does not exclusively signify a SIGNAL KILL; it can
indicate any issue that occurs when a program fails.
---
 modules/git/command.go | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/modules/git/command.go b/modules/git/command.go
index 9305ef6f92..371109730a 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -12,6 +12,7 @@ import (
 	"io"
 	"os"
 	"os/exec"
+	"runtime"
 	"strings"
 	"time"
 
@@ -344,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error {
 		log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
 	}
 
+	// We need to check if the context is canceled by the program on Windows.
+	// This is because Windows does not have signal checking when terminating the process.
+	// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
+	if runtime.GOOS == "windows" &&
+		err != nil &&
+		err.Error() == "" &&
+		cmd.ProcessState.ExitCode() == 1 &&
+		ctx.Err() == context.Canceled {
+		return ctx.Err()
+	}
+
 	if err != nil && ctx.Err() != context.DeadlineExceeded {
 		return err
 	}

From cc27b50bdf9d1e2b02c91d7c4d338e01408e8522 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 2 Mar 2024 22:03:39 +0800
Subject: [PATCH 236/679] Fix a bug returning 404 when display a single tag
 with no release (#29466)

Partially caused by #29149

When use

```go
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,
	})
```
replace
```go
release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*"))
```
It missed `IncludeTags: true,`. That means this bug will be occupied only when the release is a tag.
This PR will fix

 - Get the right tag record when it's not a release
 - Display correct tag tab but not release tag when it's a tag.
- The button will bring the tag name to the new page when it's a single tag page
- the new page will automatically hide the release target inputbox when the tag name is pre filled. This should be backport to v1.21.
---
 routers/web/repo/release.go            | 9 +++++++++
 templates/repo/release/list.tmpl       | 6 +++---
 templates/repo/release_tag_header.tmpl | 6 +++---
 tests/integration/links_test.go        | 1 +
 web_src/js/features/repo-release.js    | 9 +++++++--
 5 files changed, 23 insertions(+), 8 deletions(-)

diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index c6d8c45af1..dbc190928f 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -185,6 +185,11 @@ func Releases(ctx *context.Context) {
 		ctx.ServerError("getReleaseInfos", err)
 		return
 	}
+	for _, rel := range releases {
+		if rel.Release.IsTag && rel.Release.Title == "" {
+			rel.Release.Title = rel.Release.TagName
+		}
+	}
 
 	ctx.Data["Releases"] = releases
 
@@ -283,6 +288,7 @@ func SingleRelease(ctx *context.Context) {
 		TagNames:    []string{ctx.Params("*")},
 		// only show draft releases for users who can write, read-only users shouldn't see draft releases.
 		IncludeDrafts: writeAccess,
+		IncludeTags:   true,
 	})
 	if err != nil {
 		ctx.ServerError("getReleaseInfos", err)
@@ -294,6 +300,9 @@ func SingleRelease(ctx *context.Context) {
 	}
 
 	release := releases[0].Release
+	if release.IsTag && release.Title == "" {
+		release.Title = release.TagName
+	}
 
 	ctx.Data["PageIsSingleTag"] = release.IsTag
 	if release.IsTag {
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 7f38fa3ff0..873cccab79 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -18,18 +18,18 @@
 					<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/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>
+								{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>{{end}}
 								{{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}}
+								{{else if (not $release.IsTag)}}
 									<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
 								{{end}}
 							</h4>
 							<div>
-								{{if $.CanCreateRelease}}
+								{{if and $.CanCreateRelease (not $.PageIsSingleTag)}}
 									<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>
diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl
index 31c151da08..8e6088790d 100644
--- a/templates/repo/release_tag_header.tmpl
+++ b/templates/repo/release_tag_header.tmpl
@@ -5,9 +5,9 @@
 	<div class="gt-df">
 		<div class="gt-f1 gt-df gt-ac">
 			<h2 class="ui compact small menu header small-menu-items">
-				<a class="{{if .PageIsReleaseList}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
+				<a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
 				{{if $canReadCode}}
-					<a class="{{if .PageIsTagList}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a>
+					<a class="{{if or .PageIsTagList .PageIsSingleTag}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a>
 				{{end}}
 			</h2>
 		</div>
@@ -17,7 +17,7 @@
 			</a>
 		{{end}}
 		{{if and (not .PageIsTagList) .CanCreateRelease}}
-			<a class="ui small primary button" href="{{$.RepoLink}}/releases/new">
+			<a class="ui small primary button" href="{{$.RepoLink}}/releases/new{{if .PageIsSingleTag}}?tag={{.TagName}}{{end}}">
 				{{ctx.Locale.Tr "repo.release.new_release"}}
 			</a>
 		{{end}}
diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go
index a3937dd697..d103e2b0a9 100644
--- a/tests/integration/links_test.go
+++ b/tests/integration/links_test.go
@@ -36,6 +36,7 @@ func TestLinksNoLogin(t *testing.T) {
 		"/user2/repo1/",
 		"/user2/repo1/projects",
 		"/user2/repo1/projects/1",
+		"/user2/repo1/releases/tag/delete-tag", // It's the only one existing record on release.yml which has is_tag: true
 		"/assets/img/404.png",
 		"/assets/img/500.png",
 		"/.well-known/security.txt",
diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js
index 2db8079009..f3cfa74418 100644
--- a/web_src/js/features/repo-release.js
+++ b/web_src/js/features/repo-release.js
@@ -30,8 +30,9 @@ function initTagNameEditor() {
   const newTagHelperText = el.getAttribute('data-tag-helper-new');
   const existingTagHelperText = el.getAttribute('data-tag-helper-existing');
 
-  document.getElementById('tag-name').addEventListener('keyup', (e) => {
-    const value = e.target.value;
+  const tagNameInput = document.getElementById('tag-name');
+  const hideTargetInput = function(tagNameInput) {
+    const value = tagNameInput.value;
     const tagHelper = document.getElementById('tag-helper');
     if (existingTags.includes(value)) {
       // If the tag already exists, hide the target branch selector.
@@ -41,6 +42,10 @@ function initTagNameEditor() {
       showElem('#tag-target-selector');
       tagHelper.textContent = value ? newTagHelperText : defaultTagHelperText;
     }
+  };
+  hideTargetInput(tagNameInput); // update on page load because the input may have a value
+  tagNameInput.addEventListener('input', (e) => {
+    hideTargetInput(e.target);
   });
 }
 

From 27deea7330f83ddb37c918afbb4159053d8847cb Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 2 Mar 2024 23:05:07 +0800
Subject: [PATCH 237/679] Make PR form use toast to show error message (#29545)

![image](https://github.com/go-gitea/gitea/assets/2114189/b7a14ed6-db89-4f21-a590-66cd33307233)
---
 routers/web/repo/branch.go           |  2 +-
 routers/web/repo/editor.go           |  8 ++++----
 routers/web/repo/issue.go            | 15 ++++++++-------
 routers/web/repo/pull.go             | 15 +++++++--------
 services/context/context_response.go |  9 +++++----
 web_src/js/features/common-global.js | 11 ++++++++---
 web_src/js/modules/toast.js          |  5 ++---
 7 files changed, 35 insertions(+), 30 deletions(-)

diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 05f06a3ceb..53bd599b0d 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -231,7 +231,7 @@ func CreateBranch(ctx *context.Context) {
 			if len(e.Message) == 0 {
 				ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(e.Message),
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 8f3d9612ec..6146ce4ce4 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -341,7 +341,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 			if len(errPushRej.Message) == 0 {
 				ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -353,7 +353,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 				ctx.RenderWithErr(flashError, tplEditFile, &form)
 			}
 		} else {
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
 				"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
 				"Details": utils.SanitizeFlashErrorString(err.Error()),
@@ -542,7 +542,7 @@ func DeleteFilePost(ctx *context.Context) {
 			if len(errPushRej.Message) == 0 {
 				ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -742,7 +742,7 @@ func UploadFilePost(ctx *context.Context) {
 			if len(errPushRej.Message) == 0 {
 				ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index ebaa955ac8..cec0f87471 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -9,6 +9,7 @@ import (
 	stdCtx "context"
 	"errors"
 	"fmt"
+	"html/template"
 	"math/big"
 	"net/http"
 	"net/url"
@@ -1016,7 +1017,7 @@ func NewIssue(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplIssueNew)
 }
 
-func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
+func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) template.HTML {
 	var files []string
 	for k := range errs {
 		files = append(files, k)
@@ -1028,14 +1029,14 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
 		lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
 	}
 
-	flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+	flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 		"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
 		"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
 		"Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
 	})
 	if err != nil {
 		log.Debug("render flash error: %v", err)
-		flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates")
+		flashError = ctx.Locale.Tr("repo.issues.choose.ignore_invalid_templates")
 	}
 	return flashError
 }
@@ -3296,7 +3297,7 @@ func ChangeIssueReaction(ctx *context.Context) {
 		return
 	}
 
-	html, err := ctx.RenderToString(tplReactions, map[string]any{
+	html, err := ctx.RenderToHTML(tplReactions, map[string]any{
 		"ctxData":   ctx.Data,
 		"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
 		"Reactions": issue.Reactions.GroupByType(),
@@ -3403,7 +3404,7 @@ func ChangeCommentReaction(ctx *context.Context) {
 		return
 	}
 
-	html, err := ctx.RenderToString(tplReactions, map[string]any{
+	html, err := ctx.RenderToHTML(tplReactions, map[string]any{
 		"ctxData":   ctx.Data,
 		"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
 		"Reactions": comment.Reactions.GroupByType(),
@@ -3546,8 +3547,8 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
 	return err
 }
 
-func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
-	attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{
+func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
+	attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
 		"ctxData":     ctx.Data,
 		"Attachments": attachments,
 		"Content":     content,
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 428fe23156..bf52d76e95 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1129,7 +1129,7 @@ func UpdatePullRequest(ctx *context.Context) {
 	if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
 		if models.IsErrMergeConflicts(err) {
 			conflictError := err.(models.ErrMergeConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.merge_conflict"),
 				"Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1143,7 +1143,7 @@ func UpdatePullRequest(ctx *context.Context) {
 			return
 		} else if models.IsErrRebaseConflicts(err) {
 			conflictError := err.(models.ErrRebaseConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
 				"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1275,7 +1275,7 @@ func MergePullRequest(ctx *context.Context) {
 			ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
 		} else if models.IsErrMergeConflicts(err) {
 			conflictError := err.(models.ErrMergeConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.editor.merge_conflict"),
 				"Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1288,7 +1288,7 @@ func MergePullRequest(ctx *context.Context) {
 			ctx.JSONRedirect(issue.Link())
 		} else if models.IsErrRebaseConflicts(err) {
 			conflictError := err.(models.ErrRebaseConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
 				"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1318,7 +1318,7 @@ func MergePullRequest(ctx *context.Context) {
 			if len(message) == 0 {
 				ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.pulls.push_rejected"),
 					"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1491,7 +1491,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 				ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message"))
 				return
 			}
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.push_rejected"),
 				"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
 				"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1500,8 +1500,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 				ctx.ServerError("CompareAndPullRequest.HTMLString", err)
 				return
 			}
-			ctx.Flash.Error(flashError)
-			ctx.JSONRedirect(ctx.Link + "?" + ctx.Req.URL.RawQuery) // FIXME: it's unfriendly, and will make the content lost
+			ctx.JSONError(flashError)
 			return
 		}
 		ctx.ServerError("NewPullRequest", err)
diff --git a/services/context/context_response.go b/services/context/context_response.go
index 829bca1f59..372b4cb38b 100644
--- a/services/context/context_response.go
+++ b/services/context/context_response.go
@@ -6,6 +6,7 @@ package context
 import (
 	"errors"
 	"fmt"
+	"html/template"
 	"net"
 	"net/http"
 	"net/url"
@@ -104,11 +105,11 @@ func (ctx *Context) JSONTemplate(tmpl base.TplName) {
 	}
 }
 
-// RenderToString renders the template content to a string
-func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
+// RenderToHTML renders the template content to a HTML string
+func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) {
 	var buf strings.Builder
-	err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext)
-	return buf.String(), err
+	err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext)
+	return template.HTML(buf.String()), err
 }
 
 // RenderWithErr used for page has form validation but need to prompt error to users.
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index f90591aff3..c53d43cbb2 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -91,19 +91,24 @@ async function fetchActionDoRequest(actionElem, url, opt) {
       } else {
         window.location.reload();
       }
+      return;
     } else if (resp.status >= 400 && resp.status < 500) {
       const data = await resp.json();
       // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
       // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
-      showErrorToast(data.errorMessage || `server error: ${resp.status}`);
+      if (data.errorMessage) {
+        showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
+      } else {
+        showErrorToast(`server error: ${resp.status}`);
+      }
     } else {
       showErrorToast(`server error: ${resp.status}`);
     }
   } catch (e) {
     console.error('error when doRequest', e);
-    actionElem.classList.remove('is-loading', 'small-loading-icon');
-    showErrorToast(i18n.network_error);
+    showErrorToast(`${i18n.network_error} ${e}`);
   }
+  actionElem.classList.remove('is-loading', 'small-loading-icon');
 }
 
 async function formFetchAction(e) {
diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js
index fa075aed48..d64359799c 100644
--- a/web_src/js/modules/toast.js
+++ b/web_src/js/modules/toast.js
@@ -21,13 +21,12 @@ const levels = {
 };
 
 // See https://github.com/apvarun/toastify-js#api for options
-function showToast(message, level, {gravity, position, duration, ...other} = {}) {
+function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) {
   const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
-
   const toast = Toastify({
     text: `
       <div class='toast-icon'>${svg(icon)}</div>
-      <div class='toast-body'>${htmlEscape(message)}</div>
+      <div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div>
       <button class='toast-close'>${svg('octicon-x')}</button>
     `,
     escapeMarkup: false,

From 3f081d4b54261c1b4ee4f1df40c610fdd9581ef2 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 2 Mar 2024 23:30:18 +0800
Subject: [PATCH 238/679] Rename Action.GetDisplayName to GetActDisplayName
 (#29540)

To avoid conflicting with User.GetDisplayName, because there is no data
type in template.

And it matches other methods like GetActFullName / GetActUserName
---
 models/activities/action.go         | 8 ++++----
 templates/user/dashboard/feeds.tmpl | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/models/activities/action.go b/models/activities/action.go
index 15bd9a52ac..fcc97e3872 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -225,8 +225,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string {
 	return base.EllipsisString(a.GetActUserName(ctx), 20)
 }
 
-// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
-func (a *Action) GetDisplayName(ctx context.Context) string {
+// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
+func (a *Action) GetActDisplayName(ctx context.Context) string {
 	if setting.UI.DefaultShowFullName {
 		trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
 		if len(trimmedFullName) > 0 {
@@ -236,8 +236,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string {
 	return a.ShortActUserName(ctx)
 }
 
-// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
-func (a *Action) GetDisplayNameTitle(ctx context.Context) string {
+// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
+func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
 	if setting.UI.DefaultShowFullName {
 		return a.ShortActUserName(ctx)
 	}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index 6dec610e93..0e7371ad83 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -7,7 +7,7 @@
 			<div class="flex-item-main gt-gap-3">
 				<div>
 					{{if gt .ActUser.ID 0}}
-						<a href="{{AppSubUrl}}/{{(.GetActUserName ctx) | PathEscape}}" title="{{.GetDisplayNameTitle ctx}}">{{.GetDisplayName ctx}}</a>
+						<a href="{{AppSubUrl}}/{{(.GetActUserName ctx) | PathEscape}}" title="{{.GetActDisplayNameTitle ctx}}">{{.GetActDisplayName ctx}}</a>
 					{{else}}
 						{{.ShortActUserName ctx}}
 					{{end}}

From a3f05d0d98408bb47333b19f505b21afcefa9e7c Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Sat, 2 Mar 2024 16:42:31 +0100
Subject: [PATCH 239/679] remove util.OptionalBool and related functions
 (#29513)

and migrate affected code

_last refactoring bits to replace **util.OptionalBool** with
**optional.Option[bool]**_
---
 models/actions/runner.go                      | 13 ++--
 models/auth/source.go                         |  9 +--
 models/issues/comment.go                      | 15 ++--
 models/issues/issue_search.go                 | 25 +++----
 models/issues/issue_stats.go                  |  8 +--
 models/issues/label.go                        |  3 +-
 models/issues/milestone.go                    |  3 +-
 models/issues/milestone_list.go               |  8 +--
 models/issues/milestone_test.go               | 24 +++----
 models/issues/review_list.go                  |  8 +--
 models/issues/tracked_time.go                 |  9 +--
 models/issues/tracked_time_test.go            |  8 +--
 models/packages/nuget/search.go               |  2 +-
 models/packages/package_version.go            | 25 +++----
 models/project/project.go                     | 10 ++-
 models/repo/repo.go                           |  7 +-
 models/repo/repo_test.go                      |  6 +-
 models/user/user.go                           |  8 +--
 models/webhook/webhook.go                     |  7 +-
 models/webhook/webhook_system.go              |  8 +--
 models/webhook/webhook_test.go                |  6 +-
 modules/indexer/issues/bleve/bleve.go         |  8 +--
 modules/indexer/issues/db/options.go          |  3 +-
 .../issues/elasticsearch/elasticsearch.go     |  8 +--
 modules/indexer/issues/indexer_test.go        | 10 +--
 modules/indexer/issues/internal/model.go      |  6 +-
 .../indexer/issues/internal/tests/tests.go    | 10 +--
 .../indexer/issues/meilisearch/meilisearch.go |  8 +--
 modules/util/util.go                          | 51 -------------
 routers/api/packages/cargo/cargo.go           |  3 +-
 routers/api/packages/chef/chef.go             |  5 +-
 routers/api/packages/composer/composer.go     |  3 +-
 routers/api/packages/goproxy/goproxy.go       |  3 +-
 routers/api/packages/helm/helm.go             |  5 +-
 routers/api/packages/npm/npm.go               |  7 +-
 routers/api/packages/nuget/nuget.go           | 11 +--
 routers/api/packages/rubygems/rubygems.go     |  5 +-
 routers/api/packages/swift/swift.go           |  3 +-
 routers/api/v1/admin/hooks.go                 |  3 +-
 routers/api/v1/packages/package.go            |  4 +-
 routers/api/v1/repo/issue.go                  | 41 +++++------
 routers/api/v1/repo/issue_comment.go          | 10 +--
 routers/api/v1/repo/milestone.go              |  6 +-
 routers/web/admin/hooks.go                    |  4 +-
 routers/web/admin/packages.go                 |  4 +-
 routers/web/admin/users.go                    |  4 +-
 routers/web/auth/auth.go                      |  8 +--
 routers/web/org/projects.go                   |  8 +--
 routers/web/repo/actions/actions.go           |  4 +-
 routers/web/repo/branch.go                    |  3 +-
 routers/web/repo/issue.go                     | 72 +++++++++----------
 routers/web/repo/milestone.go                 |  6 +-
 routers/web/repo/packages.go                  |  4 +-
 routers/web/repo/projects.go                  |  6 +-
 routers/web/shared/packages/packages.go       |  4 +-
 routers/web/shared/user/header.go             |  3 +-
 routers/web/user/home.go                      | 15 ++--
 routers/web/user/notification.go              | 19 +++--
 routers/web/user/package.go                   | 11 +--
 routers/web/user/setting/security/security.go |  4 +-
 services/auth/signin.go                       |  4 +-
 services/auth/source/oauth2/init.go           |  4 +-
 services/auth/source/oauth2/providers.go      |  4 +-
 services/auth/sspi.go                         |  3 +-
 services/migrations/gitea_uploader_test.go    |  8 +--
 services/packages/cleanup/cleanup.go          |  4 +-
 services/packages/container/cleanup.go        |  6 +-
 services/packages/packages.go                 |  6 +-
 services/pull/review.go                       |  8 +--
 services/repository/branch.go                 |  5 +-
 services/webhook/webhook.go                   |  7 +-
 71 files changed, 308 insertions(+), 355 deletions(-)

diff --git a/models/actions/runner.go b/models/actions/runner.go
index b646146ee6..67f003387b 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -13,6 +13,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/shared/types"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/util"
@@ -159,7 +160,7 @@ type FindRunnerOptions struct {
 	OwnerID       int64
 	Sort          string
 	Filter        string
-	IsOnline      util.OptionalBool
+	IsOnline      optional.Option[bool]
 	WithAvailable bool // not only runners belong to, but also runners can be used
 }
 
@@ -186,10 +187,12 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
 		cond = cond.And(builder.Like{"name", opts.Filter})
 	}
 
-	if opts.IsOnline.IsTrue() {
-		cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
-	} else if opts.IsOnline.IsFalse() {
-		cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+	if opts.IsOnline.Has() {
+		if opts.IsOnline.Value() {
+			cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+		} else {
+			cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+		}
 	}
 	return cond
 }
diff --git a/models/auth/source.go b/models/auth/source.go
index 1bdde8235c..f360ca9801 100644
--- a/models/auth/source.go
+++ b/models/auth/source.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -243,14 +244,14 @@ func CreateSource(ctx context.Context, source *Source) error {
 
 type FindSourcesOptions struct {
 	db.ListOptions
-	IsActive  util.OptionalBool
+	IsActive  optional.Option[bool]
 	LoginType Type
 }
 
 func (opts FindSourcesOptions) ToConds() builder.Cond {
 	conds := builder.NewCond()
-	if !opts.IsActive.IsNone() {
-		conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
+	if opts.IsActive.Has() {
+		conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()})
 	}
 	if opts.LoginType != NoType {
 		conds = conds.And(builder.Eq{"`type`": opts.LoginType})
@@ -262,7 +263,7 @@ func (opts FindSourcesOptions) ToConds() builder.Cond {
 // source of type LoginSSPI
 func IsSSPIEnabled(ctx context.Context) bool {
 	exist, err := db.Exist[Source](ctx, FindSourcesOptions{
-		IsActive:  util.OptionalBoolTrue,
+		IsActive:  optional.Some(true),
 		LoginType: SSPI,
 	}.ToConds())
 	if err != nil {
diff --git a/models/issues/comment.go b/models/issues/comment.go
index da91a83384..e37f844b5c 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -22,6 +22,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/references"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -1036,8 +1037,8 @@ type FindCommentsOptions struct {
 	TreePath    string
 	Type        CommentType
 	IssueIDs    []int64
-	Invalidated util.OptionalBool
-	IsPull      util.OptionalBool
+	Invalidated optional.Option[bool]
+	IsPull      optional.Option[bool]
 }
 
 // ToConds implements FindOptions interface
@@ -1069,11 +1070,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
 	if len(opts.TreePath) > 0 {
 		cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
 	}
-	if !opts.Invalidated.IsNone() {
-		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
+	if opts.Invalidated.Has() {
+		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()})
 	}
-	if opts.IsPull != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
+	if opts.IsPull.Has() {
+		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()})
 	}
 	return cond
 }
@@ -1082,7 +1083,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
 func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
 	comments := make([]*Comment, 0, 10)
 	sess := db.GetEngine(ctx).Where(opts.ToConds())
-	if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
+	if opts.RepoID > 0 || opts.IsPull.Has() {
 		sess.Join("INNER", "issue", "issue.id = comment.issue_id")
 	}
 
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index 7dc277327a..c5c9cecdb9 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -13,7 +13,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 	"xorm.io/xorm"
@@ -34,8 +34,8 @@ type IssuesOptions struct { //nolint
 	MilestoneIDs       []int64
 	ProjectID          int64
 	ProjectBoardID     int64
-	IsClosed           util.OptionalBool
-	IsPull             util.OptionalBool
+	IsClosed           optional.Option[bool]
+	IsPull             optional.Option[bool]
 	LabelIDs           []int64
 	IncludedLabelNames []string
 	ExcludedLabelNames []string
@@ -46,7 +46,7 @@ type IssuesOptions struct { //nolint
 	UpdatedBeforeUnix  int64
 	// prioritize issues from this repo
 	PriorityRepoID int64
-	IsArchived     util.OptionalBool
+	IsArchived     optional.Option[bool]
 	Org            *organization.Organization // issues permission scope
 	Team           *organization.Team         // issues permission scope
 	User           *user_model.User           // issues permission scope
@@ -217,8 +217,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 
 	applyRepoConditions(sess, opts)
 
-	if !opts.IsClosed.IsNone() {
-		sess.And("issue.is_closed=?", opts.IsClosed.IsTrue())
+	if opts.IsClosed.Has() {
+		sess.And("issue.is_closed=?", opts.IsClosed.Value())
 	}
 
 	if opts.AssigneeID > 0 {
@@ -260,21 +260,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 
 	applyProjectBoardCondition(sess, opts)
 
-	switch opts.IsPull {
-	case util.OptionalBoolTrue:
-		sess.And("issue.is_pull=?", true)
-	case util.OptionalBoolFalse:
-		sess.And("issue.is_pull=?", false)
+	if opts.IsPull.Has() {
+		sess.And("issue.is_pull=?", opts.IsPull.Value())
 	}
 
-	if opts.IsArchived != util.OptionalBoolNone {
-		sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
+	if opts.IsArchived.Has() {
+		sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()})
 	}
 
 	applyLabelsCondition(sess, opts)
 
 	if opts.User != nil {
-		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue()))
+		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
 	}
 
 	return sess
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
index 99ca19f804..32c5674fc9 100644
--- a/models/issues/issue_stats.go
+++ b/models/issues/issue_stats.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 	"xorm.io/xorm"
@@ -170,11 +169,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6
 		applyReviewedCondition(sess, opts.ReviewedID)
 	}
 
-	switch opts.IsPull {
-	case util.OptionalBoolTrue:
-		sess.And("issue.is_pull=?", true)
-	case util.OptionalBoolFalse:
-		sess.And("issue.is_pull=?", false)
+	if opts.IsPull.Has() {
+		sess.And("issue.is_pull=?", opts.IsPull.Value())
 	}
 
 	return sess
diff --git a/models/issues/label.go b/models/issues/label.go
index 527d8d7853..f6ecc68cd1 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/label"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -126,7 +127,7 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
 	counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
 		RepoIDs:  []int64{repoID},
 		LabelIDs: []int64{labelID},
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 
 	for _, count := range counts {
diff --git a/models/issues/milestone.go b/models/issues/milestone.go
index ea52a64c81..db0312adf0 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -302,7 +303,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
 	}
 	numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	if err != nil {
 		return err
diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go
index a73bf73c17..d1b3f0301b 100644
--- a/models/issues/milestone_list.go
+++ b/models/issues/milestone_list.go
@@ -8,7 +8,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 )
@@ -28,7 +28,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
 type FindMilestoneOptions struct {
 	db.ListOptions
 	RepoID   int64
-	IsClosed util.OptionalBool
+	IsClosed optional.Option[bool]
 	Name     string
 	SortType string
 	RepoCond builder.Cond
@@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond {
 	if opts.RepoID != 0 {
 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 	}
-	if opts.IsClosed != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
+	if opts.IsClosed.Has() {
+		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
 	}
 	if opts.RepoCond != nil && opts.RepoCond.IsValid() {
 		cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go
index 7477af92c8..e5f6f15ca2 100644
--- a/models/issues/milestone_test.go
+++ b/models/issues/milestone_test.go
@@ -11,10 +11,10 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -39,10 +39,10 @@ func TestGetMilestoneByRepoID(t *testing.T) {
 func TestGetMilestonesByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64, state api.StateType) {
-		var isClosed util.OptionalBool
+		var isClosed optional.Option[bool]
 		switch state {
 		case api.StateClosed, api.StateOpen:
-			isClosed = util.OptionalBoolOf(state == api.StateClosed)
+			isClosed = optional.Some(state == api.StateClosed)
 		}
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 		milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
@@ -84,7 +84,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 
 	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   unittest.NonexistentID,
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	assert.NoError(t, err)
 	assert.Len(t, milestones, 0)
@@ -101,7 +101,7 @@ func TestGetMilestones(t *testing.T) {
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoID:   repo.ID,
-				IsClosed: util.OptionalBoolFalse,
+				IsClosed: optional.Some(false),
 				SortType: sortType,
 			})
 			assert.NoError(t, err)
@@ -118,7 +118,7 @@ func TestGetMilestones(t *testing.T) {
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoID:   repo.ID,
-				IsClosed: util.OptionalBoolTrue,
+				IsClosed: optional.Some(true),
 				Name:     "",
 				SortType: sortType,
 			})
@@ -178,7 +178,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 		count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 			RepoID:   repoID,
-			IsClosed: util.OptionalBoolTrue,
+			IsClosed: optional.Some(true),
 		})
 		assert.NoError(t, err)
 		assert.EqualValues(t, repo.NumClosedMilestones, count)
@@ -189,7 +189,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 
 	count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   unittest.NonexistentID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, count)
@@ -206,7 +206,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 
 	openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoIDs:  []int64{1, 2},
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1OpenCount, openCounts[1])
@@ -215,7 +215,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 	closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
 		issues_model.FindMilestoneOptions{
 			RepoIDs:  []int64{1, 2},
-			IsClosed: util.OptionalBoolTrue,
+			IsClosed: optional.Some(true),
 		})
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
@@ -234,7 +234,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoIDs:  []int64{repo1.ID, repo2.ID},
-				IsClosed: util.OptionalBoolFalse,
+				IsClosed: optional.Some(false),
 				SortType: sortType,
 			})
 			assert.NoError(t, err)
@@ -252,7 +252,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 						PageSize: setting.UI.IssuePagingNum,
 					},
 					RepoIDs:  []int64{repo1.ID, repo2.ID},
-					IsClosed: util.OptionalBoolTrue,
+					IsClosed: optional.Some(true),
 					SortType: sortType,
 				})
 			assert.NoError(t, err)
diff --git a/models/issues/review_list.go b/models/issues/review_list.go
index 282f18b4f7..ec6cb07988 100644
--- a/models/issues/review_list.go
+++ b/models/issues/review_list.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 )
@@ -68,7 +68,7 @@ type FindReviewOptions struct {
 	IssueID      int64
 	ReviewerID   int64
 	OfficialOnly bool
-	Dismissed    util.OptionalBool
+	Dismissed    optional.Option[bool]
 }
 
 func (opts *FindReviewOptions) toCond() builder.Cond {
@@ -85,8 +85,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond {
 	if opts.OfficialOnly {
 		cond = cond.And(builder.Eq{"official": true})
 	}
-	if !opts.Dismissed.IsNone() {
-		cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.IsTrue()})
+	if opts.Dismissed.Has() {
+		cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()})
 	}
 	return cond
 }
diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go
index 91c4832e49..4063ca043b 100644
--- a/models/issues/tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
@@ -340,7 +341,7 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
 }
 
 // GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
-func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool) (int64, error) {
+func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool]) (int64, error) {
 	if len(opts.IssueIDs) <= MaxQueryParameters {
 		return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
 	}
@@ -363,7 +364,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed
 	return accum, nil
 }
 
-func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool, issueIDs []int64) (int64, error) {
+func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool], issueIDs []int64) (int64, error) {
 	sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
 		sess := db.GetEngine(ctx).
 			Table("tracked_time").
@@ -378,8 +379,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC
 	}
 
 	session := sumSession(opts, issueIDs)
-	if !isClosed.IsNone() {
-		session = session.And("issue.is_closed = ?", isClosed.IsTrue())
+	if isClosed.Has() {
+		session = session.And("issue.is_closed = ?", isClosed.Value())
 	}
 	return session.SumInt(new(trackedTime), "tracked_time.time")
 }
diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go
index 9beb862ffb..d82bff967a 100644
--- a/models/issues/tracked_time_test.go
+++ b/models/issues/tracked_time_test.go
@@ -11,7 +11,7 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -120,15 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
 func TestGetIssueTotalTrackedTime(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolFalse)
+	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false))
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3682, ttt)
 
-	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolTrue)
+	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true))
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, ttt)
 
-	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolNone)
+	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]())
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3682, ttt)
 }
diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go
index 53cdf2d4ad..7a505ff08f 100644
--- a/models/packages/nuget/search.go
+++ b/models/packages/nuget/search.go
@@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption
 
 func toConds(opts *packages_model.PackageSearchOptions) builder.Cond {
 	var cond builder.Cond = builder.Eq{
-		"package.is_internal": opts.IsInternal.IsTrue(),
+		"package.is_internal": opts.IsInternal.Value(),
 		"package.owner_id":    opts.OwnerID,
 		"package.type":        packages_model.TypeNuGet,
 	}
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 8fc475691b..505dbaa0a5 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -105,7 +106,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
 			ExactMatch: true,
 			Value:      version,
 		},
-		IsInternal: util.OptionalBoolOf(isInternal),
+		IsInternal: optional.Some(isInternal),
 		Paginator:  db.NewAbsoluteListOptions(0, 1),
 	})
 	if err != nil {
@@ -122,7 +123,7 @@ func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Ty
 	pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
 		OwnerID:    ownerID,
 		Type:       packageType,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	return pvs, err
 }
@@ -136,7 +137,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty
 			ExactMatch: true,
 			Value:      name,
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	return pvs, err
 }
@@ -182,18 +183,18 @@ type PackageSearchOptions struct {
 	Name            SearchValue       // only results with the specific name are found
 	Version         SearchValue       // only results with the specific version are found
 	Properties      map[string]string // only results are found which contain all listed version properties with the specific value
-	IsInternal      util.OptionalBool
-	HasFileWithName string            // only results are found which are associated with a file with the specific name
-	HasFiles        util.OptionalBool // only results are found which have associated files
+	IsInternal      optional.Option[bool]
+	HasFileWithName string                // only results are found which are associated with a file with the specific name
+	HasFiles        optional.Option[bool] // only results are found which have associated files
 	Sort            VersionSort
 	db.Paginator
 }
 
 func (opts *PackageSearchOptions) ToConds() builder.Cond {
 	cond := builder.NewCond()
-	if !opts.IsInternal.IsNone() {
+	if opts.IsInternal.Has() {
 		cond = builder.Eq{
-			"package_version.is_internal": opts.IsInternal.IsTrue(),
+			"package_version.is_internal": opts.IsInternal.Value(),
 		}
 	}
 
@@ -250,10 +251,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond {
 		cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond)))
 	}
 
-	if !opts.HasFiles.IsNone() {
+	if opts.HasFiles.Has() {
 		filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id")))
 
-		if opts.HasFiles.IsFalse() {
+		if !opts.HasFiles.Value() {
 			filesCond = builder.Not{filesCond}
 		}
 
@@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
 		And(builder.Expr("pv2.id IS NULL"))
 
 	joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))")
-	if !opts.IsInternal.IsNone() {
-		joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()})
+	if opts.IsInternal.Has() {
+		joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()})
 	}
 
 	sess := db.GetEngine(ctx).
diff --git a/models/project/project.go b/models/project/project.go
index 42b06e58c9..8f9ee2a99e 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -12,6 +12,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -196,7 +197,7 @@ type SearchOptions struct {
 	db.ListOptions
 	OwnerID  int64
 	RepoID   int64
-	IsClosed util.OptionalBool
+	IsClosed optional.Option[bool]
 	OrderBy  db.SearchOrderBy
 	Type     Type
 	Title    string
@@ -207,11 +208,8 @@ func (opts SearchOptions) ToConds() builder.Cond {
 	if opts.RepoID > 0 {
 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 	}
-	switch opts.IsClosed {
-	case util.OptionalBoolTrue:
-		cond = cond.And(builder.Eq{"is_closed": true})
-	case util.OptionalBoolFalse:
-		cond = cond.And(builder.Eq{"is_closed": false})
+	if opts.IsClosed.Has() {
+		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
 	}
 
 	if opts.Type > 0 {
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 13493ba6e8..5ce3ecb58a 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -840,7 +841,7 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
 
 type CountRepositoryOptions struct {
 	OwnerID int64
-	Private util.OptionalBool
+	Private optional.Option[bool]
 }
 
 // CountRepositories returns number of repositories.
@@ -852,8 +853,8 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64,
 	if opts.OwnerID > 0 {
 		sess.And("owner_id = ?", opts.OwnerID)
 	}
-	if !opts.Private.IsNone() {
-		sess.And("is_private=?", opts.Private.IsTrue())
+	if opts.Private.Has() {
+		sess.And("is_private=?", opts.Private.Value())
 	}
 
 	count, err := sess.Count(new(Repository))
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index ca9209d751..1a870224bf 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -12,17 +12,17 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/test"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
 
 var (
 	countRepospts        = repo_model.CountRepositoryOptions{OwnerID: 10}
-	countReposptsPublic  = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse}
-	countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue}
+	countReposptsPublic  = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
+	countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
 )
 
 func TestGetRepositoryCount(t *testing.T) {
diff --git a/models/user/user.go b/models/user/user.go
index e92bbd4d0b..a898e71a2d 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -715,7 +715,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 
 // IsLastAdminUser check whether user is the last admin
 func IsLastAdminUser(ctx context.Context, user *User) bool {
-	if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 {
+	if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 {
 		return true
 	}
 	return false
@@ -724,7 +724,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool {
 // CountUserFilter represent optional filters for CountUsers
 type CountUserFilter struct {
 	LastLoginSince *int64
-	IsAdmin        util.OptionalBool
+	IsAdmin        optional.Option[bool]
 }
 
 // CountUsers returns number of users.
@@ -742,8 +742,8 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
 			cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince})
 		}
 
-		if !opts.IsAdmin.IsNone() {
-			cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
+		if opts.IsAdmin.Has() {
+			cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
 		}
 	}
 
diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index 4a84a3d411..894357e36a 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -433,7 +434,7 @@ type ListWebhookOptions struct {
 	db.ListOptions
 	RepoID   int64
 	OwnerID  int64
-	IsActive util.OptionalBool
+	IsActive optional.Option[bool]
 }
 
 func (opts ListWebhookOptions) ToConds() builder.Cond {
@@ -444,8 +445,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond {
 	if opts.OwnerID != 0 {
 		cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
 	}
-	if !opts.IsActive.IsNone() {
-		cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()})
+	if opts.IsActive.Has() {
+		cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()})
 	}
 	return cond
 }
diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go
index 2e89f9547b..a2a9ee321a 100644
--- a/models/webhook/webhook_system.go
+++ b/models/webhook/webhook_system.go
@@ -8,7 +8,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 // GetDefaultWebhooks returns all admin-default webhooks.
@@ -34,15 +34,15 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error)
 }
 
 // GetSystemWebhooks returns all admin system webhooks.
-func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
+func GetSystemWebhooks(ctx context.Context, isActive optional.Option[bool]) ([]*Webhook, error) {
 	webhooks := make([]*Webhook, 0, 5)
-	if isActive.IsNone() {
+	if !isActive.Has() {
 		return webhooks, db.GetEngine(ctx).
 			Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
 			Find(&webhooks)
 	}
 	return webhooks, db.GetEngine(ctx).
-		Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
+		Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.Value()).
 		Find(&webhooks)
 }
 
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index 694fd7a873..c70c8e99fc 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -11,9 +11,9 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/stretchr/testify/assert"
@@ -123,7 +123,7 @@ func TestGetWebhookByOwnerID(t *testing.T) {
 
 func TestGetActiveWebhooksByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue})
+	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
 	assert.NoError(t, err)
 	if assert.Len(t, hooks, 1) {
 		assert.Equal(t, int64(1), hooks[0].ID)
@@ -143,7 +143,7 @@ func TestGetWebhooksByRepoID(t *testing.T) {
 
 func TestGetActiveWebhooksByOwnerID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue})
+	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
 	assert.NoError(t, err)
 	if assert.Len(t, hooks, 1) {
 		assert.Equal(t, int64(3), hooks[0].ID)
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index 7c82cfbb79..6a5d65cb66 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -175,11 +175,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...))
 	}
 
-	if !options.IsPull.IsNone() {
-		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.IsTrue(), "is_pull"))
+	if options.IsPull.Has() {
+		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull"))
 	}
-	if !options.IsClosed.IsNone() {
-		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.IsTrue(), "is_closed"))
+	if options.IsClosed.Has() {
+		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed"))
 	}
 
 	if options.NoLabelOnly {
diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go
index 5406715bbc..69146573a8 100644
--- a/modules/indexer/issues/db/options.go
+++ b/modules/indexer/issues/db/options.go
@@ -11,6 +11,7 @@ import (
 	issue_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) {
@@ -75,7 +76,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 		UpdatedAfterUnix:   convertInt64(options.UpdatedAfterUnix),
 		UpdatedBeforeUnix:  convertInt64(options.UpdatedBeforeUnix),
 		PriorityRepoID:     0,
-		IsArchived:         0,
+		IsArchived:         optional.None[bool](),
 		Org:                nil,
 		Team:               nil,
 		User:               nil,
diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go
index d059f76b32..3acd3ade71 100644
--- a/modules/indexer/issues/elasticsearch/elasticsearch.go
+++ b/modules/indexer/issues/elasticsearch/elasticsearch.go
@@ -153,11 +153,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.Must(q)
 	}
 
-	if !options.IsPull.IsNone() {
-		query.Must(elastic.NewTermQuery("is_pull", options.IsPull.IsTrue()))
+	if options.IsPull.Has() {
+		query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value()))
 	}
-	if !options.IsClosed.IsNone() {
-		query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.IsTrue()))
+	if options.IsClosed.Has() {
+		query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value()))
 	}
 
 	if options.NoLabelOnly {
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index 3b96686d98..10ffa7cbe6 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -10,8 +10,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	_ "code.gitea.io/gitea/models"
 	_ "code.gitea.io/gitea/models/actions"
@@ -210,13 +210,13 @@ func searchIssueIsPull(t *testing.T) {
 	}{
 		{
 			SearchOptions{
-				IsPull: util.OptionalBoolFalse,
+				IsPull: optional.Some(false),
 			},
 			[]int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1},
 		},
 		{
 			SearchOptions{
-				IsPull: util.OptionalBoolTrue,
+				IsPull: optional.Some(true),
 			},
 			[]int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2},
 		},
@@ -237,13 +237,13 @@ func searchIssueIsClosed(t *testing.T) {
 	}{
 		{
 			SearchOptions{
-				IsClosed: util.OptionalBoolFalse,
+				IsClosed: optional.Some(false),
 			},
 			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
 		},
 		{
 			SearchOptions{
-				IsClosed: util.OptionalBoolTrue,
+				IsClosed: optional.Some(true),
 			},
 			[]int64{5, 4},
 		},
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index 031745dd2f..947335d8ce 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -5,8 +5,8 @@ package internal
 
 import (
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // IndexerData data stored in the issue indexer
@@ -77,8 +77,8 @@ type SearchOptions struct {
 	RepoIDs   []int64 // repository IDs which the issues belong to
 	AllPublic bool    // if include all public repositories
 
-	IsPull   util.OptionalBool // if the issues is a pull request
-	IsClosed util.OptionalBool // if the issues is closed
+	IsPull   optional.Option[bool] // if the issues is a pull request
+	IsClosed optional.Option[bool] // if the issues is closed
 
 	IncludedLabelIDs    []int64 // labels the issues have
 	ExcludedLabelIDs    []int64 // labels the issues don't have
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 06fddeb65b..6724471539 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -16,8 +16,8 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -166,7 +166,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsPull: util.OptionalBoolFalse,
+			IsPull: optional.Some(false),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -182,7 +182,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsPull: util.OptionalBoolTrue,
+			IsPull: optional.Some(true),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -198,7 +198,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsClosed: util.OptionalBoolFalse,
+			IsClosed: optional.Some(false),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -214,7 +214,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsClosed: util.OptionalBoolTrue,
+			IsClosed: optional.Some(true),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index ab8dcd0af4..325883196b 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -131,11 +131,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.And(q)
 	}
 
-	if !options.IsPull.IsNone() {
-		query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.IsTrue()))
+	if options.IsPull.Has() {
+		query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.Value()))
 	}
-	if !options.IsClosed.IsNone() {
-		query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.IsTrue()))
+	if options.IsClosed.Has() {
+		query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value()))
 	}
 
 	if options.NoLabelOnly {
diff --git a/modules/util/util.go b/modules/util/util.go
index 615f654e47..5c75158196 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -17,57 +17,6 @@ import (
 	"golang.org/x/text/language"
 )
 
-// OptionalBool a boolean that can be "null"
-type OptionalBool byte
-
-const (
-	// OptionalBoolNone a "null" boolean value
-	OptionalBoolNone OptionalBool = iota
-	// OptionalBoolTrue a "true" boolean value
-	OptionalBoolTrue
-	// OptionalBoolFalse a "false" boolean value
-	OptionalBoolFalse
-)
-
-// IsTrue return true if equal to OptionalBoolTrue
-func (o OptionalBool) IsTrue() bool {
-	return o == OptionalBoolTrue
-}
-
-// IsFalse return true if equal to OptionalBoolFalse
-func (o OptionalBool) IsFalse() bool {
-	return o == OptionalBoolFalse
-}
-
-// IsNone return true if equal to OptionalBoolNone
-func (o OptionalBool) IsNone() bool {
-	return o == OptionalBoolNone
-}
-
-// ToGeneric converts OptionalBool to optional.Option[bool]
-func (o OptionalBool) ToGeneric() optional.Option[bool] {
-	if o.IsNone() {
-		return optional.None[bool]()
-	}
-	return optional.Some[bool](o.IsTrue())
-}
-
-// OptionalBoolFromGeneric converts optional.Option[bool] to OptionalBool
-func OptionalBoolFromGeneric(o optional.Option[bool]) OptionalBool {
-	if o.Has() {
-		return OptionalBoolOf(o.Value())
-	}
-	return OptionalBoolNone
-}
-
-// OptionalBoolOf get the corresponding OptionalBool of a bool
-func OptionalBoolOf(b bool) OptionalBool {
-	if b {
-		return OptionalBoolTrue
-	}
-	return OptionalBoolFalse
-}
-
 // OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
 func OptionalBoolParse(s string) optional.Option[bool] {
 	v, e := strconv.ParseBool(s)
diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go
index d01a13d78f..140e532efd 100644
--- a/routers/api/packages/cargo/cargo.go
+++ b/routers/api/packages/cargo/cargo.go
@@ -13,6 +13,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	cargo_module "code.gitea.io/gitea/modules/packages/cargo"
 	"code.gitea.io/gitea/modules/setting"
@@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
 			OwnerID:    ctx.Package.Owner.ID,
 			Type:       packages_model.TypeCargo,
 			Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Paginator:  &paginator,
 		},
 	)
diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go
index 720fce0a2a..b49f4e9d0a 100644
--- a/routers/api/packages/chef/chef.go
+++ b/routers/api/packages/chef/chef.go
@@ -15,6 +15,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/setting"
@@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) {
 	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeChef,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeChef,
 		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator: db.NewAbsoluteListOptions(
 			ctx.FormInt("start"),
 			ctx.FormInt("items"),
diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go
index 346408d261..a045da40de 100644
--- a/routers/api/packages/composer/composer.go
+++ b/routers/api/packages/composer/composer.go
@@ -14,6 +14,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	composer_module "code.gitea.io/gitea/modules/packages/composer"
 	"code.gitea.io/gitea/modules/setting"
@@ -66,7 +67,7 @@ func SearchPackages(ctx *context.Context) {
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeComposer,
 		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  &paginator,
 	}
 	if ctx.FormTrim("type") != "" {
diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go
index 9eb515d9a1..d658066bb4 100644
--- a/routers/api/packages/goproxy/goproxy.go
+++ b/routers/api/packages/goproxy/goproxy.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
 	"code.gitea.io/gitea/modules/util"
@@ -129,7 +130,7 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (
 				Value:      name,
 				ExactMatch: true,
 			},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Sort:       packages_model.SortCreatedDesc,
 		})
 		if err != nil {
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
index e7a346d9ca..efdb83ec0e 100644
--- a/routers/api/packages/helm/helm.go
+++ b/routers/api/packages/helm/helm.go
@@ -15,6 +15,7 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	helm_module "code.gitea.io/gitea/modules/packages/helm"
 	"code.gitea.io/gitea/modules/setting"
@@ -42,7 +43,7 @@ func Index(ctx *context.Context) {
 	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeHelm,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -110,7 +111,7 @@ func DownloadPackageFile(ctx *context.Context) {
 			Value:      ctx.Params("package"),
 		},
 		HasFileWithName: filename,
-		IsInternal:      util.OptionalBoolFalse,
+		IsInternal:      optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index 72b4305928..84acfffae2 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -17,6 +17,7 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	npm_module "code.gitea.io/gitea/modules/packages/npm"
 	"code.gitea.io/gitea/modules/setting"
@@ -120,7 +121,7 @@ func DownloadPackageFileByName(ctx *context.Context) {
 			Value:      packageNameFromParams(ctx),
 		},
 		HasFileWithName: filename,
-		IsInternal:      util.OptionalBoolFalse,
+		IsInternal:      optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -395,7 +396,7 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
 			Properties: map[string]string{
 				npm_module.TagProperty: tag,
 			},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 		})
 		if err != nil {
 			return err
@@ -431,7 +432,7 @@ func PackageSearch(ctx *context.Context) {
 	pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeNpm,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Name: packages_model.SearchValue{
 			ExactMatch: false,
 			Value:      ctx.FormTrim("text"),
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index a0273aad5a..c28bc6c9d9 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -18,6 +18,7 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	nuget_model "code.gitea.io/gitea/models/packages/nuget"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	nuget_module "code.gitea.io/gitea/modules/packages/nuget"
 	"code.gitea.io/gitea/modules/setting"
@@ -122,7 +123,7 @@ func SearchServiceV2(ctx *context.Context) {
 		Name: packages_model.SearchValue{
 			Value: getSearchTerm(ctx),
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  paginator,
 	})
 	if err != nil {
@@ -172,7 +173,7 @@ func SearchServiceV2Count(ctx *context.Context) {
 		Name: packages_model.SearchValue{
 			Value: getSearchTerm(ctx),
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -187,7 +188,7 @@ func SearchServiceV3(ctx *context.Context) {
 	pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator: db.NewAbsoluteListOptions(
 			ctx.FormInt("skip"),
 			ctx.FormInt("take"),
@@ -313,7 +314,7 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
 			ExactMatch: true,
 			Value:      packageName,
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  paginator,
 	})
 	if err != nil {
@@ -358,7 +359,7 @@ func EnumeratePackageVersionsV2Count(ctx *context.Context) {
 			ExactMatch: true,
 			Value:      strings.Trim(ctx.FormTrim("id"), "'"),
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
index 5d05b6d524..d2fbcd01f0 100644
--- a/routers/api/packages/rubygems/rubygems.go
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -13,6 +13,7 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
 	"code.gitea.io/gitea/modules/util"
@@ -43,7 +44,7 @@ func EnumeratePackagesLatest(ctx *context.Context) {
 	pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeRubyGems,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -304,7 +305,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
 		OwnerID:         ctx.Package.Owner.ID,
 		Type:            packages_model.TypeRubyGems,
 		HasFileWithName: filename,
-		IsInternal:      util.OptionalBoolFalse,
+		IsInternal:      optional.Some(false),
 	})
 	return pvs, err
 }
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 1fc8baeaac..a9da3ea9c2 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -15,6 +15,7 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	swift_module "code.gitea.io/gitea/modules/packages/swift"
 	"code.gitea.io/gitea/modules/setting"
@@ -433,7 +434,7 @@ func LookupPackageIdentifiers(ctx *context.Context) {
 		Properties: map[string]string{
 			swift_module.PropertyRepositoryURL: url,
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go
index 2217d002a0..4c168b55bf 100644
--- a/routers/api/v1/admin/hooks.go
+++ b/routers/api/v1/admin/hooks.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
@@ -37,7 +38,7 @@ func ListHooks(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/HookList"
 
-	sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+	sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]())
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
 		return
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index 3be31b13ae..b38aa13167 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -7,8 +7,8 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
@@ -60,7 +60,7 @@ func ListPackages(ctx *context.APIContext) {
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages.Type(packageType),
 		Name:       packages.SearchValue{Value: query},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  &listOptions,
 	})
 	if err != nil {
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 227e0e725c..1b2ecd474b 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -23,7 +23,6 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/context"
@@ -123,14 +122,14 @@ func SearchIssues(ctx *context.APIContext) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	var (
@@ -205,14 +204,14 @@ func SearchIssues(ctx *context.APIContext) {
 		keyword = ""
 	}
 
-	var isPull util.OptionalBool
+	var isPull optional.Option[bool]
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
+		isPull = optional.Some(false)
 	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.None[bool]()
 	}
 
 	var includedAnyLabels []int64
@@ -397,14 +396,14 @@ func ListIssues(ctx *context.APIContext) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	keyword := ctx.FormTrim("q")
@@ -453,31 +452,29 @@ func ListIssues(ctx *context.APIContext) {
 
 	listOptions := utils.GetListOptions(ctx)
 
-	var isPull util.OptionalBool
+	isPull := optional.None[bool]()
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
-	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.Some(false)
 	}
 
-	if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) {
+	if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
 		ctx.NotFound()
 		return
 	}
 
-	if isPull == util.OptionalBoolNone {
+	if !isPull.Has() {
 		canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
 		canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
 		if !canReadIssues && !canReadPulls {
 			ctx.NotFound()
 			return
 		} else if !canReadIssues {
-			isPull = util.OptionalBoolTrue
+			isPull = optional.Some(true)
 		} else if !canReadPulls {
-			isPull = util.OptionalBoolFalse
+			isPull = optional.Some(false)
 		}
 	}
 
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 763419b7a2..6209e960af 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -14,8 +14,8 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/context"
@@ -278,15 +278,15 @@ func ListRepoIssueComments(ctx *context.APIContext) {
 		return
 	}
 
-	var isPull util.OptionalBool
+	var isPull optional.Option[bool]
 	canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
 	canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
 	if canReadIssue && canReadPull {
-		isPull = util.OptionalBoolNone
+		isPull = optional.None[bool]()
 	} else if canReadIssue {
-		isPull = util.OptionalBoolFalse
+		isPull = optional.Some(false)
 	} else if canReadPull {
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	} else {
 		ctx.NotFound()
 		return
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index d4c828fe8b..b9534016e4 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -11,9 +11,9 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/context"
@@ -61,10 +61,10 @@ func ListMilestones(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	state := api.StateType(ctx.FormString("state"))
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch state {
 	case api.StateClosed, api.StateOpen:
-		isClosed = util.OptionalBoolOf(state == api.StateClosed)
+		isClosed = optional.Some(state == api.StateClosed)
 	}
 
 	milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index 8d4c66fdb2..8d59fbb858 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -8,8 +8,8 @@ import (
 
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -35,7 +35,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
 
 	sys["Title"] = ctx.Tr("admin.systemhooks")
 	sys["Description"] = ctx.Tr("admin.systemhooks.desc")
-	sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+	sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
 	sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
 	sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
 	if err != nil {
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index 7c16b69a85..39f064a1be 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -11,8 +11,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 	packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
@@ -36,7 +36,7 @@ func Packages(ctx *context.Context) {
 		Type:       packages_model.Type(packageType),
 		Name:       packages_model.SearchValue{Value: query},
 		Sort:       sort,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator: &db.ListOptions{
 			PageSize: setting.UI.PackagesPagingNum,
 			Page:     page,
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index bbdbc820d7..ca47175401 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -96,7 +96,7 @@ func NewUser(ctx *context.Context) {
 	ctx.Data["login_type"] = "0-0"
 
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	})
 	if err != nil {
 		ctx.ServerError("auth.Sources", err)
@@ -117,7 +117,7 @@ func NewUserPost(ctx *context.Context) {
 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
 
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	})
 	if err != nil {
 		ctx.ServerError("auth.Sources", err)
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 7704a110a6..04e410543d 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -163,7 +163,7 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -186,7 +186,7 @@ func SignIn(ctx *context.Context) {
 func SignInPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -410,7 +410,7 @@ func SignUp(ctx *context.Context) {
 
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignUp", err)
 		return
@@ -439,7 +439,7 @@ func SignUpPost(ctx *context.Context) {
 
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignUp", err)
 		return
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 82cd91997a..ad8bb90d9e 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -18,9 +18,9 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	"code.gitea.io/gitea/services/context"
@@ -67,7 +67,7 @@ func Projects(ctx *context.Context) {
 			PageSize: setting.UI.IssuePagingNum,
 		},
 		OwnerID:  ctx.ContextUser.ID,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType),
 		Type:     projectType,
 		Title:    keyword,
@@ -79,7 +79,7 @@ func Projects(ctx *context.Context) {
 
 	opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
 		OwnerID:  ctx.ContextUser.ID,
-		IsClosed: util.OptionalBoolOf(!isShowClosed),
+		IsClosed: optional.Some(!isShowClosed),
 		Type:     projectType,
 	})
 	if err != nil {
@@ -388,7 +388,7 @@ func ViewProject(ctx *context.Context) {
 			if len(referencedIDs) > 0 {
 				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
 					IssueIDs: referencedIDs,
-					IsPull:   util.OptionalBoolTrue,
+					IsPull:   optional.Some(true),
 				}); err == nil {
 					linkedPrsMap[issue.ID] = linkedPrs
 				}
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index e784912377..f27329aa0f 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -16,8 +16,8 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/repo"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
@@ -78,7 +78,7 @@ func List(ctx *context.Context) {
 		// Get all runner labels
 		runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
 			RepoID:        ctx.Repo.Repository.ID,
-			IsOnline:      util.OptionalBoolTrue,
+			IsOnline:      optional.Some(true),
 			WithAvailable: true,
 		})
 		if err != nil {
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 53bd599b0d..ae51f0596b 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -18,6 +18,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -53,7 +54,7 @@ func Branches(ctx *context.Context) {
 
 	kw := ctx.FormString("q")
 
-	defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, kw, page, pageSize)
+	defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
 	if err != nil {
 		ctx.ServerError("LoadBranches", err)
 		return
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index cec0f87471..1abd5e2ba5 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -140,7 +140,7 @@ func MustAllowPulls(ctx *context.Context) {
 	}
 }
 
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
+func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
 	var err error
 	viewType := ctx.FormString("type")
 	sortType := ctx.FormString("sort")
@@ -241,18 +241,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		}
 	}
 
-	var isShowClosed util.OptionalBool
+	var isShowClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isShowClosed = util.OptionalBoolTrue
+		isShowClosed = optional.Some(true)
 	case "all":
-		isShowClosed = util.OptionalBoolNone
+		isShowClosed = optional.None[bool]()
 	default:
-		isShowClosed = util.OptionalBoolFalse
+		isShowClosed = optional.Some(false)
 	}
 	// if there are closed issues and no open issues, default to showing all issues
 	if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
-		isShowClosed = util.OptionalBoolNone
+		isShowClosed = optional.None[bool]()
 	}
 
 	if repo.IsTimetrackerEnabled(ctx) {
@@ -272,10 +272,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	}
 
 	var total int
-	switch isShowClosed {
-	case util.OptionalBoolTrue:
+	switch {
+	case isShowClosed.Value():
 		total = int(issueStats.ClosedCount)
-	case util.OptionalBoolNone:
+	case !isShowClosed.Has():
 		total = int(issueStats.OpenCount + issueStats.ClosedCount)
 	default:
 		total = int(issueStats.OpenCount)
@@ -431,7 +431,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		return
 	}
 
-	pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue())
+	pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value())
 	if err != nil {
 		ctx.ServerError("GetPinnedIssues", err)
 		return
@@ -461,10 +461,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	ctx.Data["AssigneeID"] = assigneeID
 	ctx.Data["PosterID"] = posterID
 	ctx.Data["Keyword"] = keyword
-	switch isShowClosed {
-	case util.OptionalBoolTrue:
+	switch {
+	case isShowClosed.Value():
 		ctx.Data["State"] = "closed"
-	case util.OptionalBoolNone:
+	case !isShowClosed.Has():
 		ctx.Data["State"] = "all"
 	default:
 		ctx.Data["State"] = "open"
@@ -513,7 +513,7 @@ func Issues(ctx *context.Context) {
 		ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
 	}
 
-	issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
+	issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
 	if ctx.Written() {
 		return
 	}
@@ -555,7 +555,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
 	var err error
 	ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
@@ -563,7 +563,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
 	}
 	ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
@@ -591,7 +591,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 	projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
 		ListOptions: db.ListOptionsAll,
 		RepoID:      repo.ID,
-		IsClosed:    util.OptionalBoolFalse,
+		IsClosed:    optional.Some(false),
 		Type:        project_model.TypeRepository,
 	})
 	if err != nil {
@@ -601,7 +601,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 	projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
 		ListOptions: db.ListOptionsAll,
 		OwnerID:     repo.OwnerID,
-		IsClosed:    util.OptionalBoolFalse,
+		IsClosed:    optional.Some(false),
 		Type:        repoOwnerType,
 	})
 	if err != nil {
@@ -614,7 +614,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 	projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
 		ListOptions: db.ListOptionsAll,
 		RepoID:      repo.ID,
-		IsClosed:    util.OptionalBoolTrue,
+		IsClosed:    optional.Some(true),
 		Type:        project_model.TypeRepository,
 	})
 	if err != nil {
@@ -624,7 +624,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 	projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
 		ListOptions: db.ListOptionsAll,
 		OwnerID:     repo.OwnerID,
-		IsClosed:    util.OptionalBoolTrue,
+		IsClosed:    optional.Some(true),
 		Type:        repoOwnerType,
 	})
 	if err != nil {
@@ -2502,14 +2502,14 @@ func SearchIssues(ctx *context.Context) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	var (
@@ -2584,14 +2584,12 @@ func SearchIssues(ctx *context.Context) {
 		keyword = ""
 	}
 
-	var isPull util.OptionalBool
+	isPull := optional.None[bool]()
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
-	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.Some(false)
 	}
 
 	var includedAnyLabels []int64
@@ -2726,14 +2724,14 @@ func ListIssues(ctx *context.Context) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	keyword := ctx.FormTrim("q")
@@ -2785,14 +2783,12 @@ func ListIssues(ctx *context.Context) {
 		projectID = &v
 	}
 
-	var isPull util.OptionalBool
+	isPull := optional.None[bool]()
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
-	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.Some(false)
 	}
 
 	// FIXME: we should be more efficient here
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 49ac94aaf1..c41b844ce4 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -14,9 +14,9 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
@@ -51,7 +51,7 @@ func Milestones(ctx *context.Context) {
 			PageSize: setting.UI.IssuePagingNum,
 		},
 		RepoID:   ctx.Repo.Repository.ID,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		SortType: sortType,
 		Name:     keyword,
 	})
@@ -292,7 +292,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
 	ctx.Data["Title"] = milestone.Name
 	ctx.Data["Milestone"] = milestone
 
-	issues(ctx, milestoneID, projectID, util.OptionalBoolNone)
+	issues(ctx, milestoneID, projectID, optional.None[bool]())
 
 	ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
 	ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index 6ed5909dcf..11874ab0d0 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -10,8 +10,8 @@ import (
 	"code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -37,7 +37,7 @@ func Packages(ctx *context.Context) {
 		RepoID:     ctx.Repo.Repository.ID,
 		Type:       packages.Type(packageType),
 		Name:       packages.SearchValue{Value: query},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("SearchLatestVersions", err)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 1f9ee727c3..4c171defbd 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -20,8 +20,8 @@ import (
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
@@ -78,7 +78,7 @@ func Projects(ctx *context.Context) {
 			Page:     page,
 		},
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType),
 		Type:     project_model.TypeRepository,
 		Title:    keyword,
@@ -349,7 +349,7 @@ func ViewProject(ctx *context.Context) {
 			if len(referencedIDs) > 0 {
 				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
 					IssueIDs: referencedIDs,
-					IsPull:   util.OptionalBoolTrue,
+					IsPull:   optional.Some(true),
 				}); err == nil {
 					linkedPrsMap[issue.ID] = linkedPrs
 				}
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index 1454396f04..57671ad8f1 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -13,7 +13,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
@@ -157,7 +157,7 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
 	for _, p := range packages {
 		pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 			PackageID:  p.ID,
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Sort:       packages_model.SortCreatedDesc,
 			Paginator:  db.NewAbsoluteListOptions(pcr.KeepCount, 200),
 		})
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 51b04e0613..3bc1adae99 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -18,7 +18,6 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -131,7 +130,7 @@ func LoadHeaderCount(ctx *context.Context) error {
 	}
 	projectCount, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
 		OwnerID:  ctx.ContextUser.ID,
-		IsClosed: util.OptionalBoolOf(false),
+		IsClosed: optional.Some(false),
 		Type:     projectType,
 	})
 	if err != nil {
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 6f36806ff7..caa7115259 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -30,7 +30,6 @@ import (
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
 	"code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
@@ -215,7 +214,7 @@ func Milestones(ctx *context.Context) {
 	counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{
 		RepoCond: userRepoCond,
 		Name:     keyword,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 	})
 	if err != nil {
 		ctx.ServerError("CountMilestonesByRepoIDs", err)
@@ -228,7 +227,7 @@ func Milestones(ctx *context.Context) {
 			PageSize: setting.UI.IssuePagingNum,
 		},
 		RepoCond: repoCond,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		SortType: sortType,
 		Name:     keyword,
 	})
@@ -440,9 +439,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	isPullList := unitType == unit.TypePullRequests
 	opts := &issues_model.IssuesOptions{
-		IsPull:     util.OptionalBoolOf(isPullList),
+		IsPull:     optional.Some(isPullList),
 		SortType:   sortType,
-		IsArchived: util.OptionalBoolFalse,
+		IsArchived: optional.Some(false),
 		Org:        org,
 		Team:       team,
 		User:       ctx.Doer,
@@ -516,7 +515,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	// Educated guess: Do or don't show closed issues.
 	isShowClosed := ctx.FormString("state") == "closed"
-	opts.IsClosed = util.OptionalBoolOf(isShowClosed)
+	opts.IsClosed = optional.Some(isShowClosed)
 
 	// Make sure page number is at least 1. Will be posted to ctx.Data.
 	page := ctx.FormInt("page")
@@ -800,12 +799,12 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
 		case issues_model.FilterModeReviewed:
 			openClosedOpts.ReviewedID = &doerID
 		}
-		openClosedOpts.IsClosed = util.OptionalBoolFalse
+		openClosedOpts.IsClosed = optional.Some(false)
 		ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
 		if err != nil {
 			return nil, err
 		}
-		openClosedOpts.IsClosed = util.OptionalBoolTrue
+		openClosedOpts.IsClosed = optional.Some(true)
 		ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
 		if err != nil {
 			return nil, err
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 801e1cf95e..09e592d63a 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -233,26 +233,25 @@ func NotificationSubscriptions(ctx *context.Context) {
 	if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) {
 		state = "all"
 	}
+
 	ctx.Data["State"] = state
-	var showClosed util.OptionalBool
+	// default state filter is "all"
+	showClosed := optional.None[bool]()
 	switch state {
-	case "all":
-		showClosed = util.OptionalBoolNone
 	case "closed":
-		showClosed = util.OptionalBoolTrue
+		showClosed = optional.Some(true)
 	case "open":
-		showClosed = util.OptionalBoolFalse
+		showClosed = optional.Some(false)
 	}
 
-	var issueTypeBool util.OptionalBool
 	issueType := ctx.FormString("issueType")
+	// default issue type is no filter
+	issueTypeBool := optional.None[bool]()
 	switch issueType {
 	case "issues":
-		issueTypeBool = util.OptionalBoolFalse
+		issueTypeBool = optional.Some(false)
 	case "pulls":
-		issueTypeBool = util.OptionalBoolTrue
-	default:
-		issueTypeBool = util.OptionalBoolNone
+		issueTypeBool = optional.Some(true)
 	}
 	ctx.Data["IssueType"] = issueType
 
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index d03b28309f..4f3de13dfb 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 	debian_module "code.gitea.io/gitea/modules/packages/debian"
 	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
@@ -54,7 +55,7 @@ func ListPackages(ctx *context.Context) {
 		OwnerID:    ctx.ContextUser.ID,
 		Type:       packages_model.Type(packageType),
 		Name:       packages_model.SearchValue{Value: query},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("SearchLatestVersions", err)
@@ -145,7 +146,7 @@ func RedirectToLastVersion(ctx *context.Context) {
 
 	pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 		PackageID:  p.ID,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("GetPackageByName", err)
@@ -255,7 +256,7 @@ func ViewPackageVersion(ctx *context.Context) {
 		pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 			Paginator:  db.NewAbsoluteListOptions(0, 5),
 			PackageID:  pd.Package.ID,
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 		})
 	}
 	if err != nil {
@@ -359,7 +360,7 @@ func ListPackageVersions(ctx *context.Context) {
 				ExactMatch: false,
 				Value:      query,
 			},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Sort:       sort,
 		})
 		if err != nil {
@@ -467,7 +468,7 @@ func PackageSettingsPost(ctx *context.Context) {
 
 		redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
 		// redirect to the package if there are still versions available
-		if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: util.OptionalBoolFalse}); has {
+		if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: optional.Some(false)}); has {
 			redirectURL = ctx.Package.Descriptor.PackageWebLink()
 		}
 
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index 30611dd9f1..8d6859ab87 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -12,8 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/context"
 )
@@ -112,7 +112,7 @@ func loadSecurityData(ctx *context.Context) {
 	ctx.Data["AccountLinks"] = sources
 
 	authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{
-		IsActive:  util.OptionalBoolNone,
+		IsActive:  optional.None[bool](),
 		LoginType: auth_model.OAuth2,
 	})
 	if err != nil {
diff --git a/services/auth/signin.go b/services/auth/signin.go
index fafe3ef3c6..e116a088e0 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -11,7 +11,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/auth/source/smtp"
 
@@ -87,7 +87,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use
 	}
 
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	})
 	if err != nil {
 		return nil, nil, err
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
index 3ad6e307f1..5c25681548 100644
--- a/services/auth/source/oauth2/init.go
+++ b/services/auth/source/oauth2/init.go
@@ -12,8 +12,8 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/google/uuid"
 	"github.com/gorilla/sessions"
@@ -66,7 +66,7 @@ func ResetOAuth2(ctx context.Context) error {
 // initOAuth2Sources is used to load and register all active OAuth2 providers
 func initOAuth2Sources(ctx context.Context) error {
 	authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive:  util.OptionalBoolTrue,
+		IsActive:  optional.Some(true),
 		LoginType: auth.OAuth2,
 	})
 	if err != nil {
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index f4edb507f2..ac32647839 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -15,8 +15,8 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/markbates/goth"
 )
@@ -107,7 +107,7 @@ func CreateProviderFromSource(source *auth.Source) (Provider, error) {
 }
 
 // GetOAuth2Providers returns the list of configured OAuth2 providers
-func GetOAuth2Providers(ctx context.Context, isActive util.OptionalBool) ([]Provider, error) {
+func GetOAuth2Providers(ctx context.Context, isActive optional.Option[bool]) ([]Provider, error) {
 	authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
 		IsActive:  isActive,
 		LoginType: auth.OAuth2,
diff --git a/services/auth/sspi.go b/services/auth/sspi.go
index 9108a0a668..64a127e97a 100644
--- a/services/auth/sspi.go
+++ b/services/auth/sspi.go
@@ -17,7 +17,6 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/services/auth/source/sspi"
 	gitea_context "code.gitea.io/gitea/services/context"
@@ -132,7 +131,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
 // getConfig retrieves the SSPI configuration from login sources
 func (s *SSPI) getConfig(ctx context.Context) (*sspi.Source, error) {
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive:  util.OptionalBoolTrue,
+		IsActive:  optional.Some(true),
 		LoginType: auth.SSPI,
 	})
 	if err != nil {
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index c8102c6b8b..c9b9248098 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -23,9 +23,9 @@ import (
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	base "code.gitea.io/gitea/modules/migration"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -68,14 +68,14 @@ func TestGiteaUploadRepo(t *testing.T) {
 
 	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	assert.NoError(t, err)
 	assert.Len(t, milestones, 1)
 
 	milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	assert.NoError(t, err)
 	assert.Empty(t, milestones)
@@ -108,7 +108,7 @@ func TestGiteaUploadRepo(t *testing.T) {
 
 	issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
 		RepoIDs:  []int64{repo.ID},
-		IsPull:   util.OptionalBoolFalse,
+		IsPull:   optional.Some(false),
 		SortType: "oldest",
 	})
 	assert.NoError(t, err)
diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go
index 0ff8077bc9..5d5120c6a0 100644
--- a/services/packages/cleanup/cleanup.go
+++ b/services/packages/cleanup/cleanup.go
@@ -12,8 +12,8 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
-	"code.gitea.io/gitea/modules/util"
 	packages_service "code.gitea.io/gitea/services/packages"
 	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 	cargo_service "code.gitea.io/gitea/services/packages/cargo"
@@ -60,7 +60,7 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
 		for _, p := range packages {
 			pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 				PackageID:  p.ID,
-				IsInternal: util.OptionalBoolFalse,
+				IsInternal: optional.Some(false),
 				Sort:       packages_model.SortCreatedDesc,
 				Paginator:  db.NewAbsoluteListOptions(pcr.KeepCount, 200),
 			})
diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go
index dd3f158dbf..3f5f43bbc0 100644
--- a/services/packages/container/cleanup.go
+++ b/services/packages/container/cleanup.go
@@ -9,8 +9,8 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	container_model "code.gitea.io/gitea/models/packages/container"
+	"code.gitea.io/gitea/modules/optional"
 	container_module "code.gitea.io/gitea/modules/packages/container"
-	"code.gitea.io/gitea/modules/util"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	digest "github.com/opencontainers/go-digest"
@@ -59,8 +59,8 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
 			ExactMatch: true,
 			Value:      container_model.UploadVersion,
 		},
-		IsInternal: util.OptionalBoolTrue,
-		HasFiles:   util.OptionalBoolFalse,
+		IsInternal: optional.Some(true),
+		HasFiles:   optional.Some(false),
 	})
 	if err != nil {
 		return err
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 56d5cc04de..64b1ddd869 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -18,10 +18,10 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
-	"code.gitea.io/gitea/modules/util"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
@@ -330,7 +330,7 @@ func CheckCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User)
 	if setting.Packages.LimitTotalOwnerCount > -1 {
 		totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
 			OwnerID:    owner.ID,
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 		})
 		if err != nil {
 			log.Error("CountVersions failed: %v", err)
@@ -640,7 +640,7 @@ func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
 				Page:     1,
 			},
 			OwnerID:    userID,
-			IsInternal: util.OptionalBoolNone,
+			IsInternal: optional.None[bool](),
 		})
 		if err != nil {
 			return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
diff --git a/services/pull/review.go b/services/pull/review.go
index 3ffc276778..90d07c8358 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -18,8 +18,8 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
@@ -56,7 +56,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
 			ListAll: true,
 		},
 		Type:        issues_model.CommentTypeCode,
-		Invalidated: util.OptionalBoolFalse,
+		Invalidated: optional.Some(false),
 		IssueIDs:    issueIDs,
 	})
 	if err != nil {
@@ -327,7 +327,7 @@ func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *is
 		},
 		IssueID:   pull.IssueID,
 		Type:      issues_model.ReviewTypeApprove,
-		Dismissed: util.OptionalBoolFalse,
+		Dismissed: optional.Some(false),
 	})
 	if err != nil {
 		return err
@@ -394,7 +394,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
 		reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
 			IssueID:    review.IssueID,
 			ReviewerID: review.ReviewerID,
-			Dismissed:  util.OptionalBoolFalse,
+			Dismissed:  optional.Some(false),
 		})
 		if err != nil {
 			return nil, err
diff --git a/services/repository/branch.go b/services/repository/branch.go
index ec41173da8..55cedf5d84 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -24,7 +24,6 @@ import (
 	"code.gitea.io/gitea/modules/queue"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 	notify_service "code.gitea.io/gitea/services/notify"
 	files_service "code.gitea.io/gitea/services/repository/files"
@@ -54,7 +53,7 @@ type Branch struct {
 }
 
 // LoadBranches loads branches from the repository limited by page & pageSize.
-func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch util.OptionalBool, keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
+func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
 	defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch)
 	if err != nil {
 		return nil, nil, 0, err
@@ -62,7 +61,7 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          repo.ID,
-		IsDeletedBranch: isDeletedBranch.ToGeneric(),
+		IsDeletedBranch: isDeletedBranch,
 		ListOptions: db.ListOptions{
 			Page:     page,
 			PageSize: pageSize,
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index ac18da3525..35c760dc62 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/queue"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
@@ -225,7 +226,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
 	if source.Repository != nil {
 		repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
 			RepoID:   source.Repository.ID,
-			IsActive: util.OptionalBoolTrue,
+			IsActive: optional.Some(true),
 		})
 		if err != nil {
 			return fmt.Errorf("ListWebhooksByOpts: %w", err)
@@ -239,7 +240,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
 	if owner != nil {
 		ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
 			OwnerID:  owner.ID,
-			IsActive: util.OptionalBoolTrue,
+			IsActive: optional.Some(true),
 		})
 		if err != nil {
 			return fmt.Errorf("ListWebhooksByOpts: %w", err)
@@ -248,7 +249,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
 	}
 
 	// Add any admin-defined system webhooks
-	systemHooks, err := webhook_model.GetSystemWebhooks(ctx, util.OptionalBoolTrue)
+	systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true))
 	if err != nil {
 		return fmt.Errorf("GetSystemWebhooks: %w", err)
 	}

From 6465f94a2d26cdacc232fddc20f98d98df61ddac Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 3 Mar 2024 00:07:54 +0800
Subject: [PATCH 240/679] Fix queue worker incorrectly stopped when there are
 still more items in the queue (#29532)

Without `case <-t.C`, the workers would stop incorrectly, the test won't
pass. For the worse case, there might be only one running worker
processing the queue items for long time because other workers are
stopped. The root cause is related to the logic of doDispatchBatchToWorker.
It isn't a serious problem at the moment, so keep it as-is.
---
 modules/queue/workergroup.go      | 20 ++++++++++++++++----
 modules/queue/workerqueue.go      |  2 ++
 modules/queue/workerqueue_test.go | 29 ++++++++++++++++++++++++-----
 3 files changed, 42 insertions(+), 9 deletions(-)

diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go
index 147a4f335e..e3801ef2b2 100644
--- a/modules/queue/workergroup.go
+++ b/modules/queue/workergroup.go
@@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
 		full = true
 	}
 
+	// TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum"
+	// The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later
+	// So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary.
 	q.workerNumMu.Lock()
 	noWorker := q.workerNum == 0
 	if full || noWorker {
@@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
 		log.Debug("Queue %q starts new worker", q.GetName())
 		defer log.Debug("Queue %q stops idle worker", q.GetName())
 
+		atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging
+
 		t := time.NewTicker(workerIdleDuration)
+		defer t.Stop()
+
 		keepWorking := true
 		stopWorking := func() {
 			q.workerNumMu.Lock()
@@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
 			case batch, ok := <-q.batchChan:
 				if !ok {
 					stopWorking()
-				} else {
-					q.doWorkerHandle(batch)
-					t.Reset(workerIdleDuration)
+					continue
+				}
+				q.doWorkerHandle(batch)
+				// reset the idle ticker, and drain the tick after reset in case a tick is already triggered
+				t.Reset(workerIdleDuration)
+				select {
+				case <-t.C:
+				default:
 				}
 			case <-t.C:
 				q.workerNumMu.Lock()
-				keepWorking = q.workerNum <= 1
+				keepWorking = q.workerNum <= 1 // keep the last worker running
 				if !keepWorking {
 					q.workerNum--
 				}
diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go
index b28fd88027..4160622d81 100644
--- a/modules/queue/workerqueue.go
+++ b/modules/queue/workerqueue.go
@@ -40,6 +40,8 @@ type WorkerPoolQueue[T any] struct {
 	workerMaxNum    int
 	workerActiveNum int
 	workerNumMu     sync.Mutex
+
+	workerStartedCounter int32
 }
 
 type flushType chan struct{}
diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go
index e60120162a..e09669c542 100644
--- a/modules/queue/workerqueue_test.go
+++ b/modules/queue/workerqueue_test.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -175,11 +176,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
 }
 
 func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
-	oldWorkerIdleDuration := workerIdleDuration
-	workerIdleDuration = 300 * time.Millisecond
-	defer func() {
-		workerIdleDuration = oldWorkerIdleDuration
-	}()
+	defer test.MockVariableValue(&workerIdleDuration, 300*time.Millisecond)()
 
 	handler := func(items ...int) (unhandled []int) {
 		time.Sleep(100 * time.Millisecond)
@@ -250,3 +247,25 @@ func TestWorkerPoolQueueShutdown(t *testing.T) {
 	q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false)
 	assert.EqualValues(t, 20, q.GetQueueItemNumber())
 }
+
+func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) {
+	defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)()
+
+	handler := func(items ...int) (unhandled []int) {
+		time.Sleep(50 * time.Millisecond)
+		return nil
+	}
+
+	q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
+	stop := runWorkerPoolQueue(q)
+	for i := 0; i < 20; i++ {
+		assert.NoError(t, q.Push(i))
+	}
+
+	time.Sleep(500 * time.Millisecond)
+	assert.EqualValues(t, 2, q.GetWorkerNumber())
+	assert.EqualValues(t, 2, q.GetWorkerActiveNumber())
+	// when the queue never becomes empty, the existing workers should keep working
+	assert.EqualValues(t, 2, q.workerStartedCounter)
+	stop()
+}

From 70c126e6184872a6ac63cae2f327fc745b25d1d7 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sat, 2 Mar 2024 18:02:01 +0100
Subject: [PATCH 241/679] Add support for API blob upload of release
 attachments (#29507)

Fixes #29502

Our endpoint is not Github compatible.


https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 routers/api/v1/repo/release_attachment.go | 41 ++++++++++----
 services/attachment/attachment.go         |  6 +-
 templates/swagger/v1_json.tmpl            |  6 +-
 tests/integration/api_releases_test.go    | 68 +++++++++++++++++------
 4 files changed, 88 insertions(+), 33 deletions(-)

diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index a29bce66a4..59fd83e3a2 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -4,7 +4,9 @@
 package repo
 
 import (
+	"io"
 	"net/http"
+	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/log"
@@ -154,6 +156,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	// - application/json
 	// consumes:
 	// - multipart/form-data
+	// - application/octet-stream
 	// parameters:
 	// - name: owner
 	//   in: path
@@ -180,7 +183,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	//   in: formData
 	//   description: attachment to upload
 	//   type: file
-	//   required: true
+	//   required: false
 	// responses:
 	//   "201":
 	//     "$ref": "#/responses/Attachment"
@@ -202,20 +205,36 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	}
 
 	// Get uploaded file from request
-	file, header, err := ctx.Req.FormFile("attachment")
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetFile", err)
-		return
-	}
-	defer file.Close()
+	var content io.ReadCloser
+	var filename string
+	var size int64 = -1
 
-	filename := header.Filename
-	if query := ctx.FormString("name"); query != "" {
-		filename = query
+	if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
+		file, header, err := ctx.Req.FormFile("attachment")
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "GetFile", err)
+			return
+		}
+		defer file.Close()
+
+		content = file
+		size = header.Size
+		filename = header.Filename
+		if name := ctx.FormString("name"); name != "" {
+			filename = name
+		}
+	} else {
+		content = ctx.Req.Body
+		filename = ctx.FormString("name")
+	}
+
+	if filename == "" {
+		ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.")
+		return
 	}
 
 	// Create a new attachment and save the file
-	attach, err := attachment.UploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{
+	attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
 		Name:       filename,
 		UploaderID: ctx.Doer.ID,
 		RepoID:     ctx.Repo.Repository.ID,
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index eab3d0b142..0fd51e4fa5 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -39,14 +39,14 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
 }
 
 // UploadAttachment upload new attachment into storage and update database
-func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, opts *repo_model.Attachment) (*repo_model.Attachment, error) {
+func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
 	buf := make([]byte, 1024)
 	n, _ := util.ReadAtMost(file, buf)
 	buf = buf[:n]
 
-	if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil {
+	if err := upload.Verify(buf, attach.Name, allowedTypes); err != nil {
 		return nil, err
 	}
 
-	return NewAttachment(ctx, opts, io.MultiReader(bytes.NewReader(buf), file), fileSize)
+	return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize)
 }
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index d4c5d9a7ee..b739bea60d 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -12343,7 +12343,8 @@
       },
       "post": {
         "consumes": [
-          "multipart/form-data"
+          "multipart/form-data",
+          "application/octet-stream"
         ],
         "produces": [
           "application/json"
@@ -12386,8 +12387,7 @@
             "type": "file",
             "description": "attachment to upload",
             "name": "attachment",
-            "in": "formData",
-            "required": true
+            "in": "formData"
           }
         ],
         "responses": {
diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go
index 5b1ab76ce9..49aa4c4e1b 100644
--- a/tests/integration/api_releases_test.go
+++ b/tests/integration/api_releases_test.go
@@ -262,24 +262,60 @@ func TestAPIUploadAssetRelease(t *testing.T) {
 
 	filename := "image.png"
 	buff := generateImg()
-	body := &bytes.Buffer{}
 
-	writer := multipart.NewWriter(body)
-	part, err := writer.CreateFormFile("attachment", filename)
-	assert.NoError(t, err)
-	_, err = io.Copy(part, &buff)
-	assert.NoError(t, err)
-	err = writer.Close()
-	assert.NoError(t, err)
+	assetURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner.Name, repo.Name, r.ID)
 
-	req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset", owner.Name, repo.Name, r.ID), body).
-		AddTokenAuth(token)
-	req.Header.Add("Content-Type", writer.FormDataContentType())
-	resp := MakeRequest(t, req, http.StatusCreated)
+	t.Run("multipart/form-data", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
 
-	var attachment *api.Attachment
-	DecodeJSON(t, resp, &attachment)
+		body := &bytes.Buffer{}
 
-	assert.EqualValues(t, "test-asset", attachment.Name)
-	assert.EqualValues(t, 104, attachment.Size)
+		writer := multipart.NewWriter(body)
+		part, err := writer.CreateFormFile("attachment", filename)
+		assert.NoError(t, err)
+		_, err = io.Copy(part, bytes.NewReader(buff.Bytes()))
+		assert.NoError(t, err)
+		err = writer.Close()
+		assert.NoError(t, err)
+
+		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())).
+			AddTokenAuth(token).
+			SetHeader("Content-Type", writer.FormDataContentType())
+		resp := MakeRequest(t, req, http.StatusCreated)
+
+		var attachment *api.Attachment
+		DecodeJSON(t, resp, &attachment)
+
+		assert.EqualValues(t, filename, attachment.Name)
+		assert.EqualValues(t, 104, attachment.Size)
+
+		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=test-asset", bytes.NewReader(body.Bytes())).
+			AddTokenAuth(token).
+			SetHeader("Content-Type", writer.FormDataContentType())
+		resp = MakeRequest(t, req, http.StatusCreated)
+
+		var attachment2 *api.Attachment
+		DecodeJSON(t, resp, &attachment2)
+
+		assert.EqualValues(t, "test-asset", attachment2.Name)
+		assert.EqualValues(t, 104, attachment2.Size)
+	})
+
+	t.Run("application/octet-stream", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(buff.Bytes())).
+			AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusBadRequest)
+
+		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=stream.bin", bytes.NewReader(buff.Bytes())).
+			AddTokenAuth(token)
+		resp := MakeRequest(t, req, http.StatusCreated)
+
+		var attachment *api.Attachment
+		DecodeJSON(t, resp, &attachment)
+
+		assert.EqualValues(t, "stream.bin", attachment.Name)
+		assert.EqualValues(t, 104, attachment.Size)
+	})
 }

From bf6502a8f7a2e9a2b64b43b7733316d863c9a768 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 3 Mar 2024 01:38:38 +0800
Subject: [PATCH 242/679] Fix incorrect relative/absolute URL usages (#29531)

Add two "HTMLURL" methods for PackageDescriptor.
And rename "FullWebLink" to "VersionWebLink"
---
 models/packages/descriptor.go   | 16 +++++++++++++---
 routers/api/packages/npm/api.go |  3 ++-
 routers/web/user/package.go     |  2 +-
 services/convert/package.go     |  2 +-
 4 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index f849ab5c04..b8ef698d38 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -70,16 +70,26 @@ type PackageFileDescriptor struct {
 	Properties PackagePropertyList
 }
 
-// PackageWebLink returns the package web link
+// PackageWebLink returns the relative package web link
 func (pd *PackageDescriptor) PackageWebLink() string {
 	return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
 }
 
-// FullWebLink returns the package version web link
-func (pd *PackageDescriptor) FullWebLink() string {
+// VersionWebLink returns the relative package version web link
+func (pd *PackageDescriptor) VersionWebLink() string {
 	return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion))
 }
 
+// PackageHTMLURL returns the absolute package HTML URL
+func (pd *PackageDescriptor) PackageHTMLURL() string {
+	return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
+}
+
+// VersionHTMLURL returns the absolute package version HTML URL
+func (pd *PackageDescriptor) VersionHTMLURL() string {
+	return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion))
+}
+
 // CalculateBlobSize returns the total blobs size in bytes
 func (pd *PackageDescriptor) CalculateBlobSize() int64 {
 	size := int64(0)
diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go
index 8470874884..f8e839c424 100644
--- a/routers/api/packages/npm/api.go
+++ b/routers/api/packages/npm/api.go
@@ -12,6 +12,7 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	npm_module "code.gitea.io/gitea/modules/packages/npm"
+	"code.gitea.io/gitea/modules/setting"
 )
 
 func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
@@ -98,7 +99,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
 				Maintainers: []npm_module.User{}, // npm cli needs this field
 				Keywords:    metadata.Keywords,
 				Links: &npm_module.PackageSearchPackageLinks{
-					Registry: pd.FullWebLink(),
+					Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
 					Homepage: metadata.ProjectURL,
 				},
 			},
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 4f3de13dfb..3ecc59a2ab 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -163,7 +163,7 @@ func RedirectToLastVersion(ctx *context.Context) {
 		return
 	}
 
-	ctx.Redirect(pd.FullWebLink())
+	ctx.Redirect(pd.VersionWebLink())
 }
 
 // ViewPackageVersion displays a single package version
diff --git a/services/convert/package.go b/services/convert/package.go
index e90ce8a00f..b5fca21a3c 100644
--- a/services/convert/package.go
+++ b/services/convert/package.go
@@ -35,7 +35,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m
 		Name:       pd.Package.Name,
 		Version:    pd.Version.Version,
 		CreatedAt:  pd.Version.CreatedUnix.AsTime(),
-		HTMLURL:    pd.FullWebLink(),
+		HTMLURL:    pd.VersionHTMLURL(),
 	}, nil
 }
 

From 937e8b55149388840bbf6c4d7216495bc3dd2fe9 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 2 Mar 2024 21:31:59 +0200
Subject: [PATCH 243/679] Fix elipsis button not working if the last commit
 loading is deferred (#29544)

Before this change, if we had more than 200 entries being deferred in
loading, the entire table would get replaced thus losing any event
listeners attached to the elements within the table, such as the elipsis
button and commit list with tippy.

With this change we remove the previous javascript code that replaced
the table and use htmx to replace the table.

htmx attributes added:
- `hx-indicator="tr.notready td.message span"`: attach the loading
spinner to the files whose last commit is still being loaded
- `hx-trigger="load"` trigger the request-replace behavior as soon as
possible
- `hx-swap="morph"`: use the idiomorph morphing algorithm, this is the
thing that makes it so the elipsis button event listener is kept during
the replacement, fixing the bug because we don't actually replace the
table, only modifying it
- `hx-post="{{.LastCommitLoaderURL}}"`: make a post request to this url
to get the table with all of the commit information

As part of this change I removed the handling of partial replacement in
the case we have less than 200 "not ready" files. The first reason is
that I couldn't make htmx replace only a subset of returned elements,
the second reason is that we have a cache implemented in the backend
already so the only cost added is that we query the cache a few times
(which is sure to be populated due to the initial request), and the last
reason is that since the last refactor of this functionality that
removed jQuery we don't properly send the "not ready" entries as the
backend expects `FormData` with `f[]` and we send a JSON with `f` so we
always query for all rows anyway.

# Before

![before](https://github.com/go-gitea/gitea/assets/20454870/482ebfec-66c5-40cc-9c1e-e3b3bfe1bbc1)

# After

![after](https://github.com/go-gitea/gitea/assets/20454870/454c517e-3a4e-4006-a49f-99cc56e0fd60)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/web/repo/view.go           | 24 ++++++-----------
 templates/repo/view_list.tmpl      |  6 ++---
 web_src/js/features/repo-commit.js | 43 ------------------------------
 web_src/js/index.js                |  7 +----
 4 files changed, 12 insertions(+), 68 deletions(-)

diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index e89739e2fb..4df10fbea1 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -35,7 +35,6 @@ import (
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/lfs"
@@ -859,25 +858,18 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
 		defer cancel()
 	}
 
-	selected := make(container.Set[string])
-	selected.AddMultiple(ctx.FormStrings("f[]")...)
-
-	entries := allEntries
-	if len(selected) > 0 {
-		entries = make(git.Entries, 0, len(selected))
-		for _, entry := range allEntries {
-			if selected.Contains(entry.Name()) {
-				entries = append(entries, entry)
-			}
-		}
-	}
-
-	var latestCommit *git.Commit
-	ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
+	files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
 	if err != nil {
 		ctx.ServerError("GetCommitsInfo", err)
 		return nil
 	}
+	ctx.Data["Files"] = files
+	for _, f := range files {
+		if f.Commit == nil {
+			ctx.Data["HasFilesWithoutLatestCommit"] = true
+			break
+		}
+	}
 
 	if !loadLatestCommitData(ctx, latestCommit) {
 		return nil
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index c1ef4ff4cb..988a5ddd50 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -1,7 +1,7 @@
-<table id="repo-files-table" class="ui single line table gt-mt-0" data-last-commit-loader-url="{{.LastCommitLoaderURL}}">
+<table id="repo-files-table" class="ui single line table gt-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
 	<thead>
 		<tr class="commit-list">
-			<th colspan="2" {{if not .LatestCommit}}class="notready"{{end}}>
+			<th colspan="2">
 				{{template "repo/latest_commit" .}}
 			</th>
 			<th class="text grey right age">{{if .LatestCommit}}{{if .LatestCommit.Committer}}{{TimeSince .LatestCommit.Committer.When ctx.Locale}}{{end}}{{end}}</th>
@@ -55,7 +55,7 @@
 							{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
 							{{RenderCommitMessageLinkSubject $.Context $commit.Message $commitLink ($.Repository.ComposeMetas ctx)}}
 						{{else}}
-							<div class="ui active tiny slow centered inline">…</div>
+							<div class="ui active tiny slow centered inline"></div>
 						{{end}}
 					</span>
 				</td>
diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js
index 7e2f6fa58e..f61ea08a42 100644
--- a/web_src/js/features/repo-commit.js
+++ b/web_src/js/features/repo-commit.js
@@ -1,7 +1,5 @@
 import {createTippy} from '../modules/tippy.js';
 import {toggleElem} from '../utils/dom.js';
-import {parseDom} from '../utils.js';
-import {POST} from '../modules/fetch.js';
 
 export function initRepoEllipsisButton() {
   for (const button of document.querySelectorAll('.js-toggle-commit-body')) {
@@ -14,47 +12,6 @@ export function initRepoEllipsisButton() {
   }
 }
 
-export async function initRepoCommitLastCommitLoader() {
-  const entryMap = {};
-
-  const entries = Array.from(document.querySelectorAll('table#repo-files-table tr.notready'), (el) => {
-    const entryName = el.getAttribute('data-entryname');
-    entryMap[entryName] = el;
-    return entryName;
-  });
-
-  if (entries.length === 0) {
-    return;
-  }
-
-  const lastCommitLoaderURL = document.querySelector('table#repo-files-table').getAttribute('data-last-commit-loader-url');
-
-  if (entries.length > 200) {
-    // For more than 200 entries, replace the entire table
-    const response = await POST(lastCommitLoaderURL);
-    const data = await response.text();
-    document.querySelector('table#repo-files-table').outerHTML = data;
-    return;
-  }
-
-  // For fewer entries, update individual rows
-  const response = await POST(lastCommitLoaderURL, {data: {'f': entries}});
-  const data = await response.text();
-  const doc = parseDom(data, 'text/html');
-  for (const row of doc.querySelectorAll('tr')) {
-    if (row.className === 'commit-list') {
-      document.querySelector('table#repo-files-table .commit-list')?.replaceWith(row);
-      continue;
-    }
-    // there are other <tr> rows in response (eg: <tr class="has-parent">)
-    // at the moment only the "data-entryname" rows should be processed
-    const entryName = row.getAttribute('data-entryname');
-    if (entryName) {
-      entryMap[entryName]?.replaceWith(row);
-    }
-  }
-}
-
 export function initCommitStatuses() {
   for (const element of document.querySelectorAll('[data-tippy="commit-statuses"]')) {
     const top = document.querySelector('.repository.file.list') || document.querySelector('.repository.diff');
diff --git a/web_src/js/index.js b/web_src/js/index.js
index b7f3ba99a0..c7eac9d242 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -33,11 +33,7 @@ import {
   initRepoPullRequestAllowMaintainerEdit,
   initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
 } from './features/repo-issue.js';
-import {
-  initRepoEllipsisButton,
-  initRepoCommitLastCommitLoader,
-  initCommitStatuses,
-} from './features/repo-commit.js';
+import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js';
 import {
   initFootLanguageMenu,
   initGlobalButtonClickOnEnter,
@@ -148,7 +144,6 @@ onDomReady(() => {
   initRepoCommentForm();
   initRepoEllipsisButton();
   initRepoDiffCommitBranchesAndTags();
-  initRepoCommitLastCommitLoader();
   initRepoEditor();
   initRepoGraphGit();
   initRepoIssueContentHistory();

From e3e6569c5fd8c69aa65384e6d1636cc14b23a32b Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Sat, 2 Mar 2024 22:55:02 +0100
Subject: [PATCH 244/679] Add option to set language in admin user view
 (#28449)

![image](https://github.com/go-gitea/gitea/assets/24977596/be7e3f92-af3f-4628-b4ed-abf6439687f3)
`/admin/users/<UserID>/edit`


![image](https://github.com/go-gitea/gitea/assets/24977596/906af0dd-cceb-4ed9-9cd9-32c71ae1bf71)
`/admin/users/<UserID>`

---
*Sponsored by Kithara Software GmbH*
---
 routers/web/admin/users.go             |  1 +
 services/forms/admin.go                |  1 +
 templates/admin/user/edit.tmpl         | 15 +++++++++++++++
 templates/admin/user/view_details.tmpl |  8 ++++++++
 4 files changed, 25 insertions(+)

diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index ca47175401..a34e0d0f0d 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -439,6 +439,7 @@ func EditUserPost(ctx *context.Context) {
 		AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
 		IsRestricted:            optional.Some(form.Restricted),
 		Visibility:              optional.Some(form.Visibility),
+		Language:                optional.Some(form.Language),
 	}
 
 	if err := user_service.UpdateUser(ctx, u, opts); err != nil {
diff --git a/services/forms/admin.go b/services/forms/admin.go
index f112013060..81276f8f46 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -41,6 +41,7 @@ type AdminEditUserForm struct {
 	Password                string `binding:"MaxSize(255)"`
 	Website                 string `binding:"ValidUrl;MaxSize(255)"`
 	Location                string `binding:"MaxSize(50)"`
+	Language                string `binding:"MaxSize(5)"`
 	MaxRepoCreation         int
 	Active                  bool
 	Admin                   bool
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index fcb8ce0827..159c821099 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -70,6 +70,21 @@
 					<input id="password" name="password" type="password" autocomplete="new-password">
 					<p class="help">{{ctx.Locale.Tr "admin.users.password_helper"}}</p>
 				</div>
+
+				<div class="field {{if .Err_Language}}error{{end}}">
+					<label for="language">{{ctx.Locale.Tr "settings.language"}}</label>
+					<div class="ui selection dropdown">
+						<input name="language" type="hidden" value="{{.User.Language}}">
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="text">{{range .AllLangs}}{{if eq $.User.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
+						<div class="menu">
+						{{range .AllLangs}}
+							<div class="item{{if eq $.User.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
+						{{end}}
+						</div>
+					</div>
+				</div>
+
 				<div class="field {{if .Err_Website}}error{{end}}">
 					<label for="website">{{ctx.Locale.Tr "settings.website"}}</label>
 					<input id="website" name="website" type="url" value="{{.User.Website}}" placeholder="http://mydomain.com or https://mydomain.com" maxlength="255">
diff --git a/templates/admin/user/view_details.tmpl b/templates/admin/user/view_details.tmpl
index 21425eecb4..be2f32b5ec 100644
--- a/templates/admin/user/view_details.tmpl
+++ b/templates/admin/user/view_details.tmpl
@@ -48,6 +48,14 @@
 					{{svg "octicon-x"}}
 				{{end}}
 			</div>
+			{{if .User.Language}}
+				<div class="flex-item-body">
+					<span class="flex-text-inline">
+						<b>{{ctx.Locale.Tr "settings.language"}}:</b>
+						{{range .AllLangs}}{{if eq $.User.Language .Lang}}{{.Name}}{{end}}{{end}}
+					</span>
+				</div>
+			{{end}}
 			{{if .User.Location}}
 				<div class="flex-item-body">
 					<span class="flex-text-inline">{{svg "octicon-location"}}{{.User.Location}}</span>

From cc896258b9ea9f60c33077c937ce9b3951b58b35 Mon Sep 17 00:00:00 2001
From: Martin <spleefer90@gmail.com>
Date: Sun, 3 Mar 2024 00:44:43 +0100
Subject: [PATCH 245/679] gitea.service: Remove syslog.target (#29550)

Remove syslog.target from service file, this target hasn't existed for
over a decade.


https://github.com/systemd/systemd/blob/6aa8d43ade72e24c9426e604f7fc4b7582b9db7c/NEWS#L72-L73
---
 contrib/systemd/gitea.service | 1 -
 1 file changed, 1 deletion(-)

diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service
index d205c6ee8b..c091722a74 100644
--- a/contrib/systemd/gitea.service
+++ b/contrib/systemd/gitea.service
@@ -1,6 +1,5 @@
 [Unit]
 Description=Gitea (Git with a cup of tea)
-After=syslog.target
 After=network.target
 ###
 # Don't forget to add the database service dependencies

From 44398e405ffe297997c6b9c8dbb97f966926b65a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 3 Mar 2024 08:14:12 +0800
Subject: [PATCH 246/679] Fix incorrect cookie path for AppSubURL (#29534)

Regression of #24107
---
 modules/setting/session.go   | 7 +++++--
 routers/common/middleware.go | 1 +
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/modules/setting/session.go b/modules/setting/session.go
index 8b9b754b38..70497e5eaa 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -20,7 +20,7 @@ var SessionConfig = struct {
 	ProviderConfig string
 	// Cookie name to save session ID. Default is "MacaronSession".
 	CookieName string
-	// Cookie path to store. Default is "/". HINT: there was a bug, the old value doesn't have trailing slash, and could be empty "".
+	// Cookie path to store. Default is "/".
 	CookiePath string
 	// GC interval time in seconds. Default is 3600.
 	Gclifetime int64
@@ -49,7 +49,10 @@ func loadSessionFrom(rootCfg ConfigProvider) {
 		fatalDuplicatedPath("session", SessionConfig.ProviderConfig)
 	}
 	SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
-	SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
+	SessionConfig.CookiePath = AppSubURL
+	if SessionConfig.CookiePath == "" {
+		SessionConfig.CookiePath = "/"
+	}
 	SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
 	SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
 	SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
index 1ee4c629ad..c7c75fb099 100644
--- a/routers/common/middleware.go
+++ b/routers/common/middleware.go
@@ -38,6 +38,7 @@ func ProtocolMiddlewares() (handlers []any) {
 		})
 	})
 
+	// wrap the request and response, use the process context and add it to the process manager
 	handlers = append(handlers, func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
 			ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)

From 22b4f0c09f1de5e581929bd10f39833d30d2c482 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Sun, 3 Mar 2024 00:25:22 +0000
Subject: [PATCH 247/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_ja-JP.ini | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 5d9e21703e..4d1ecef61c 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -123,6 +123,7 @@ pin=ピン留め
 unpin=ピン留め解除
 
 artifacts=成果物
+confirm_delete_artifact=アーティファクト %s を削除してよろしいですか?
 
 archived=アーカイブ
 
@@ -423,6 +424,7 @@ authorization_failed_desc=無効なリクエストを検出したため認可が
 sspi_auth_failed=SSPI認証に失敗しました
 password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した<a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">盗まれたパスワードのリスト</a>に含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。
 password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした
+last_admin=最後の管理者は削除できません。少なくとも一人の管理者が必要です。
 
 [mail]
 view_it_on=%s で見る
@@ -588,6 +590,7 @@ org_still_own_packages=組織はまだ1つ以上のパッケージを所有し
 
 target_branch_not_exist=ターゲットのブランチが存在していません。
 
+admin_cannot_delete_self=あなたが管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
 
 [user]
 change_avatar=アバターを変更…
@@ -968,6 +971,8 @@ issue_labels_helper=イシューのラベルセットを選択
 license=ライセンス
 license_helper=ライセンス ファイルを選択してください。
 license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。
+object_format=オブジェクトのフォーマット
+object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。
 readme=README
 readme_helper=READMEファイル テンプレートを選択してください。
 readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。
@@ -1035,6 +1040,7 @@ desc.public=公開
 desc.template=テンプレート
 desc.internal=組織内
 desc.archived=アーカイブ
+desc.sha256=SHA256
 
 template.items=テンプレート項目
 template.git_content=Gitコンテンツ (デフォルトブランチ)

From e71b69257c38178eed9ccd0b62a5ae47d67858d4 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 3 Mar 2024 12:57:22 +0800
Subject: [PATCH 248/679] Breaking summary for template refactoring (#29395)

https://github.com/go-gitea/gitea/pull/29395
---
 .../administration/mail-templates.en-us.md    | 16 +++++------
 .../administration/mail-templates.zh-cn.md    | 16 +++++------
 modules/templates/mailer.go                   | 28 +++++++++----------
 templates/mail/issue/default.tmpl             |  2 +-
 4 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index 9077f97aea..0154fe55d0 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -259,14 +259,14 @@ This template produces something along these lines:
 The template system contains several functions that can be used to further process and format
 the messages. Here's a list of some of them:
 
-| Name             | Parameters  | Available | Usage                                                                       |
-| ---------------- | ----------- | --------- |-----------------------------------------------------------------------------|
-| `AppUrl`         | -           | Any       | Gitea's URL                                                                 |
-| `AppName`        | -           | Any       | Set from `app.ini`, usually "Gitea"                                         |
-| `AppDomain`      | -           | Any       | Gitea's host name                                                           |
-| `EllipsisString` | string, int | Any       | Truncates a string to the specified length; adds ellipsis as needed         |
-| `SanitizeHTML`   | string      | Body only | Sanitizes text by removing any dangerous HTML tags from it.                 |
-| `SafeHTML`       | string      | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. |
+| Name             | Parameters  | Available | Usage                                                               |
+| ---------------- | ----------- | --------- | ------------------------------------------------------------------- |
+| `AppUrl`         | -           | Any       | Gitea's URL                                                         |
+| `AppName`        | -           | Any       | Set from `app.ini`, usually "Gitea"                                 |
+| `AppDomain`      | -           | Any       | Gitea's host name                                                   |
+| `EllipsisString` | string, int | Any       | Truncates a string to the specified length; adds ellipsis as needed |
+| `SanitizeHTML`   | string      | Body only | Sanitizes text by removing any dangerous HTML tags from it          |
+| `SafeHTML`       | string      | Body only | Takes the input as HTML, can be used for outputing raw HTML content |
 
 These are _functions_, not metadata, so they have to be used:
 
diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md
index d58f9dc176..e8c2817336 100644
--- a/docs/content/administration/mail-templates.zh-cn.md
+++ b/docs/content/administration/mail-templates.zh-cn.md
@@ -242,14 +242,14 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 
 模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
 
-| 函数名              | 参数        | 可用于       | 用法                                                      |
-|------------------| ----------- | ------------ |---------------------------------------------------------|
-| `AppUrl`         | -           | 任何地方     | Gitea 的 URL                                             |
-| `AppName`        | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"                             |
-| `AppDomain`      | -           | 任何地方     | Gitea 的主机名                                              |
-| `EllipsisString` | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号                                   |
-| `SanitizeHTML`   | string      | 仅正文部分   | 通过删除其中的危险 HTML 标签对文本进行清理                                |
-| `SafeHTML`       | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段 |
+| 函数名              | 参数        | 可用于       | 用法                             |
+|------------------| ----------- | ------------ | ------------------------------ |
+| `AppUrl`         | -           | 任何地方     | Gitea 的 URL                    |
+| `AppName`        | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"    |
+| `AppDomain`      | -           | 任何地方     | Gitea 的主机名                     |
+| `EllipsisString` | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号          |
+| `SanitizeHTML`   | string      | 仅正文部分   | 通过删除其中的危险 HTML 标签对文本进行清理       |
+| `SafeHTML`       | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 |
 
 这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
 
diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go
index 04032e3982..f1832cba0e 100644
--- a/modules/templates/mailer.go
+++ b/modules/templates/mailer.go
@@ -5,6 +5,7 @@ package templates
 
 import (
 	"context"
+	"fmt"
 	"html/template"
 	"regexp"
 	"strings"
@@ -33,7 +34,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
 	}
 }
 
-func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
+func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
 	// Split template into subject and body
 	var subjectContent []byte
 	bodyContent := content
@@ -42,20 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
 		subjectContent = content[0:loc[0]]
 		bodyContent = content[loc[1]:]
 	}
-	if _, err := stpl.New(name).
-		Parse(string(subjectContent)); err != nil {
-		log.Error("Failed to parse template [%s/subject]: %v", name, err)
-		if !setting.IsProd {
-			log.Fatal("Please fix the mail template error")
-		}
+	if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
+		return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
 	}
-	if _, err := btpl.New(name).
-		Parse(string(bodyContent)); err != nil {
-		log.Error("Failed to parse template [%s/body]: %v", name, err)
-		if !setting.IsProd {
-			log.Fatal("Please fix the mail template error")
-		}
+	if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
+		return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
 	}
+	return nil
 }
 
 // Mailer provides the templates required for sending notification mails.
@@ -87,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
 			if firstRun {
 				log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
 			}
-			buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content)
+			if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
+				if firstRun {
+					log.Fatal("Failed to parse mail template, err: %v", err)
+				} else {
+					log.Error("Failed to parse mail template, err: %v", err)
+				}
+			}
 		}
 	}
 
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index 10fa0f1ffc..021ca3989d 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -65,7 +65,7 @@
 			{{$.locale.Tr "mail.issue.in_tree_path" .TreePath}}
 			<div class="review">
 				<pre>{{.Patch}}</pre>
-				<div>{{.RenderedContent | SafeHTML}}</div>
+				<div>{{.RenderedContent}}</div>
 			</div>
 		{{end -}}
 		{{if eq .ActionName "push"}}

From e3524c63d6d42865ea8288af89b372544d35474b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Sun, 3 Mar 2024 11:18:34 +0100
Subject: [PATCH 249/679] Filter Repositories by type (#29231)

Filter Repositories by type (resolves #1170, #1318)

before:

![image](https://github.com/go-gitea/gitea/assets/72873130/74e6be62-9010-4ab4-8f9b-bd8afbebb8fb)


after:

![image](https://github.com/go-gitea/gitea/assets/72873130/e4d85ed6-7864-4150-8d72-5194dac1293f)
---
 options/locale/locale_en-US.ini               | 13 ++++
 routers/web/explore/repo.go                   | 20 ++++++
 routers/web/org/home.go                       | 20 ++++++
 routers/web/user/notification.go              | 20 ++++++
 routers/web/user/profile.go                   | 30 +++++++++
 templates/admin/repo/list.tmpl                |  2 +-
 templates/admin/repo/search.tmpl              | 29 --------
 templates/explore/repo_search.tmpl            | 42 ------------
 templates/explore/repos.tmpl                  |  2 +-
 templates/org/home.tmpl                       |  2 +-
 templates/shared/repo_search.tmpl             | 67 +++++++++++++++++++
 .../notification_subscriptions.tmpl           |  2 +-
 templates/user/profile.tmpl                   |  4 +-
 web_src/js/features/repo-search.js            | 22 ++++++
 web_src/js/index.js                           |  2 +
 15 files changed, 200 insertions(+), 77 deletions(-)
 delete mode 100644 templates/admin/repo/search.tmpl
 delete mode 100644 templates/explore/repo_search.tmpl
 create mode 100644 templates/shared/repo_search.tmpl
 create mode 100644 web_src/js/features/repo-search.js

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index beda02603e..8c4dae753b 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -142,6 +142,19 @@ confirm_delete_selected = Confirm to delete all selected items?
 name = Name
 value = Value
 
+filter = Filter
+filter.clear = Clear Filter
+filter.is_archived = Archived
+filter.not_archived = Not Archived
+filter.is_fork = Forked
+filter.not_fork = Not Forked
+filter.is_mirror = Mirrored
+filter.not_mirror = Not Mirrored
+filter.is_template = Template
+filter.not_template = Not Template
+filter.public = Public
+filter.private = Private
+
 [aria]
 navbar = Navigation Bar
 footer = Footer
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index d5a46f6883..cf7381512b 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -109,6 +109,21 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 	language := ctx.FormTrim("language")
 	ctx.Data["Language"] = language
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
 			Page:     page,
@@ -125,6 +140,11 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 		Language:           language,
 		IncludeDescription: setting.UI.SearchRepoDescription,
 		OnlyShowRelevant:   opts.OnlyShowRelevant,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 4a7378689a..71d10f3a43 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -85,6 +85,21 @@ func Home(ctx *context.Context) {
 		page = 1
 	}
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	var (
 		repos []*repo_model.Repository
 		count int64
@@ -102,6 +117,11 @@ func Home(ctx *context.Context) {
 		Actor:              ctx.Doer,
 		Language:           language,
 		IncludeDescription: setting.UI.SearchRepoDescription,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 09e592d63a..324205ed91 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -389,6 +389,21 @@ func NotificationWatching(ctx *context.Context) {
 		orderBy = db.SearchOrderByRecentUpdated
 	}
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
 			PageSize: setting.UI.User.RepoPagingNum,
@@ -402,6 +417,11 @@ func NotificationWatching(ctx *context.Context) {
 		Collaborate:        optional.Some(false),
 		TopicOnly:          ctx.FormBool("topic"),
 		IncludeDescription: setting.UI.SearchRepoDescription,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index b9b069b0d4..833312c501 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -162,6 +162,21 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 	}
 	ctx.Data["NumFollowing"] = numFollowing
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	switch tab {
 	case "followers":
 		ctx.Data["Cards"] = followers
@@ -208,6 +223,11 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
@@ -230,6 +250,11 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
@@ -275,6 +300,11 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index e11247aed4..e977c8307c 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -7,7 +7,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			{{template "admin/repo/search" .}}
+			{{template "shared/repo_search" .}}
 		</div>
 		<div class="ui attached table segment">
 			<table class="ui very basic striped table unstackable">
diff --git a/templates/admin/repo/search.tmpl b/templates/admin/repo/search.tmpl
deleted file mode 100644
index 247ec5491a..0000000000
--- a/templates/admin/repo/search.tmpl
+++ /dev/null
@@ -1,29 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-			<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?sort=moststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
-			<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?sort=feweststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
-			<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?sort=mostforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
-			<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?sort=fewestforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
-			<a class="{{if eq .SortType "size"}}active {{end}}item" href="{{$.Link}}?sort=size&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</a>
-			<a class="{{if eq .SortType "reversesize"}}active {{end}}item" href="{{$.Link}}?sort=reversesize&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</a>
-		</div>
-	</div>
-</div>
diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl
deleted file mode 100644
index e268670e93..0000000000
--- a/templates/explore/repo_search.tmpl
+++ /dev/null
@@ -1,42 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<input type="hidden" name="sort" value="{{$.SortType}}">
-		<input type="hidden" name="language" value="{{$.Language}}">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			{{if .PageIsExploreRepositories}}
-				<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">
-			{{else if .TabName}}
-				<input type="hidden" name="tab" value="{{.TabName}}">
-			{{end}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=newest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=oldest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=alphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=reversealphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=recentupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=leastupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-			{{if not .DisableStars}}
-				<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=moststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
-				<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=feweststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
-			{{end}}
-			<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
-			<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
-		</div>
-	</div>
-</div>
-{{if and .PageIsExploreRepositories .OnlyShowRelevant}}
-	<div class="ui message explore-relevancy-note">
-		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}</span>
-	</div>
-{{end}}
-<div class="divider"></div>
diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl
index dfede2ffcc..53742bf0d9 100644
--- a/templates/explore/repos.tmpl
+++ b/templates/explore/repos.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content explore repositories">
 	{{template "explore/navbar" .}}
 	<div class="ui container">
-		{{template "explore/repo_search" .}}
+		{{template "shared/repo_search" .}}
 		{{template "explore/repo_list" .}}
 		{{template "base/paginate" .}}
 	</div>
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index 892ba0da5b..ddd05b4738 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -8,7 +8,7 @@
 				{{if .ProfileReadme}}
 					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{end}}
-				{{template "explore/repo_search" .}}
+				{{template "shared/repo_search" .}}
 				{{template "explore/repo_list" .}}
 				{{template "base/paginate" .}}
 			</div>
diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo_search.tmpl
new file mode 100644
index 0000000000..2ea4bfaad7
--- /dev/null
+++ b/templates/shared/repo_search.tmpl
@@ -0,0 +1,67 @@
+<div class="ui secondary filter menu">
+	<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-flex-row tw-gap-x-2">
+		{{if .Language}}<input hidden name="language" value="{{.Language}}">{{end}}
+		<div class="ui fluid action input tw-flex-1">
+			{{template "shared/searchinput" dict "Value" .Keyword}}
+			{{if .PageIsExploreRepositories}}
+				<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">
+			{{else if .TabName}}
+				<input type="hidden" name="tab" value="{{.TabName}}">
+			{{end}}
+			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+		</div>
+		<!-- Filter -->
+		<div class="ui dropdown type jump item tw-mr-0">
+			<span class="text">
+				{{ctx.Locale.Tr "filter"}}
+			</span>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="menu">
+				<label class="item"><input type="radio" name="clear-filter"> {{ctx.Locale.Tr "filter.clear"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="archived" {{if .IsArchived.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_archived"}}</label>
+				<label class="item"><input type="radio" name="archived" {{if (not (.IsArchived.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_archived"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="fork" {{if .IsFork.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_fork"}}</label>
+				<label class="item"><input type="radio" name="fork" {{if (not (.IsFork.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_fork"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="mirror" {{if .IsMirror.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_mirror"}}</label>
+				<label class="item"><input type="radio" name="mirror" {{if (not (.IsMirror.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_mirror"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="template" {{if .IsTemplate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_template"}}</label>
+				<label class="item"><input type="radio" name="template" {{if (not (.IsTemplate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_template"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="private" {{if .IsPrivate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.private"}}</label>
+				<label class="item"><input type="radio" name="private" {{if (not (.IsPrivate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.public"}}</label>
+			</div>
+		</div>
+		<!-- Sort -->
+		<div class="ui dropdown type jump item gt-mr-0">
+			<span class="text">
+				{{ctx.Locale.Tr "repo.issues.filter_sort"}}
+			</span>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="menu">
+				<label class="{{if eq .SortType "newest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "newest"}}checked{{end}} value="newest"> {{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</label>
+				<label class="{{if eq .SortType "oldest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "oldest"}}checked{{end}} value="oldest"> {{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</label>
+				<label class="{{if eq .SortType "alphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "alphabetically"}}checked{{end}} value="alphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</label>
+				<label class="{{if eq .SortType "reversealphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversealphabetically"}}checked{{end}} value="reversealphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</label>
+				<label class="{{if eq .SortType "recentupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "recentupdate"}}checked{{end}} value="recentupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</label>
+				<label class="{{if eq .SortType "leastupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "leastupdate"}}checked{{end}} value="leastupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</label>
+				{{if not .DisableStars}}
+					<label class="{{if eq .SortType "moststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "moststars"}}checked{{end}} value="moststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</label>
+					<label class="{{if eq .SortType "feweststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "feweststars"}}checked{{end}} value="feweststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</label>
+				{{end}}
+				<label class="{{if eq .SortType "mostforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "mostforks"}}checked{{end}} value="mostforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</label>
+				<label class="{{if eq .SortType "fewestforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "fewestforks"}}checked{{end}} value="fewestforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</label>
+				<label class="{{if eq .SortType "size"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "size"}}checked{{end}} value="size"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</label>
+				<label class="{{if eq .SortType "reversesize"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversesize"}}checked{{end}} value="reversesize"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</label>
+			</div>
+		</div>
+	</form>
+</div>
+{{if and .PageIsExploreRepositories .OnlyShowRelevant}}
+	<div class="ui message explore-relevancy-note">
+		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}</span>
+	</div>
+{{end}}
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
index ec40d3afea..a37f0c352e 100644
--- a/templates/user/notification/notification_subscriptions.tmpl
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -69,7 +69,7 @@
 					{{template "shared/issuelist" dict "." . "listType" "dashboard"}}
 				{{end}}
 			{{else}}
-				{{template "explore/repo_search" .}}
+				{{template "shared/repo_search" .}}
 				{{template "explore/repo_list" .}}
 				{{template "base/paginate" .}}
 			{{end}}
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index 37590fc2fa..1495d58dd3 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -20,7 +20,7 @@
 					{{template "user/dashboard/feeds" .}}
 				{{else if eq .TabName "stars"}}
 					<div class="stars">
-						{{template "explore/repo_search" .}}
+						{{template "shared/repo_search" .}}
 						{{template "explore/repo_list" .}}
 						{{template "base/paginate" .}}
 					</div>
@@ -31,7 +31,7 @@
 				{{else if eq .TabName "overview"}}
 					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{else}}
-					{{template "explore/repo_search" .}}
+					{{template "shared/repo_search" .}}
 					{{template "explore/repo_list" .}}
 					{{template "base/paginate" .}}
 				{{end}}
diff --git a/web_src/js/features/repo-search.js b/web_src/js/features/repo-search.js
new file mode 100644
index 0000000000..185f6119d9
--- /dev/null
+++ b/web_src/js/features/repo-search.js
@@ -0,0 +1,22 @@
+export function initRepositorySearch() {
+  const repositorySearchForm = document.querySelector('#repo-search-form');
+  if (!repositorySearchForm) return;
+
+  repositorySearchForm.addEventListener('change', (e) => {
+    e.preventDefault();
+
+    const formData = new FormData(repositorySearchForm);
+    const params = new URLSearchParams(formData);
+
+    if (e.target.name === 'clear-filter') {
+      params.delete('archived');
+      params.delete('fork');
+      params.delete('mirror');
+      params.delete('template');
+      params.delete('private');
+    }
+
+    params.delete('clear-filter');
+    window.location.search = params.toString();
+  });
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index c7eac9d242..abf0d469d1 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -84,6 +84,7 @@ import {initRepoCodeFrequency} from './features/code-frequency.js';
 import {initRepoRecentCommits} from './features/recent-commits.js';
 import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 import {initDirAuto} from './modules/dirauto.js';
+import {initRepositorySearch} from './features/repo-search.js';
 
 // Init Gitea's Fomantic settings
 initGiteaFomantic();
@@ -170,6 +171,7 @@ onDomReady(() => {
   initRepoWikiForm();
   initRepository();
   initRepositoryActionView();
+  initRepositorySearch();
   initRepoContributors();
   initRepoCodeFrequency();
   initRepoRecentCommits();

From efa631aeead094267d46ea8f86e6d568f0c731e4 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 3 Mar 2024 17:23:14 +0100
Subject: [PATCH 250/679] Update js and py dependencies, bump python (#29561)

- Update js and py dependencies excluding `@mcaptcha/vanilla-glue`,
`eslint-plugin-array-func`
- Update stylelint config
- Require python 3.10 and use 3.12 on CI, bump setup-python as well
- Tested markdown toolbar, charts, clipboard, swagger ui, vue
---
 .github/workflows/pull-compliance.yml |    8 +-
 .stylelintrc.yaml                     |    1 +
 package-lock.json                     | 1026 +++++++++++++------------
 package.json                          |   32 +-
 poetry.lock                           |   39 +-
 pyproject.toml                        |    4 +-
 6 files changed, 597 insertions(+), 513 deletions(-)

diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 391137f015..02a265b1ff 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -32,9 +32,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: "3.11"
+          python-version: "3.12"
       - run: pip install poetry
       - run: make deps-py
       - run: make lint-templates
@@ -45,9 +45,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: "3.11"
+          python-version: "3.12"
       - run: pip install poetry
       - run: make deps-py
       - run: make lint-yaml
diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
index 7dd0a566f2..c7725159f1 100644
--- a/.stylelintrc.yaml
+++ b/.stylelintrc.yaml
@@ -64,6 +64,7 @@ rules:
   "@stylistic/media-query-list-comma-newline-before": null
   "@stylistic/media-query-list-comma-space-after": null
   "@stylistic/media-query-list-comma-space-before": null
+  "@stylistic/named-grid-areas-alignment": null
   "@stylistic/no-empty-first-line": null
   "@stylistic/no-eol-whitespace": true
   "@stylistic/no-extra-semicolons": true
diff --git a/package-lock.json b/package-lock.json
index 8f641edb5b..1189c90db9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
         "@citation-js/plugin-csl": "0.7.6",
         "@citation-js/plugin-software-formats": "0.6.1",
         "@claviska/jquery-minicolors": "2.3.6",
-        "@github/markdown-toolbar-element": "2.2.1",
+        "@github/markdown-toolbar-element": "2.2.3",
         "@github/relative-time-element": "4.3.1",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
@@ -18,11 +18,11 @@
         "@webcomponents/custom-elements": "1.6.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
-        "asciinema-player": "3.6.4",
-        "chart.js": "4.4.1",
+        "asciinema-player": "3.7.0",
+        "chart.js": "4.4.2",
         "chartjs-adapter-dayjs-4": "1.0.4",
         "chartjs-plugin-zoom": "2.0.1",
-        "clippie": "4.0.6",
+        "clippie": "4.0.7",
         "css-loader": "6.10.0",
         "css-variables-parser": "1.0.1",
         "dayjs": "1.11.10",
@@ -37,16 +37,16 @@
         "katex": "0.16.9",
         "license-checker-webpack-plugin": "0.2.1",
         "mermaid": "10.8.0",
-        "mini-css-extract-plugin": "2.8.0",
+        "mini-css-extract-plugin": "2.8.1",
         "minimatch": "9.0.3",
         "monaco-editor": "0.46.0",
         "monaco-editor-webpack-plugin": "7.1.0",
         "pdfobject": "2.3.0",
         "postcss": "8.4.35",
-        "postcss-loader": "8.1.0",
+        "postcss-loader": "8.1.1",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
-        "swagger-ui-dist": "5.11.6",
+        "swagger-ui-dist": "5.11.8",
         "tailwindcss": "3.4.1",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
@@ -54,25 +54,25 @@
         "toastify-js": "1.12.0",
         "tributejs": "5.1.3",
         "uint8-to-base64": "0.2.0",
-        "vue": "3.4.19",
+        "vue": "3.4.21",
         "vue-bar-graph": "2.0.0",
         "vue-chartjs": "5.3.0",
         "vue-loader": "17.4.2",
         "vue3-calendar-heatmap": "2.0.5",
-        "webpack": "5.90.2",
+        "webpack": "5.90.3",
         "webpack-cli": "5.1.4",
         "wrap-ansi": "9.0.0"
       },
       "devDependencies": {
         "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
-        "@playwright/test": "1.41.2",
+        "@playwright/test": "1.42.1",
         "@stoplight/spectral-cli": "6.11.0",
-        "@stylistic/eslint-plugin-js": "1.6.2",
-        "@stylistic/stylelint-plugin": "2.0.0",
+        "@stylistic/eslint-plugin-js": "1.6.3",
+        "@stylistic/stylelint-plugin": "2.1.0",
         "@vitejs/plugin-vue": "5.0.4",
-        "eslint": "8.56.0",
+        "eslint": "8.57.0",
         "eslint-plugin-array-func": "4.0.0",
-        "eslint-plugin-github": "4.10.1",
+        "eslint-plugin-github": "4.10.2",
         "eslint-plugin-i": "2.29.1",
         "eslint-plugin-jquery": "1.5.1",
         "eslint-plugin-no-jquery": "2.7.0",
@@ -82,7 +82,7 @@
         "eslint-plugin-unicorn": "51.0.1",
         "eslint-plugin-vitest": "0.3.22",
         "eslint-plugin-vitest-globals": "1.4.0",
-        "eslint-plugin-vue": "9.21.1",
+        "eslint-plugin-vue": "9.22.0",
         "eslint-plugin-vue-scoped-css": "2.7.2",
         "eslint-plugin-wc": "2.0.4",
         "jsdom": "24.0.0",
@@ -94,7 +94,7 @@
         "svgo": "3.2.0",
         "updates": "15.1.2",
         "vite-string-plugin": "1.1.5",
-        "vitest": "1.2.2"
+        "vitest": "1.3.1"
       },
       "engines": {
         "node": ">= 18.0.0"
@@ -296,9 +296,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
-      "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
+      "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -307,9 +307,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
-      "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
+      "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -466,9 +466,9 @@
       }
     },
     "node_modules/@csstools/css-parser-algorithms": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz",
-      "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==",
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.0.tgz",
+      "integrity": "sha512-YfEHq0eRH98ffb5/EsrrDspVWAuph6gDggAE74ZtjecsmyyWpW768hOyiONa8zwWGbIWYfa2Xp4tRTrpQQ00CQ==",
       "dev": true,
       "funding": [
         {
@@ -507,9 +507,9 @@
       }
     },
     "node_modules/@csstools/media-query-list-parser": {
-      "version": "2.1.7",
-      "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz",
-      "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.8.tgz",
+      "integrity": "sha512-DiD3vG5ciNzeuTEoh74S+JMjQDs50R3zlxHnBnfd04YYfA/kh2KiBCGhzqLxlJcNq+7yNQ3stuZZYLX6wK/U2g==",
       "dev": true,
       "funding": [
         {
@@ -525,14 +525,14 @@
         "node": "^14 || ^16 || >=18"
       },
       "peerDependencies": {
-        "@csstools/css-parser-algorithms": "^2.5.0",
+        "@csstools/css-parser-algorithms": "^2.6.0",
         "@csstools/css-tokenizer": "^2.2.3"
       }
     },
     "node_modules/@csstools/selector-specificity": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz",
-      "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz",
+      "integrity": "sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg==",
       "dev": true,
       "funding": [
         {
@@ -1012,9 +1012,9 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "8.56.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
-      "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+      "version": "8.57.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
+      "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1032,9 +1032,9 @@
       "integrity": "sha512-gwxPzLw8XKecy1nP63i9lOBritS3bWmxl02UX6G0TwMQZbMem1BCS1tEZgYd3mkrkiDrUMWaX+DbFCuDFo3K+A=="
     },
     "node_modules/@github/markdown-toolbar-element": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.1.tgz",
-      "integrity": "sha512-ap+ulyqzG3aVqwKsKjbDdYwM75TQXZpPtmIuPwm+54OTgcC96267oX3cEqd1wSqGsH7O5PonZ//fE9jH7Q4JkA=="
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.3.tgz",
+      "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
     },
     "node_modules/@github/relative-time-element": {
       "version": "4.3.1",
@@ -1142,11 +1142,6 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
-    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
-      "version": "9.2.2",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
-    },
     "node_modules/@isaacs/cliui/node_modules/string-width": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -1206,13 +1201,13 @@
       }
     },
     "node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
-      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
       "dependencies": {
-        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/set-array": "^1.2.1",
         "@jridgewell/sourcemap-codec": "^1.4.10",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/trace-mapping": "^0.3.24"
       },
       "engines": {
         "node": ">=6.0.0"
@@ -1227,9 +1222,9 @@
       }
     },
     "node_modules/@jridgewell/set-array": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
       "engines": {
         "node": ">=6.0.0"
       }
@@ -1249,9 +1244,9 @@
       "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
     },
     "node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.22",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
-      "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
+      "version": "0.3.25",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.1.0",
         "@jridgewell/sourcemap-codec": "^1.4.14"
@@ -1389,12 +1384,12 @@
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.41.2",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
-      "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
+      "version": "1.42.1",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
+      "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
       "dev": true,
       "dependencies": {
-        "playwright": "1.41.2"
+        "playwright": "1.42.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -1465,9 +1460,9 @@
       "dev": true
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.11.0.tgz",
-      "integrity": "sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz",
+      "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==",
       "cpu": [
         "arm"
       ],
@@ -1478,9 +1473,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.11.0.tgz",
-      "integrity": "sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz",
+      "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==",
       "cpu": [
         "arm64"
       ],
@@ -1491,9 +1486,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.11.0.tgz",
-      "integrity": "sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz",
+      "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==",
       "cpu": [
         "arm64"
       ],
@@ -1504,9 +1499,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.11.0.tgz",
-      "integrity": "sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz",
+      "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==",
       "cpu": [
         "x64"
       ],
@@ -1517,9 +1512,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.11.0.tgz",
-      "integrity": "sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz",
+      "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==",
       "cpu": [
         "arm"
       ],
@@ -1530,9 +1525,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.11.0.tgz",
-      "integrity": "sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz",
+      "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==",
       "cpu": [
         "arm64"
       ],
@@ -1543,9 +1538,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.11.0.tgz",
-      "integrity": "sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz",
+      "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==",
       "cpu": [
         "arm64"
       ],
@@ -1556,9 +1551,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.11.0.tgz",
-      "integrity": "sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz",
+      "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==",
       "cpu": [
         "riscv64"
       ],
@@ -1569,9 +1564,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.11.0.tgz",
-      "integrity": "sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz",
+      "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==",
       "cpu": [
         "x64"
       ],
@@ -1582,9 +1577,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.11.0.tgz",
-      "integrity": "sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz",
+      "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==",
       "cpu": [
         "x64"
       ],
@@ -1595,9 +1590,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.11.0.tgz",
-      "integrity": "sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz",
+      "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==",
       "cpu": [
         "arm64"
       ],
@@ -1608,9 +1603,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.11.0.tgz",
-      "integrity": "sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz",
+      "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==",
       "cpu": [
         "ia32"
       ],
@@ -1621,9 +1616,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.11.0.tgz",
-      "integrity": "sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz",
+      "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==",
       "cpu": [
         "x64"
       ],
@@ -2091,9 +2086,9 @@
       "dev": true
     },
     "node_modules/@stylistic/eslint-plugin-js": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.2.tgz",
-      "integrity": "sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==",
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.3.tgz",
+      "integrity": "sha512-ckdz51oHxD2FaxgY2piJWJVJiwgp8Uu96s+as2yB3RMwavn3nHBrpliVukXY9S/DmMicPRB2+H8nBk23GDG+qA==",
       "dev": true,
       "dependencies": {
         "@types/eslint": "^8.56.2",
@@ -2110,19 +2105,19 @@
       }
     },
     "node_modules/@stylistic/stylelint-plugin": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.0.0.tgz",
-      "integrity": "sha512-dHKuT6PGd1WGZLOTuozAM7GdQzdmlmnFXYzvV1jYJXXpcCpV/OJ3+n8TXpMkoOeKHpJydY43EOoZTO1W/FOA4Q==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.1.0.tgz",
+      "integrity": "sha512-mUZEW9uImHSbXeyzbFmHb8WPBv56UTaEnWL/3dGdAiJ54C+8GTfDwDVdI6gbqT9wV7zynkPu7tCXc5746H9mZQ==",
       "dev": true,
       "dependencies": {
-        "@csstools/css-parser-algorithms": "^2.3.2",
-        "@csstools/css-tokenizer": "^2.2.1",
-        "@csstools/media-query-list-parser": "^2.1.5",
+        "@csstools/css-parser-algorithms": "^2.5.0",
+        "@csstools/css-tokenizer": "^2.2.3",
+        "@csstools/media-query-list-parser": "^2.1.7",
         "is-plain-object": "^5.0.0",
-        "postcss-selector-parser": "^6.0.13",
+        "postcss-selector-parser": "^6.0.15",
         "postcss-value-parser": "^4.2.0",
         "style-search": "^0.1.0",
-        "stylelint": "^16.0.2"
+        "stylelint": "^16.2.1"
       },
       "engines": {
         "node": "^18.12 || >=20.9"
@@ -2189,9 +2184,9 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.2",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
-      "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
+      "version": "8.56.5",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz",
+      "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==",
       "dependencies": {
         "@types/estree": "*",
         "@types/json-schema": "*"
@@ -2241,9 +2236,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.19",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
-      "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
+      "version": "20.11.24",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
+      "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -2261,9 +2256,9 @@
       "dev": true
     },
     "node_modules/@types/semver": {
-      "version": "7.5.7",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
-      "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
+      "version": "7.5.8",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
       "dev": true
     },
     "node_modules/@types/tern": {
@@ -2286,16 +2281,16 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
-      "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz",
+      "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "6.21.0",
-        "@typescript-eslint/type-utils": "6.21.0",
-        "@typescript-eslint/utils": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0",
+        "@typescript-eslint/scope-manager": "7.1.0",
+        "@typescript-eslint/type-utils": "7.1.0",
+        "@typescript-eslint/utils": "7.1.0",
+        "@typescript-eslint/visitor-keys": "7.1.0",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
@@ -2311,8 +2306,8 @@
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
-        "eslint": "^7.0.0 || ^8.0.0"
+        "@typescript-eslint/parser": "^7.0.0",
+        "eslint": "^8.56.0"
       },
       "peerDependenciesMeta": {
         "typescript": {
@@ -2321,15 +2316,15 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
-      "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz",
+      "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "6.21.0",
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/typescript-estree": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0",
+        "@typescript-eslint/scope-manager": "7.1.0",
+        "@typescript-eslint/types": "7.1.0",
+        "@typescript-eslint/typescript-estree": "7.1.0",
+        "@typescript-eslint/visitor-keys": "7.1.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2340,7 +2335,7 @@
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
+        "eslint": "^8.56.0"
       },
       "peerDependenciesMeta": {
         "typescript": {
@@ -2349,13 +2344,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
-      "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz",
+      "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0"
+        "@typescript-eslint/types": "7.1.0",
+        "@typescript-eslint/visitor-keys": "7.1.0"
       },
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2366,13 +2361,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
-      "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz",
+      "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "6.21.0",
-        "@typescript-eslint/utils": "6.21.0",
+        "@typescript-eslint/typescript-estree": "7.1.0",
+        "@typescript-eslint/utils": "7.1.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2384,7 +2379,7 @@
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
+        "eslint": "^8.56.0"
       },
       "peerDependenciesMeta": {
         "typescript": {
@@ -2393,9 +2388,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
-      "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz",
+      "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2406,13 +2401,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
-      "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz",
+      "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0",
+        "@typescript-eslint/types": "7.1.0",
+        "@typescript-eslint/visitor-keys": "7.1.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2434,17 +2429,17 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
-      "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz",
+      "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.21.0",
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/scope-manager": "7.1.0",
+        "@typescript-eslint/types": "7.1.0",
+        "@typescript-eslint/typescript-estree": "7.1.0",
         "semver": "^7.5.4"
       },
       "engines": {
@@ -2455,16 +2450,16 @@
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
+        "eslint": "^8.56.0"
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
-      "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz",
+      "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/types": "7.1.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2495,13 +2490,13 @@
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
-      "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
+      "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
       "dev": true,
       "dependencies": {
-        "@vitest/spy": "1.2.2",
-        "@vitest/utils": "1.2.2",
+        "@vitest/spy": "1.3.1",
+        "@vitest/utils": "1.3.1",
         "chai": "^4.3.10"
       },
       "funding": {
@@ -2509,12 +2504,12 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
-      "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
+      "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
       "dev": true,
       "dependencies": {
-        "@vitest/utils": "1.2.2",
+        "@vitest/utils": "1.3.1",
         "p-limit": "^5.0.0",
         "pathe": "^1.1.1"
       },
@@ -2550,9 +2545,9 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
-      "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
+      "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
       "dev": true,
       "dependencies": {
         "magic-string": "^0.30.5",
@@ -2576,9 +2571,9 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
-      "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
+      "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
       "dev": true,
       "dependencies": {
         "tinyspy": "^2.2.0"
@@ -2588,9 +2583,9 @@
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
-      "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
+      "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
       "dev": true,
       "dependencies": {
         "diff-sequences": "^29.6.3",
@@ -2618,39 +2613,39 @@
       }
     },
     "node_modules/@vue/compiler-core": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
-      "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
+      "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
       "dependencies": {
         "@babel/parser": "^7.23.9",
-        "@vue/shared": "3.4.19",
+        "@vue/shared": "3.4.21",
         "entities": "^4.5.0",
         "estree-walker": "^2.0.2",
         "source-map-js": "^1.0.2"
       }
     },
     "node_modules/@vue/compiler-dom": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
-      "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
+      "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
       "dependencies": {
-        "@vue/compiler-core": "3.4.19",
-        "@vue/shared": "3.4.19"
+        "@vue/compiler-core": "3.4.21",
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz",
-      "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
+      "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
       "dependencies": {
         "@babel/parser": "^7.23.9",
-        "@vue/compiler-core": "3.4.19",
-        "@vue/compiler-dom": "3.4.19",
-        "@vue/compiler-ssr": "3.4.19",
-        "@vue/shared": "3.4.19",
+        "@vue/compiler-core": "3.4.21",
+        "@vue/compiler-dom": "3.4.21",
+        "@vue/compiler-ssr": "3.4.21",
+        "@vue/shared": "3.4.21",
         "estree-walker": "^2.0.2",
-        "magic-string": "^0.30.6",
-        "postcss": "^8.4.33",
+        "magic-string": "^0.30.7",
+        "postcss": "^8.4.35",
         "source-map-js": "^1.0.2"
       }
     },
@@ -2666,57 +2661,57 @@
       }
     },
     "node_modules/@vue/compiler-ssr": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
-      "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
+      "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.19",
-        "@vue/shared": "3.4.19"
+        "@vue/compiler-dom": "3.4.21",
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/reactivity": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz",
-      "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
+      "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
       "dependencies": {
-        "@vue/shared": "3.4.19"
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/runtime-core": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz",
-      "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
+      "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
       "dependencies": {
-        "@vue/reactivity": "3.4.19",
-        "@vue/shared": "3.4.19"
+        "@vue/reactivity": "3.4.21",
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/runtime-dom": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz",
-      "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
+      "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
       "dependencies": {
-        "@vue/runtime-core": "3.4.19",
-        "@vue/shared": "3.4.19",
+        "@vue/runtime-core": "3.4.21",
+        "@vue/shared": "3.4.21",
         "csstype": "^3.1.3"
       }
     },
     "node_modules/@vue/server-renderer": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz",
-      "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
+      "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
       "dependencies": {
-        "@vue/compiler-ssr": "3.4.19",
-        "@vue/shared": "3.4.19"
+        "@vue/compiler-ssr": "3.4.21",
+        "@vue/shared": "3.4.21"
       },
       "peerDependencies": {
-        "vue": "3.4.19"
+        "vue": "3.4.21"
       }
     },
     "node_modules/@vue/shared": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
-      "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw=="
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
+      "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
     },
     "node_modules/@webassemblyjs/ast": {
       "version": "1.11.6",
@@ -3269,9 +3264,9 @@
       }
     },
     "node_modules/asciinema-player": {
-      "version": "3.6.4",
-      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.4.tgz",
-      "integrity": "sha512-yyMHTjoDuz82/BYPrc3J5GjOtlNI5t2VHTZWss8BmRcY/6nXv+Vilip+XzwIyRBa3/2SSn9FJIEg8bJXBc9o4w==",
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.7.0.tgz",
+      "integrity": "sha512-0RDc4j7TkjyhAwxkDe3vNqjAcizc7tubYW2VZi/06csY8iAoSC2uRvSyfNzh9ONDZu8pdf0bZJ91A84Gexb3tg==",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
         "solid-js": "^1.3.0"
@@ -3350,10 +3345,13 @@
       }
     },
     "node_modules/available-typed-arrays": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz",
-      "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
       "dev": true,
+      "dependencies": {
+        "possible-typed-array-names": "^1.0.0"
+      },
       "engines": {
         "node": ">= 0.4"
       },
@@ -3566,9 +3564,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001587",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz",
-      "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==",
+      "version": "1.0.30001591",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
+      "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -3627,14 +3625,14 @@
       }
     },
     "node_modules/chart.js": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
-      "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
+      "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
       "dependencies": {
         "@kurkle/color": "^0.3.0"
       },
       "engines": {
-        "pnpm": ">=7"
+        "pnpm": ">=8"
       }
     },
     "node_modules/chartjs-adapter-dayjs-4": {
@@ -3756,9 +3754,9 @@
       }
     },
     "node_modules/clippie": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.6.tgz",
-      "integrity": "sha512-E5EtOw8iMH0enuL3kBZJ+Po1nPnBD7O+HHpIaWpfWgHbHmdoOQoERrlNOcEEn2yMJQ98WqeKacouAcnRXn7oWA=="
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.7.tgz",
+      "integrity": "sha512-xmIARCRFQUoCR0kNNu4uIv5f/IFqM1fUts0vQwt1hQEdCPEqs3/dTaG38WenlWOgs3Fcn73PBYXbPIVSlOgFRw=="
     },
     "node_modules/cliui": {
       "version": "7.0.4",
@@ -4842,9 +4840,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.0.8",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz",
-      "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ=="
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz",
+      "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ=="
     },
     "node_modules/domutils": {
       "version": "3.1.0",
@@ -4887,19 +4885,19 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.671",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.671.tgz",
-      "integrity": "sha512-UUlE+/rWbydmp+FW8xlnnTA5WNA0ZZd2XL8CuMS72rh+k4y1f8+z6yk3UQhEwqHQWj6IBdL78DwWOdGMvYfQyA=="
+      "version": "1.4.690",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz",
+      "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA=="
     },
     "node_modules/elkjs": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.1.tgz",
-      "integrity": "sha512-JWKDyqAdltuUcyxaECtYG6H4sqysXSLeoXuGUBfRNESMTkj+w+qdb0jya8Z/WI0jVd03WQtCGhS6FOFtlhD5FQ=="
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz",
+      "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw=="
     },
     "node_modules/emoji-regex": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
     },
     "node_modules/emojis-list": {
       "version": "3.0.0",
@@ -4910,9 +4908,9 @@
       }
     },
     "node_modules/enhanced-resolve": {
-      "version": "5.15.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
-      "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+      "version": "5.15.1",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz",
+      "integrity": "sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg==",
       "dependencies": {
         "graceful-fs": "^4.2.4",
         "tapable": "^2.2.0"
@@ -4960,18 +4958,18 @@
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.22.4",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz",
-      "integrity": "sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==",
+      "version": "1.22.5",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz",
+      "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==",
       "dev": true,
       "dependencies": {
         "array-buffer-byte-length": "^1.0.1",
         "arraybuffer.prototype.slice": "^1.0.3",
-        "available-typed-arrays": "^1.0.6",
+        "available-typed-arrays": "^1.0.7",
         "call-bind": "^1.0.7",
         "es-define-property": "^1.0.0",
         "es-errors": "^1.3.0",
-        "es-set-tostringtag": "^2.0.2",
+        "es-set-tostringtag": "^2.0.3",
         "es-to-primitive": "^1.2.1",
         "function.prototype.name": "^1.1.6",
         "get-intrinsic": "^1.2.4",
@@ -4979,15 +4977,15 @@
         "globalthis": "^1.0.3",
         "gopd": "^1.0.1",
         "has-property-descriptors": "^1.0.2",
-        "has-proto": "^1.0.1",
+        "has-proto": "^1.0.3",
         "has-symbols": "^1.0.3",
         "hasown": "^2.0.1",
         "internal-slot": "^1.0.7",
         "is-array-buffer": "^3.0.4",
         "is-callable": "^1.2.7",
-        "is-negative-zero": "^2.0.2",
+        "is-negative-zero": "^2.0.3",
         "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.2",
+        "is-shared-array-buffer": "^1.0.3",
         "is-string": "^1.0.7",
         "is-typed-array": "^1.1.13",
         "is-weakref": "^1.0.2",
@@ -5000,10 +4998,10 @@
         "string.prototype.trim": "^1.2.8",
         "string.prototype.trimend": "^1.0.7",
         "string.prototype.trimstart": "^1.0.7",
-        "typed-array-buffer": "^1.0.1",
-        "typed-array-byte-length": "^1.0.0",
-        "typed-array-byte-offset": "^1.0.0",
-        "typed-array-length": "^1.0.4",
+        "typed-array-buffer": "^1.0.2",
+        "typed-array-byte-length": "^1.0.1",
+        "typed-array-byte-offset": "^1.0.2",
+        "typed-array-length": "^1.0.5",
         "unbox-primitive": "^1.0.2",
         "which-typed-array": "^1.1.14"
       },
@@ -5095,14 +5093,14 @@
       "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="
     },
     "node_modules/es-set-tostringtag": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
-      "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+      "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.2",
-        "has-tostringtag": "^1.0.0",
-        "hasown": "^2.0.0"
+        "get-intrinsic": "^1.2.4",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5220,16 +5218,16 @@
       }
     },
     "node_modules/eslint": {
-      "version": "8.56.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
-      "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+      "version": "8.57.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
+      "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
         "@eslint/eslintrc": "^2.1.4",
-        "@eslint/js": "8.56.0",
-        "@humanwhocodes/config-array": "^0.11.13",
+        "@eslint/js": "8.57.0",
+        "@humanwhocodes/config-array": "^0.11.14",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@nodelib/fs.walk": "^1.2.8",
         "@ungap/structured-clone": "^1.2.0",
@@ -5322,9 +5320,9 @@
       }
     },
     "node_modules/eslint-module-utils": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
-      "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
+      "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
       "dev": true,
       "dependencies": {
         "debug": "^3.2.7"
@@ -5415,14 +5413,14 @@
       }
     },
     "node_modules/eslint-plugin-github": {
-      "version": "4.10.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.1.tgz",
-      "integrity": "sha512-1AqQBockOM+m0ZUpwfjWtX0lWdX5cRi/hwJnSNvXoOmz/Hh+ULH6QFz6ENWueTWjoWpgPv0af3bj+snps6o4og==",
+      "version": "4.10.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.2.tgz",
+      "integrity": "sha512-F1F5aAFgi1Y5hYoTFzGQACBkw5W1hu2Fu5FSTrMlXqrojJnKl1S2pWO/rprlowRQpt+hzHhqSpsfnodJEVd5QA==",
       "dev": true,
       "dependencies": {
         "@github/browserslist-config": "^1.0.0",
-        "@typescript-eslint/eslint-plugin": "^6.0.0",
-        "@typescript-eslint/parser": "^6.0.0",
+        "@typescript-eslint/eslint-plugin": "^7.0.1",
+        "@typescript-eslint/parser": "^7.0.1",
         "aria-query": "^5.3.0",
         "eslint-config-prettier": ">=8.0.0",
         "eslint-plugin-escompat": "^3.3.3",
@@ -5633,12 +5631,6 @@
         "concat-map": "0.0.1"
       }
     },
-    "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": {
-      "version": "9.2.2",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-      "dev": true
-    },
     "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -5810,17 +5802,117 @@
       "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==",
       "dev": true
     },
+    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/scope-manager": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+      "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+      "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+      "dev": true,
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/typescript-estree": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+      "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "minimatch": "9.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/utils": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+      "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/visitor-keys": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+      "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
     "node_modules/eslint-plugin-vue": {
-      "version": "9.21.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz",
-      "integrity": "sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==",
+      "version": "9.22.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.22.0.tgz",
+      "integrity": "sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "natural-compare": "^1.4.0",
         "nth-check": "^2.1.1",
-        "postcss-selector-parser": "^6.0.13",
-        "semver": "^7.5.4",
+        "postcss-selector-parser": "^6.0.15",
+        "semver": "^7.6.0",
         "vue-eslint-parser": "^9.4.2",
         "xml-name-validator": "^4.0.0"
       },
@@ -6231,9 +6323,9 @@
       }
     },
     "node_modules/flatted": {
-      "version": "3.2.9",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
-      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
       "dev": true
     },
     "node_modules/for-each": {
@@ -6681,9 +6773,9 @@
       }
     },
     "node_modules/has-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
-      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
@@ -7218,9 +7310,9 @@
       }
     },
     "node_modules/is-negative-zero": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
-      "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+      "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
@@ -7331,12 +7423,15 @@
       }
     },
     "node_modules/is-shared-array-buffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
-      "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2"
+        "call-bind": "^1.0.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -8777,9 +8872,9 @@
       }
     },
     "node_modules/mini-css-extract-plugin": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz",
-      "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==",
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz",
+      "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==",
       "dependencies": {
         "schema-utils": "^4.0.0",
         "tapable": "^2.2.1"
@@ -8827,9 +8922,9 @@
       }
     },
     "node_modules/mlly": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz",
-      "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==",
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
+      "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==",
       "dev": true,
       "dependencies": {
         "acorn": "^8.11.3",
@@ -9031,9 +9126,9 @@
       }
     },
     "node_modules/npm-run-path": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
-      "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
       "dev": true,
       "dependencies": {
         "path-key": "^4.0.0"
@@ -9516,12 +9611,12 @@
       "dev": true
     },
     "node_modules/playwright": {
-      "version": "1.41.2",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
-      "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
+      "version": "1.42.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
+      "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
       "dev": true,
       "dependencies": {
-        "playwright-core": "1.41.2"
+        "playwright-core": "1.42.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -9534,9 +9629,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.41.2",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
-      "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
+      "version": "1.42.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
+      "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -9563,6 +9658,15 @@
         "node": ">=12.0.0"
       }
     },
+    "node_modules/possible-typed-array-names": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+      "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.4.35",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
@@ -9640,9 +9744,9 @@
       }
     },
     "node_modules/postcss-loader": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz",
-      "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==",
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
+      "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==",
       "dependencies": {
         "cosmiconfig": "^9.0.0",
         "jiti": "^1.20.0",
@@ -10570,14 +10674,15 @@
       }
     },
     "node_modules/set-function-name": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
-      "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.0.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
         "functions-have-names": "^1.2.3",
-        "has-property-descriptors": "^1.0.0"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -10614,12 +10719,12 @@
       }
     },
     "node_modules/side-channel": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
-      "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+      "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bind": "^1.0.7",
         "es-errors": "^1.3.0",
         "get-intrinsic": "^1.2.4",
         "object-inspect": "^1.13.1"
@@ -10860,6 +10965,16 @@
         "node": ">=8"
       }
     },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/string-width/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
     "node_modules/string.prototype.trim": {
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
@@ -10974,12 +11089,12 @@
       }
     },
     "node_modules/strip-literal": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
-      "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz",
+      "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==",
       "dev": true,
       "dependencies": {
-        "acorn": "^8.10.0"
+        "js-tokens": "^8.0.2"
       },
       "funding": {
         "url": "https://github.com/sponsors/antfu"
@@ -11102,41 +11217,18 @@
       }
     },
     "node_modules/stylelint/node_modules/flat-cache": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.0.tgz",
-      "integrity": "sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
       "dev": true,
       "dependencies": {
         "flatted": "^3.2.9",
-        "keyv": "^4.5.4",
-        "rimraf": "^5.0.5"
+        "keyv": "^4.5.4"
       },
       "engines": {
         "node": ">=16"
       }
     },
-    "node_modules/stylelint/node_modules/glob": {
-      "version": "10.3.10",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
-      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
-      "dev": true,
-      "dependencies": {
-        "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.5",
-        "minimatch": "^9.0.1",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-        "path-scurry": "^1.10.1"
-      },
-      "bin": {
-        "glob": "dist/esm/bin.mjs"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/stylelint/node_modules/postcss-safe-parser": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz",
@@ -11172,24 +11264,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/stylelint/node_modules/rimraf": {
-      "version": "5.0.5",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
-      "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
-      "dev": true,
-      "dependencies": {
-        "glob": "^10.3.7"
-      },
-      "bin": {
-        "rimraf": "dist/esm/bin.mjs"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/stylelint/node_modules/strip-ansi": {
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
@@ -11380,9 +11454,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.11.6",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.6.tgz",
-      "integrity": "sha512-K5BpYuMoPpJY7NwCHIWohH6tU9o0fs1+plNT5KJ+3BBlVEh4H1CpeKJV8o91lpscVY9oqb2jmaAassnW3wVoTg=="
+      "version": "5.11.8",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.8.tgz",
+      "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA=="
     },
     "node_modules/symbol-tree": {
       "version": "3.2.4",
@@ -11524,9 +11598,9 @@
       }
     },
     "node_modules/terser": {
-      "version": "5.27.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz",
-      "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==",
+      "version": "5.28.1",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz",
+      "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
@@ -11839,12 +11913,12 @@
       }
     },
     "node_modules/typed-array-buffer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz",
-      "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
+      "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bind": "^1.0.7",
         "es-errors": "^1.3.0",
         "is-typed-array": "^1.1.13"
       },
@@ -11853,15 +11927,16 @@
       }
     },
     "node_modules/typed-array-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
-      "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
+      "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13"
       },
       "engines": {
         "node": ">= 0.4"
@@ -11871,16 +11946,17 @@
       }
     },
     "node_modules/typed-array-byte-offset": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
-      "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
+      "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
       "dev": true,
       "dependencies": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.2",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13"
       },
       "engines": {
         "node": ">= 0.4"
@@ -11890,14 +11966,20 @@
       }
     },
     "node_modules/typed-array-length": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
-      "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz",
+      "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "is-typed-array": "^1.1.9"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13",
+        "possible-typed-array-names": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -11923,9 +12005,9 @@
       "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA=="
     },
     "node_modules/uc.micro": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz",
-      "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
       "dev": true
     },
     "node_modules/ufo": {
@@ -12108,9 +12190,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.1.3",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
-      "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
+      "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
       "dev": true,
       "dependencies": {
         "esbuild": "^0.19.3",
@@ -12163,9 +12245,9 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
-      "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
+      "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
       "dev": true,
       "dependencies": {
         "cac": "^6.7.14",
@@ -12211,9 +12293,9 @@
       }
     },
     "node_modules/vite/node_modules/rollup": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.11.0.tgz",
-      "integrity": "sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz",
+      "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==",
       "dev": true,
       "dependencies": {
         "@types/estree": "1.0.5"
@@ -12226,35 +12308,34 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.11.0",
-        "@rollup/rollup-android-arm64": "4.11.0",
-        "@rollup/rollup-darwin-arm64": "4.11.0",
-        "@rollup/rollup-darwin-x64": "4.11.0",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.11.0",
-        "@rollup/rollup-linux-arm64-gnu": "4.11.0",
-        "@rollup/rollup-linux-arm64-musl": "4.11.0",
-        "@rollup/rollup-linux-riscv64-gnu": "4.11.0",
-        "@rollup/rollup-linux-x64-gnu": "4.11.0",
-        "@rollup/rollup-linux-x64-musl": "4.11.0",
-        "@rollup/rollup-win32-arm64-msvc": "4.11.0",
-        "@rollup/rollup-win32-ia32-msvc": "4.11.0",
-        "@rollup/rollup-win32-x64-msvc": "4.11.0",
+        "@rollup/rollup-android-arm-eabi": "4.12.0",
+        "@rollup/rollup-android-arm64": "4.12.0",
+        "@rollup/rollup-darwin-arm64": "4.12.0",
+        "@rollup/rollup-darwin-x64": "4.12.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.12.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.12.0",
+        "@rollup/rollup-linux-arm64-musl": "4.12.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.12.0",
+        "@rollup/rollup-linux-x64-gnu": "4.12.0",
+        "@rollup/rollup-linux-x64-musl": "4.12.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.12.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.12.0",
+        "@rollup/rollup-win32-x64-msvc": "4.12.0",
         "fsevents": "~2.3.2"
       }
     },
     "node_modules/vitest": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
-      "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
+      "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
       "dev": true,
       "dependencies": {
-        "@vitest/expect": "1.2.2",
-        "@vitest/runner": "1.2.2",
-        "@vitest/snapshot": "1.2.2",
-        "@vitest/spy": "1.2.2",
-        "@vitest/utils": "1.2.2",
+        "@vitest/expect": "1.3.1",
+        "@vitest/runner": "1.3.1",
+        "@vitest/snapshot": "1.3.1",
+        "@vitest/spy": "1.3.1",
+        "@vitest/utils": "1.3.1",
         "acorn-walk": "^8.3.2",
-        "cac": "^6.7.14",
         "chai": "^4.3.10",
         "debug": "^4.3.4",
         "execa": "^8.0.1",
@@ -12263,11 +12344,11 @@
         "pathe": "^1.1.1",
         "picocolors": "^1.0.0",
         "std-env": "^3.5.0",
-        "strip-literal": "^1.3.0",
+        "strip-literal": "^2.0.0",
         "tinybench": "^2.5.1",
         "tinypool": "^0.8.2",
         "vite": "^5.0.0",
-        "vite-node": "1.2.2",
+        "vite-node": "1.3.1",
         "why-is-node-running": "^2.2.2"
       },
       "bin": {
@@ -12282,8 +12363,8 @@
       "peerDependencies": {
         "@edge-runtime/vm": "*",
         "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "^1.0.0",
-        "@vitest/ui": "^1.0.0",
+        "@vitest/browser": "1.3.1",
+        "@vitest/ui": "1.3.1",
         "happy-dom": "*",
         "jsdom": "*"
       },
@@ -12321,15 +12402,15 @@
       }
     },
     "node_modules/vue": {
-      "version": "3.4.19",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",
-      "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
+      "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.19",
-        "@vue/compiler-sfc": "3.4.19",
-        "@vue/runtime-dom": "3.4.19",
-        "@vue/server-renderer": "3.4.19",
-        "@vue/shared": "3.4.19"
+        "@vue/compiler-dom": "3.4.21",
+        "@vue/compiler-sfc": "3.4.21",
+        "@vue/runtime-dom": "3.4.21",
+        "@vue/server-renderer": "3.4.21",
+        "@vue/shared": "3.4.21"
       },
       "peerDependencies": {
         "typescript": "*"
@@ -12463,9 +12544,9 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.90.2",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz",
-      "integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==",
+      "version": "5.90.3",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
+      "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^1.0.5",
@@ -12964,9 +13045,12 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/yaml": {
-      "version": "2.3.4",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
-      "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz",
+      "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
       "engines": {
         "node": ">= 14"
       }
diff --git a/package.json b/package.json
index 3f0f9103cf..1152bfef72 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
     "@citation-js/plugin-csl": "0.7.6",
     "@citation-js/plugin-software-formats": "0.6.1",
     "@claviska/jquery-minicolors": "2.3.6",
-    "@github/markdown-toolbar-element": "2.2.1",
+    "@github/markdown-toolbar-element": "2.2.3",
     "@github/relative-time-element": "4.3.1",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
@@ -17,11 +17,11 @@
     "@webcomponents/custom-elements": "1.6.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
-    "asciinema-player": "3.6.4",
-    "chart.js": "4.4.1",
+    "asciinema-player": "3.7.0",
+    "chart.js": "4.4.2",
     "chartjs-adapter-dayjs-4": "1.0.4",
     "chartjs-plugin-zoom": "2.0.1",
-    "clippie": "4.0.6",
+    "clippie": "4.0.7",
     "css-loader": "6.10.0",
     "css-variables-parser": "1.0.1",
     "dayjs": "1.11.10",
@@ -36,16 +36,16 @@
     "katex": "0.16.9",
     "license-checker-webpack-plugin": "0.2.1",
     "mermaid": "10.8.0",
-    "mini-css-extract-plugin": "2.8.0",
+    "mini-css-extract-plugin": "2.8.1",
     "minimatch": "9.0.3",
     "monaco-editor": "0.46.0",
     "monaco-editor-webpack-plugin": "7.1.0",
     "pdfobject": "2.3.0",
     "postcss": "8.4.35",
-    "postcss-loader": "8.1.0",
+    "postcss-loader": "8.1.1",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
-    "swagger-ui-dist": "5.11.6",
+    "swagger-ui-dist": "5.11.8",
     "tailwindcss": "3.4.1",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
@@ -53,25 +53,25 @@
     "toastify-js": "1.12.0",
     "tributejs": "5.1.3",
     "uint8-to-base64": "0.2.0",
-    "vue": "3.4.19",
+    "vue": "3.4.21",
     "vue-bar-graph": "2.0.0",
     "vue-chartjs": "5.3.0",
     "vue-loader": "17.4.2",
     "vue3-calendar-heatmap": "2.0.5",
-    "webpack": "5.90.2",
+    "webpack": "5.90.3",
     "webpack-cli": "5.1.4",
     "wrap-ansi": "9.0.0"
   },
   "devDependencies": {
     "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
-    "@playwright/test": "1.41.2",
+    "@playwright/test": "1.42.1",
     "@stoplight/spectral-cli": "6.11.0",
-    "@stylistic/eslint-plugin-js": "1.6.2",
-    "@stylistic/stylelint-plugin": "2.0.0",
+    "@stylistic/eslint-plugin-js": "1.6.3",
+    "@stylistic/stylelint-plugin": "2.1.0",
     "@vitejs/plugin-vue": "5.0.4",
-    "eslint": "8.56.0",
+    "eslint": "8.57.0",
     "eslint-plugin-array-func": "4.0.0",
-    "eslint-plugin-github": "4.10.1",
+    "eslint-plugin-github": "4.10.2",
     "eslint-plugin-i": "2.29.1",
     "eslint-plugin-jquery": "1.5.1",
     "eslint-plugin-no-jquery": "2.7.0",
@@ -81,7 +81,7 @@
     "eslint-plugin-unicorn": "51.0.1",
     "eslint-plugin-vitest": "0.3.22",
     "eslint-plugin-vitest-globals": "1.4.0",
-    "eslint-plugin-vue": "9.21.1",
+    "eslint-plugin-vue": "9.22.0",
     "eslint-plugin-vue-scoped-css": "2.7.2",
     "eslint-plugin-wc": "2.0.4",
     "jsdom": "24.0.0",
@@ -93,7 +93,7 @@
     "svgo": "3.2.0",
     "updates": "15.1.2",
     "vite-string-plugin": "1.1.5",
-    "vitest": "1.2.2"
+    "vitest": "1.3.1"
   },
   "browserslist": [
     "defaults"
diff --git a/poetry.lock b/poetry.lock
index 4cb58c6ef2..46520fba3c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 
 [[package]]
 name = "click"
@@ -27,12 +27,12 @@ files = [
 
 [[package]]
 name = "cssbeautifier"
-version = "1.14.11"
+version = "1.15.1"
 description = "CSS unobfuscator and beautifier."
 optional = false
 python-versions = "*"
 files = [
-    {file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"},
+    {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
 ]
 
 [package.dependencies]
@@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0"
 
 [[package]]
 name = "editorconfig"
-version = "0.12.3"
+version = "0.12.4"
 description = "EditorConfig File Locator and Interpreter for Python"
 optional = false
 python-versions = "*"
 files = [
-    {file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"},
-    {file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
+    {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
 ]
 
 [[package]]
@@ -100,12 +99,12 @@ files = [
 
 [[package]]
 name = "jsbeautifier"
-version = "1.14.11"
+version = "1.15.1"
 description = "JavaScript unobfuscator and beautifier."
 optional = false
 python-versions = "*"
 files = [
-    {file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"},
+    {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
 ]
 
 [package.dependencies]
@@ -114,13 +113,13 @@ six = ">=1.13.0"
 
 [[package]]
 name = "json5"
-version = "0.9.14"
+version = "0.9.18"
 description = "A Python implementation of the JSON5 data format."
 optional = false
-python-versions = "*"
+python-versions = ">=3.8"
 files = [
-    {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
-    {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
+    {file = "json5-0.9.18-py2.py3-none-any.whl", hash = "sha256:3f20193ff8dfdec6ab114b344e7ac5d76fac453c8bab9bdfe1460d1d528ec393"},
+    {file = "json5-0.9.18.tar.gz", hash = "sha256:ecb8ac357004e3522fb989da1bf08b146011edbd14fdffae6caad3bd68493467"},
 ]
 
 [package.extras]
@@ -322,13 +321,13 @@ files = [
 
 [[package]]
 name = "tqdm"
-version = "4.66.1"
+version = "4.66.2"
 description = "Fast, Extensible Progress Meter"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
-    {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
+    {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
+    {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
 ]
 
 [package.dependencies]
@@ -342,13 +341,13 @@ telegram = ["requests"]
 
 [[package]]
 name = "yamllint"
-version = "1.35.0"
+version = "1.35.1"
 description = "A linter for YAML files."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"},
-    {file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"},
+    {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
+    {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
 ]
 
 [package.dependencies]
@@ -360,5 +359,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
 
 [metadata]
 lock-version = "2.0"
-python-versions = "^3.8"
-content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece"
+python-versions = "^3.10"
+content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a"
diff --git a/pyproject.toml b/pyproject.toml
index bef41d6266..bb768d5cb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,11 +5,11 @@ description = ""
 authors = []
 
 [tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.10"
 
 [tool.poetry.group.dev.dependencies]
 djlint = "1.34.1"
-yamllint = "1.35.0"
+yamllint = "1.35.1"
 
 [tool.djlint]
 profile="golang"

From 6e2aafd5130cb9436f02209ae90bf79a58cc13ae Mon Sep 17 00:00:00 2001
From: Nanguan Lin <nanguanlin6@gmail.com>
Date: Mon, 4 Mar 2024 00:49:05 +0800
Subject: [PATCH 251/679] Fix 500 when pushing release to an empty repo
 (#29554)

As title.
The former code directly used `ctx.Repo.GitRepo`, causing 500.

https://github.com/go-gitea/gitea/blob/22b4f0c09f1de5e581929bd10f39833d30d2c482/routers/api/v1/repo/release.go#L241
---
 routers/api/v1/repo/release.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go
index a47fc1cc59..f0f3c0bbc7 100644
--- a/routers/api/v1/repo/release.go
+++ b/routers/api/v1/repo/release.go
@@ -4,6 +4,7 @@
 package repo
 
 import (
+	"fmt"
 	"net/http"
 
 	"code.gitea.io/gitea/models"
@@ -215,6 +216,10 @@ func CreateRelease(ctx *context.APIContext) {
 	//   "409":
 	//     "$ref": "#/responses/error"
 	form := web.GetForm(ctx).(*api.CreateReleaseOption)
+	if ctx.Repo.Repository.IsEmpty {
+		ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
+		return
+	}
 	rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
 	if err != nil {
 		if !repo_model.IsErrReleaseNotExist(err) {

From 9616dbec334aacb32c6d73b01fd749b11b1e3cdb Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 4 Mar 2024 03:37:41 +0900
Subject: [PATCH 252/679] Fix workflow trigger event IssueChangeXXX bug
 (#29559)

Bugs from #29308
Follow #29467

partly fix #29558
---
 services/actions/notifier.go | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 1e99c51a8b..aa88d4e0d8 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -171,14 +171,26 @@ func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
 	} else {
 		action = api.HookIssueDemilestoned
 	}
-	notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestMilestone, action)
+
+	hookEvent := webhook_module.HookEventIssueMilestone
+	if issue.IsPull {
+		hookEvent = webhook_module.HookEventPullRequestMilestone
+	}
+
+	notifyIssueChange(ctx, doer, issue, hookEvent, action)
 }
 
 func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
 	_, _ []*issues_model.Label,
 ) {
 	ctx = withMethod(ctx, "IssueChangeLabels")
-	notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestLabel, api.HookIssueLabelUpdated)
+
+	hookEvent := webhook_module.HookEventIssueLabel
+	if issue.IsPull {
+		hookEvent = webhook_module.HookEventPullRequestLabel
+	}
+
+	notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated)
 }
 
 func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {

From 2fb917f69e59f8b75825bf4fe659856b9dd02f44 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 4 Mar 2024 00:24:22 +0000
Subject: [PATCH 253/679] [skip ci] Updated licenses and gitignores

---
 options/license/MIT-Khronos-old | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)
 create mode 100644 options/license/MIT-Khronos-old

diff --git a/options/license/MIT-Khronos-old b/options/license/MIT-Khronos-old
new file mode 100644
index 0000000000..430863bc98
--- /dev/null
+++ b/options/license/MIT-Khronos-old
@@ -0,0 +1,23 @@
+Copyright (c) 2014-2020 The Khronos Group Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and/or associated documentation files (the "Materials"),
+to deal in the Materials without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Materials, and to permit persons to whom the
+Materials are furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Materials.
+
+MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS
+STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND
+HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/
+
+THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS
+IN THE MATERIALS.

From 77e29e0c39392f142627303bd798fb55258072b2 Mon Sep 17 00:00:00 2001
From: 6543 <m.huber@kithara.com>
Date: Mon, 4 Mar 2024 01:37:00 +0100
Subject: [PATCH 254/679] Extend issue template yaml engine (#29274)

Add new option:

`visible`: witch can hide a specific field of the form or the created
content afterwards

It is a string array witch can contain `form` and `content`. If only
`form` is present, it wont show up in the created issue afterwards and
the other way around. By default it sets both except for markdown

As they are optional and github don't have any similar thing, it is non
breaking and also do not conflict with it.

With this you can:
- define "post issue creation" elements like a TODO list to track an
issue state
- make sure to have a checkbox that reminds the user to check for a
thing but dont have it in the created issue afterwards
- define markdown for the created issue (was the downside of using yaml
instead of md in the past)
 - ...

## Demo

```yaml
name: New Contribution
description: External Contributor creating a pull

body:
- type: checkboxes
  id: extern-todo
  visible: [form]
  attributes:
    label: Contribution Guidelines
    options:
      - label: I checked there exist no similar feature to be extended
        required: true
      - label: I did read the CONTRIBUTION.MD
        required: true
- type: checkboxes
  id: intern-todo
  visible: [content]
  attributes:
    label: Maintainer Check-List
    options:
      - label: Does this pull follow the KISS principe
      - label: Checked if internal bord was notifyed
# ....
```
[Demo
Video](https://cloud.obermui.de/s/tm34fSAbJp9qw9z/download/vid-20240220-152751.mkv)


---
*Sponsored by Kithara Software GmbH*

---------

Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
---
 .../issue-pull-request-templates.en-us.md     |  52 ++++++---
 modules/issue/template/template.go            |  69 +++++++++--
 modules/issue/template/template_test.go       | 109 +++++++++++++++---
 modules/issue/template/unmarshal.go           |   9 ++
 modules/structs/issue.go                      |  34 +++++-
 templates/repo/issue/fields/checkboxes.tmpl   |   4 +-
 templates/repo/issue/fields/dropdown.tmpl     |   2 +-
 templates/repo/issue/fields/input.tmpl        |   2 +-
 templates/repo/issue/fields/markdown.tmpl     |   2 +-
 templates/repo/issue/fields/textarea.tmpl     |   2 +-
 templates/swagger/v1_json.tmpl                |  12 ++
 11 files changed, 247 insertions(+), 50 deletions(-)

diff --git a/docs/content/usage/issue-pull-request-templates.en-us.md b/docs/content/usage/issue-pull-request-templates.en-us.md
index b031b262fb..e203c0d379 100644
--- a/docs/content/usage/issue-pull-request-templates.en-us.md
+++ b/docs/content/usage/issue-pull-request-templates.en-us.md
@@ -136,6 +136,12 @@ body:
     attributes:
       value: |
         Thanks for taking the time to fill out this bug report!
+  # some markdown that will only be visible once the issue has been created
+  - type: markdown
+    attributes:
+      value: |
+        This issue was created by an issue **template** :)
+    visible: [content]
   - type: input
     id: contact
     attributes:
@@ -187,11 +193,16 @@ body:
       options:
         - label: I agree to follow this project's Code of Conduct
           required: true
+        - label: I have also read the CONTRIBUTION.MD
+          required: true
+          visible: [form]
+        - label: This is a TODO only visible after issue creation
+          visible: [content]
 ```
 
 ### Markdown
 
-You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted.
+You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted by default.
 
 Attributes:
 
@@ -199,6 +210,8 @@ Attributes:
 |-------|--------------------------------------------------------------|----------|--------|---------|--------------|
 | value | The text that is rendered. Markdown formatting is supported. | Required | String | -       | -            |
 
+visible: Default is **[form]**
+
 ### Textarea
 
 You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields.
@@ -219,6 +232,8 @@ Validations:
 |----------|------------------------------------------------------|----------|---------|---------|--------------|
 | required | Prevents form submission until element is completed. | Optional | Boolean | false   | -            |
 
+visible: Default is **[form, content]**
+
 ### Input
 
 You can use an `input` element to add a single-line text field to your form.
@@ -240,6 +255,8 @@ Validations:
 | is_number | Prevents form submission until element is filled with a number.                                  | Optional | Boolean | false   | -                                                                        |
 | regex     | Prevents form submission until element is filled with a value that match the regular expression. | Optional | String  | -       | a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) |
 
+visible: Default is **[form, content]**
+
 ### Dropdown
 
 You can use a `dropdown` element to add a dropdown menu in your form.
@@ -259,6 +276,8 @@ Validations:
 |----------|------------------------------------------------------|----------|---------|---------|--------------|
 | required | Prevents form submission until element is completed. | Optional | Boolean | false   | -            |
 
+visible: Default is **[form, content]**
+
 ### Checkboxes
 
 You can use the `checkboxes` element to add a set of checkboxes to your form.
@@ -266,17 +285,20 @@ You can use the `checkboxes` element to add a set of checkboxes to your form.
 Attributes:
 
 | Key         | Description                                                                                           | Required | Type   | Default      | Valid values |
-|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------|
+| ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ |
 | label       | A brief description of the expected user input, which is displayed in the form.                       | Required | String | -            | -            |
 | description | A description of the set of checkboxes, which is displayed in the form. Supports Markdown formatting. | Optional | String | Empty String | -            |
 | options     | An array of checkboxes that the user can select. For syntax, see below.                               | Required | Array  | -            | -            |
 
 For each value in the options array, you can set the following keys.
 
-| Key      | Description                                                                                                                              | Required | Type    | Default | Options |
-|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
-| label    | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String  | -       | -       |
-| required | Prevents form submission until element is completed.                                                                                     | Optional | Boolean | false   | -       |
+| Key          | Description                                                                                                                              | Required | Type         | Default | Options |
+|--------------|------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------|---------|---------|
+| label        | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String       | -       | -       |
+| required     | Prevents form submission until element is completed.                                                                                     | Optional | Boolean      | false   | -       |
+| visible      | Whether a specific checkbox appears in the form only, in the created issue only, or both. Valid options are "form" and "content".        | Optional | String array | false   | -       |
+
+visible: Default is **[form, content]**
 
 ## Syntax for issue config
 
@@ -292,15 +314,15 @@ contact_links:
 
 ### Possible Options
 
-| Key                  | Description                                                                                           | Type               | Default        |
-|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------|
-| blank_issues_enabled | If set to false, the User is forced to use a Template                                                 | Boolean            | true           |
-| contact_links        | Custom Links to show in the Choose Box                                                                | Contact Link Array | Empty Array    |
+| Key                  | Description                                           | Type               | Default     |
+|----------------------|-------------------------------------------------------|--------------------|-------------|
+| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean            | true        |
+| contact_links        | Custom Links to show in the Choose Box                | Contact Link Array | Empty Array |
 
 ### Contact Link
 
-| Key                  | Description                                                                                           | Type    | Required |
-|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------|
-| name  | the name of your link                                                                                                | String  | true     |
-| url   | The URL of your Link                                                                                                 | String  | true     |
-| about | A short description of your Link                                                                                     | String  | true     |
+| Key   | Description                      | Type   | Required |
+|-------|----------------------------------|--------|----------|
+| name  | the name of your link            | String | true     |
+| url   | The URL of your Link             | String | true     |
+| about | A short description of your Link | String | true     |
diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go
index 4e813fc91f..3be48b9edc 100644
--- a/modules/issue/template/template.go
+++ b/modules/issue/template/template.go
@@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
 		// The label is not required for a markdown or checkboxes field
 		return nil
 	}
-	return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required")
+	if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil {
+		return err
+	}
+	if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() {
+		return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field")
+	}
+	return nil
 }
 
 func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error {
@@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error {
 				return position.Errorf("'label' is required and should be a string")
 			}
 
+			if visibility, ok := opt["visible"]; ok {
+				visibilityList, ok := visibility.([]any)
+				if !ok {
+					return position.Errorf("'visible' should be list")
+				}
+				for _, visibleType := range visibilityList {
+					visibleType, ok := visibleType.(string)
+					if !ok || !(visibleType == "form" || visibleType == "content") {
+						return position.Errorf("'visible' list can only contain strings of 'form' and 'content'")
+					}
+				}
+			}
+
 			if required, ok := opt["required"]; ok {
 				if _, ok := required.(bool); !ok {
 					return position.Errorf("'required' should be a bool")
 				}
+
+				// validate if hidden field is required
+				if visibility, ok := opt["visible"]; ok {
+					visibilityList, _ := visibility.([]any)
+					isVisible := false
+					for _, v := range visibilityList {
+						if vv, _ := v.(string); vv == "form" {
+							isVisible = true
+							break
+						}
+					}
+					if !isVisible {
+						return position.Errorf("can not require a hidden checkbox")
+					}
+				}
 			}
 		}
 	}
@@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string {
 			IssueFormField: field,
 			Values:         values,
 		}
-		if f.ID == "" {
+		if f.ID == "" || !f.VisibleInContent() {
 			continue
 		}
 		f.WriteTo(builder)
@@ -253,11 +287,6 @@ type valuedField struct {
 }
 
 func (f *valuedField) WriteTo(builder *strings.Builder) {
-	if f.Type == api.IssueFormFieldTypeMarkdown {
-		// markdown blocks do not appear in output
-		return
-	}
-
 	// write label
 	if !f.HideLabel() {
 		_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
@@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
 	switch f.Type {
 	case api.IssueFormFieldTypeCheckboxes:
 		for _, option := range f.Options() {
+			if !option.VisibleInContent() {
+				continue
+			}
 			checked := " "
 			if option.IsChecked() {
 				checked = "x"
@@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
 		} else {
 			_, _ = fmt.Fprintf(builder, "%s\n", value)
 		}
+	case api.IssueFormFieldTypeMarkdown:
+		if value, ok := f.Attributes["value"].(string); ok {
+			_, _ = fmt.Fprintf(builder, "%s\n", value)
+		}
 	}
 	_, _ = fmt.Fprintln(builder)
 }
@@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
 }
 
 func (f *valuedField) HideLabel() bool {
+	if f.Type == api.IssueFormFieldTypeMarkdown {
+		return true
+	}
 	if label, ok := f.Attributes["hide_label"].(bool); ok {
 		return label
 	}
@@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
 	return false
 }
 
+func (o *valuedOption) VisibleInContent() bool {
+	if o.field.Type == api.IssueFormFieldTypeCheckboxes {
+		if vs, ok := o.data.(map[string]any); ok {
+			if vl, ok := vs["visible"].([]any); ok {
+				for _, v := range vl {
+					if vv, _ := v.(string); vv == "content" {
+						return true
+					}
+				}
+				return false
+			}
+		}
+	}
+	return true
+}
+
 var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
 
 // minQuotes return 3 or more back-quotes.
diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go
index 06e6b70d35..e24b962d61 100644
--- a/modules/issue/template/template_test.go
+++ b/modules/issue/template/template_test.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -318,6 +319,42 @@ body:
 `,
 			wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
 		},
+		{
+			name: "field is required but hidden",
+			content: `
+name: "test"
+about: "this is about"
+body:
+  - type: "input"
+    id: "1"
+    attributes:
+      label: "a"
+    validations:
+      required: true
+    visible: [content]
+`,
+			wantErr: "body[0](input): can not require a hidden field",
+		},
+		{
+			name: "checkboxes is required but hidden",
+			content: `
+name: "test"
+about: "this is about"
+body:
+  - type: checkboxes
+    id: "1"
+    attributes:
+      label: Label of checkboxes
+      description: Description of checkboxes
+      options:
+        - label: Option 1
+          required: false
+        - label: Required and hidden
+          required: true
+          visible: [content]
+`,
+			wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
+		},
 		{
 			name: "valid",
 			content: `
@@ -374,8 +411,11 @@ body:
           required: true
         - label: Option 2 of checkboxes
           required: false
-        - label: Option 3 of checkboxes
+        - label: Hidden Option 3 of checkboxes
+          visible: [content]
+        - label: Required but not submitted
           required: true
+          visible: [form]
 `,
 			want: &api.IssueTemplate{
 				Name:   "Name",
@@ -390,6 +430,7 @@ body:
 						Attributes: map[string]any{
 							"value": "Value of the markdown",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 					},
 					{
 						Type: "textarea",
@@ -404,6 +445,7 @@ body:
 						Validations: map[string]any{
 							"required": true,
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 					{
 						Type: "input",
@@ -419,6 +461,7 @@ body:
 							"is_number": true,
 							"regex":     "[a-zA-Z0-9]+",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 					{
 						Type: "dropdown",
@@ -436,6 +479,7 @@ body:
 						Validations: map[string]any{
 							"required": true,
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 					{
 						Type: "checkboxes",
@@ -446,9 +490,11 @@ body:
 							"options": []any{
 								map[string]any{"label": "Option 1 of checkboxes", "required": true},
 								map[string]any{"label": "Option 2 of checkboxes", "required": false},
-								map[string]any{"label": "Option 3 of checkboxes", "required": true},
+								map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
+								map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
 							},
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 				},
 				FileName: "test.yaml",
@@ -467,7 +513,12 @@ body:
   - type: markdown
     id: id1
     attributes:
-      value: Value of the markdown
+      value: Value of the markdown shown in form
+  - type: markdown
+    id: id2
+    attributes:
+      value: Value of the markdown shown in created issue
+    visible: [content]
 `,
 			want: &api.IssueTemplate{
 				Name:   "Name",
@@ -480,8 +531,17 @@ body:
 						Type: "markdown",
 						ID:   "id1",
 						Attributes: map[string]any{
-							"value": "Value of the markdown",
+							"value": "Value of the markdown shown in form",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
+					},
+					{
+						Type: "markdown",
+						ID:   "id2",
+						Attributes: map[string]any{
+							"value": "Value of the markdown shown in created issue",
+						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
 					},
 				},
 				FileName: "test.yaml",
@@ -515,6 +575,7 @@ body:
 						Attributes: map[string]any{
 							"value": "Value of the markdown",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 					},
 				},
 				FileName: "test.yaml",
@@ -548,6 +609,7 @@ body:
 						Attributes: map[string]any{
 							"value": "Value of the markdown",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 					},
 				},
 				FileName: "test.yaml",
@@ -622,9 +684,14 @@ body:
   - type: markdown
     id: id1
     attributes:
-      value: Value of the markdown
-  - type: textarea
+      value: Value of the markdown shown in form
+  - type: markdown
     id: id2
+    attributes:
+      value: Value of the markdown shown in created issue
+    visible: [content]
+  - type: textarea
+    id: id3
     attributes:
       label: Label of textarea
       description: Description of textarea
@@ -634,7 +701,7 @@ body:
     validations:
       required: true
   - type: input
-    id: id3
+    id: id4
     attributes:
       label: Label of input
       description: Description of input
@@ -646,7 +713,7 @@ body:
       is_number: true
       regex: "[a-zA-Z0-9]+"
   - type: dropdown
-    id: id4
+    id: id5
     attributes:
       label: Label of dropdown
       description: Description of dropdown
@@ -658,7 +725,7 @@ body:
     validations:
       required: true
   - type: checkboxes
-    id: id5
+    id: id6
     attributes:
       label: Label of checkboxes
       description: Description of checkboxes
@@ -669,20 +736,26 @@ body:
           required: false
         - label: Option 3 of checkboxes
           required: true
+          visible: [form]
+        - label: Hidden Option of checkboxes
+          visible: [content]
 `,
 				values: map[string][]string{
-					"form-field-id2":   {"Value of id2"},
 					"form-field-id3":   {"Value of id3"},
-					"form-field-id4":   {"0,1"},
-					"form-field-id5-0": {"on"},
-					"form-field-id5-2": {"on"},
+					"form-field-id4":   {"Value of id4"},
+					"form-field-id5":   {"0,1"},
+					"form-field-id6-0": {"on"},
+					"form-field-id6-2": {"on"},
 				},
 			},
-			want: `### Label of textarea
 
-` + "```bash\nValue of id2\n```" + `
+			want: `Value of the markdown shown in created issue
 
-Value of id3
+### Label of textarea
+
+` + "```bash\nValue of id3\n```" + `
+
+Value of id4
 
 ### Label of dropdown
 
@@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
 
 - [x] Option 1 of checkboxes
 - [ ] Option 2 of checkboxes
-- [x] Option 3 of checkboxes
+- [ ] Hidden Option of checkboxes
 
 `,
 		},
@@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown
 				t.Fatal(err)
 			}
 			if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
-				t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
+				assert.EqualValues(t, tt.want, got)
 			}
 		})
 	}
diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go
index 8cae8d4c42..0fc13d7ddf 100644
--- a/modules/issue/template/unmarshal.go
+++ b/modules/issue/template/unmarshal.go
@@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
 			}
 		}
 		for i, v := range it.Fields {
+			// set default id value
 			if v.ID == "" {
 				v.ID = strconv.Itoa(i)
 			}
+			// set default visibility
+			if v.Visible == nil {
+				v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}
+				// markdown is not submitted by default
+				if v.Type != api.IssueFormFieldTypeMarkdown {
+					v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent)
+				}
+			}
 		}
 	}
 
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 34eae69329..16242d18ad 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -6,6 +6,7 @@ package structs
 import (
 	"fmt"
 	"path"
+	"slices"
 	"strings"
 	"time"
 
@@ -141,12 +142,37 @@ const (
 // IssueFormField represents a form field
 // swagger:model
 type IssueFormField struct {
-	Type        IssueFormFieldType `json:"type" yaml:"type"`
-	ID          string             `json:"id" yaml:"id"`
-	Attributes  map[string]any     `json:"attributes" yaml:"attributes"`
-	Validations map[string]any     `json:"validations" yaml:"validations"`
+	Type        IssueFormFieldType      `json:"type" yaml:"type"`
+	ID          string                  `json:"id" yaml:"id"`
+	Attributes  map[string]any          `json:"attributes" yaml:"attributes"`
+	Validations map[string]any          `json:"validations" yaml:"validations"`
+	Visible     []IssueFormFieldVisible `json:"visible,omitempty"`
 }
 
+func (iff IssueFormField) VisibleOnForm() bool {
+	if len(iff.Visible) == 0 {
+		return true
+	}
+	return slices.Contains(iff.Visible, IssueFormFieldVisibleForm)
+}
+
+func (iff IssueFormField) VisibleInContent() bool {
+	if len(iff.Visible) == 0 {
+		// we have our markdown exception
+		return iff.Type != IssueFormFieldTypeMarkdown
+	}
+	return slices.Contains(iff.Visible, IssueFormFieldVisibleContent)
+}
+
+// IssueFormFieldVisible defines issue form field visible
+// swagger:model
+type IssueFormFieldVisible string
+
+const (
+	IssueFormFieldVisibleForm    IssueFormFieldVisible = "form"
+	IssueFormFieldVisibleContent IssueFormFieldVisible = "content"
+)
+
 // IssueTemplate represents an issue template for a repository
 // swagger:model
 type IssueTemplate struct {
diff --git a/templates/repo/issue/fields/checkboxes.tmpl b/templates/repo/issue/fields/checkboxes.tmpl
index 237f2eb5dd..b928b2be58 100644
--- a/templates/repo/issue/fields/checkboxes.tmpl
+++ b/templates/repo/issue/fields/checkboxes.tmpl
@@ -1,8 +1,8 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	{{range $i, $opt := .item.Attributes.options}}
 		<div class="field inline">
-			<div class="ui checkbox gt-mr-0">
+			<div class="ui checkbox gt-mr-0 {{if and ($opt.visible) (not (SliceUtils.Contains $opt.visible "form"))}}gt-hidden{{end}}">
 				<input type="checkbox" name="form-field-{{$.item.ID}}-{{$i}}" {{if $opt.required}}required{{end}}>
 				<label>{{RenderMarkdownToHtml $.context $opt.label}}</label>
 			</div>
diff --git a/templates/repo/issue/fields/dropdown.tmpl b/templates/repo/issue/fields/dropdown.tmpl
index 23aa373cd2..b8df6908e3 100644
--- a/templates/repo/issue/fields/dropdown.tmpl
+++ b/templates/repo/issue/fields/dropdown.tmpl
@@ -1,4 +1,4 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	{{/* FIXME: required validation */}}
 	<div class="ui fluid selection dropdown {{if .item.Attributes.multiple}}multiple clearable{{end}}">
diff --git a/templates/repo/issue/fields/input.tmpl b/templates/repo/issue/fields/input.tmpl
index 3fc8a86510..ad0fe3d783 100644
--- a/templates/repo/issue/fields/input.tmpl
+++ b/templates/repo/issue/fields/input.tmpl
@@ -1,4 +1,4 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	<input type="{{if .item.Validations.is_number}}number{{else}}text{{end}}" name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" value="{{.item.Attributes.value}}" {{if .item.Validations.required}}required{{end}} {{if .item.Validations.regex}}pattern="{{.item.Validations.regex}}" title="{{.item.Validations.regex}}"{{end}}>
 </div>
diff --git a/templates/repo/issue/fields/markdown.tmpl b/templates/repo/issue/fields/markdown.tmpl
index fd5b6afd22..97813cc1d8 100644
--- a/templates/repo/issue/fields/markdown.tmpl
+++ b/templates/repo/issue/fields/markdown.tmpl
@@ -1,3 +1,3 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
 	<div>{{RenderMarkdownToHtml .Context .item.Attributes.value}}</div>
 </div>
diff --git a/templates/repo/issue/fields/textarea.tmpl b/templates/repo/issue/fields/textarea.tmpl
index 55adeb28d0..4f68b4038b 100644
--- a/templates/repo/issue/fields/textarea.tmpl
+++ b/templates/repo/issue/fields/textarea.tmpl
@@ -1,5 +1,5 @@
 {{$useMarkdownEditor := not .item.Attributes.render}}
-<div class="field {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
+<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}} {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
 	{{template "repo/issue/fields/header" .}}
 
 	{{/* the real form element to provide the value */}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index b739bea60d..fa7cd60eb3 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -20623,6 +20623,13 @@
           "type": "object",
           "additionalProperties": {},
           "x-go-name": "Validations"
+        },
+        "visible": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/IssueFormFieldVisible"
+          },
+          "x-go-name": "Visible"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
@@ -20632,6 +20639,11 @@
       "title": "IssueFormFieldType defines issue form field type, can be \"markdown\", \"textarea\", \"input\", \"dropdown\" or \"checkboxes\"",
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "IssueFormFieldVisible": {
+      "description": "IssueFormFieldVisible defines issue form field visible",
+      "type": "string",
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "IssueLabelsOption": {
       "description": "IssueLabelsOption a collection of labels",
       "type": "object",

From 8553b4600e3035b6f6ad6907c37cebd013fa4d64 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 4 Mar 2024 09:02:51 +0800
Subject: [PATCH 255/679] Add an trailing slash to dashboard links (#29555)

Fix #29533, and add some tests for "base/paginate.tmpl"
---
 modules/translation/mock.go           |  4 +++-
 modules/translation/translation.go    |  2 +-
 routers/web/user/home_test.go         | 17 +++++++++++++++++
 services/contexttest/context_tests.go |  4 +++-
 templates/base/paginate.tmpl          | 14 ++++++++------
 5 files changed, 32 insertions(+), 9 deletions(-)

diff --git a/modules/translation/mock.go b/modules/translation/mock.go
index 1f0559f38d..18fbc1044a 100644
--- a/modules/translation/mock.go
+++ b/modules/translation/mock.go
@@ -9,7 +9,9 @@ import (
 )
 
 // MockLocale provides a mocked locale without any translations
-type MockLocale struct{}
+type MockLocale struct {
+	Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
+}
 
 var _ Locale = (*MockLocale)(nil)
 
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index b7c18f610a..36ae58a9f1 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -144,7 +144,7 @@ func Match(tags ...language.Tag) language.Tag {
 // locale represents the information of localization.
 type locale struct {
 	i18n.Locale
-	Lang, LangName string // these fields are used directly in templates: .i18n.Lang
+	Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
 	msgPrinter     *message.Printer
 }
 
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index 3f5fd26689..1cc9886308 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -11,6 +11,8 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
@@ -113,3 +115,18 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
 	assert.Len(t, ctx.Data["Milestones"], 1)
 	assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
 }
+
+func TestDashboardPagination(t *testing.T) {
+	ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	page := context.NewPagination(10, 3, 1, 3)
+
+	setting.AppSubURL = "/SubPath"
+	out, err := ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
+	assert.NoError(t, err)
+	assert.Contains(t, out, `<a class=" item navigation" href="/SubPath/?page=2">`)
+
+	setting.AppSubURL = ""
+	out, err = ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
+	assert.NoError(t, err)
+	assert.Contains(t, out, `<a class=" item navigation" href="/?page=2">`)
+}
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index 8a7dd69a0f..431017a30d 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -12,6 +12,7 @@ import (
 	"net/url"
 	"strings"
 	"testing"
+	"time"
 
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -61,7 +62,8 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
 	base.Locale = &translation.MockLocale{}
 
 	ctx := context.NewWebContext(base, opt.Render, nil)
-
+	ctx.PageData = map[string]any{}
+	ctx.Data["PageStartTime"] = time.Now()
 	chiCtx := chi.NewRouteContext()
 	ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
 	return ctx, resp
diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl
index 503817c339..ef7d0b341b 100644
--- a/templates/base/paginate.tmpl
+++ b/templates/base/paginate.tmpl
@@ -1,13 +1,15 @@
-{{$paginationLink := .Page.GetParams}}
+{{$paginationParams := .Page.GetParams}}
+{{$paginationLink := $.Link}}
+{{if eq $paginationLink AppSubUrl}}{{$paginationLink = print $paginationLink "/"}}{{end}}
 {{with .Page.Paginater}}
 	{{if gt .TotalPages 1}}
 		<div class="center page buttons">
 			<div class="ui borderless pagination menu">
-				<a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$.Link}}{{if $paginationLink}}?{{$paginationLink}}{{end}}"{{end}}>
+				<a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$paginationLink}}{{if $paginationParams}}?{{$paginationParams}}{{end}}"{{end}}>
 					{{svg "gitea-double-chevron-left" 16 "gt-mr-2"}}
 					<span class="navigation_label">{{ctx.Locale.Tr "admin.first_page"}}</span>
 				</a>
-				<a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$.Link}}?page={{.Previous}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>
+				<a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$paginationLink}}?page={{.Previous}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
 					{{svg "octicon-chevron-left" 16 "gt-mr-2"}}
 					<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.previous"}}</span>
 				</a>
@@ -15,14 +17,14 @@
 					{{if eq .Num -1}}
 						<a class="disabled item">...</a>
 					{{else}}
-						<a class="{{if .IsCurrent}}active {{end}}item gt-content-center" {{if not .IsCurrent}}href="{{$.Link}}?page={{.Num}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>{{.Num}}</a>
+						<a class="{{if .IsCurrent}}active {{end}}item gt-content-center" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a>
 					{{end}}
 				{{end}}
-				<a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$.Link}}?page={{.Next}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>
+				<a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$paginationLink}}?page={{.Next}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
 					<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.next"}}</span>
 					{{svg "octicon-chevron-right" 16 "gt-ml-2"}}
 				</a>
-				<a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$.Link}}?page={{.TotalPages}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>
+				<a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$paginationLink}}?page={{.TotalPages}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
 					<span class="navigation_label">{{ctx.Locale.Tr "admin.last_page"}}</span>
 					{{svg "gitea-double-chevron-right" 16 "gt-ml-2"}}
 				</a>

From fe6792dff3d167e87b0c4476f7e7a7ce15742855 Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Mon, 4 Mar 2024 03:56:52 +0100
Subject: [PATCH 256/679] Enable/disable owner and repo projects independently
 (#28805)

Part of #23318

Add menu in repo settings to allow for repo admin to decide not just if
projects are enabled or disabled per repo, but also which kind of
projects (repo-level/owner-level) are enabled. If repo projects
disabled, don't show the projects tab.


![grafik](https://github.com/go-gitea/gitea/assets/47871822/b9b43fb4-824b-47f9-b8e2-12004313647c)

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 models/fixtures/repo_unit.yml        |  7 +--
 models/repo/repo.go                  |  5 ++
 models/repo/repo_unit.go             | 56 ++++++++++++++++-
 modules/repository/create.go         |  6 ++
 modules/structs/repo.go              |  3 +
 options/locale/locale_en-US.ini      |  6 +-
 routers/api/v1/repo/repo.go          | 26 +++++++-
 routers/web/repo/issue.go            | 93 ++++++++++++++++------------
 routers/web/repo/projects.go         | 15 ++---
 routers/web/repo/setting/setting.go  |  3 +
 routers/web/web.go                   |  2 +-
 services/convert/repository.go       |  6 +-
 services/forms/repo_form.go          |  1 +
 templates/repo/header.tmpl           |  3 +-
 templates/repo/settings/options.tmpl | 34 +++++++++-
 templates/swagger/v1_json.tmpl       |  9 +++
 16 files changed, 212 insertions(+), 63 deletions(-)

diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index 4b26674990..6714294e2b 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -520,6 +520,7 @@
   id: 75
   repo_id: 1
   type: 8
+  config: "{\"ProjectsMode\":\"all\"}"
   created_unix: 946684810
 
 -
@@ -650,12 +651,6 @@
   type: 2
   created_unix: 946684810
 
--
-  id: 98
-  repo_id: 1
-  type: 8
-  created_unix: 946684810
-
 -
   id: 99
   repo_id: 1
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 5ce3ecb58a..ad2e21b66b 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -411,6 +411,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
 			Type:   tp,
 			Config: new(ActionsConfig),
 		}
+	} else if tp == unit.TypeProjects {
+		return &RepoUnit{
+			Type:   tp,
+			Config: new(ProjectsConfig),
+		}
 	}
 
 	return &RepoUnit{
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 31a2a2e248..6b9dde7faf 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -202,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
 	return json.Marshal(cfg)
 }
 
+// ProjectsMode represents the projects enabled for a repository
+type ProjectsMode string
+
+const (
+	// ProjectsModeRepo allows only repo-level projects
+	ProjectsModeRepo ProjectsMode = "repo"
+	// ProjectsModeOwner allows only owner-level projects
+	ProjectsModeOwner ProjectsMode = "owner"
+	// ProjectsModeAll allows both kinds of projects
+	ProjectsModeAll ProjectsMode = "all"
+	// ProjectsModeNone doesn't allow projects
+	ProjectsModeNone ProjectsMode = "none"
+)
+
+// ProjectsConfig describes projects config
+type ProjectsConfig struct {
+	ProjectsMode ProjectsMode
+}
+
+// FromDB fills up a ProjectsConfig from serialized format.
+func (cfg *ProjectsConfig) FromDB(bs []byte) error {
+	return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+}
+
+// ToDB exports a ProjectsConfig to a serialized format.
+func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
+	return json.Marshal(cfg)
+}
+
+func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
+	if cfg.ProjectsMode != "" {
+		return cfg.ProjectsMode
+	}
+
+	return ProjectsModeNone
+}
+
+func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
+	projectsMode := cfg.GetProjectsMode()
+
+	if m == ProjectsModeNone {
+		return true
+	}
+
+	return projectsMode == m || projectsMode == ProjectsModeAll
+}
+
 // BeforeSet is invoked from XORM before setting the value of a field of this object.
 func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
 	switch colName {
@@ -217,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
 			r.Config = new(IssuesConfig)
 		case unit.TypeActions:
 			r.Config = new(ActionsConfig)
-		case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages:
+		case unit.TypeProjects:
+			r.Config = new(ProjectsConfig)
+		case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
 			fallthrough
 		default:
 			r.Config = new(UnitConfig)
@@ -265,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig {
 	return r.Config.(*ActionsConfig)
 }
 
+// ProjectsConfig returns config for unit.ProjectsConfig
+func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
+	return r.Config.(*ProjectsConfig)
+}
+
 func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
 	var tmpUnits []*RepoUnit
 	if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
diff --git a/modules/repository/create.go b/modules/repository/create.go
index ca2150b972..f009c0880d 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
 					AllowRebaseUpdate: true,
 				},
 			})
+		} else if tp == unit.TypeProjects {
+			units = append(units, repo_model.RepoUnit{
+				RepoID: repo.ID,
+				Type:   tp,
+				Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
+			})
 		} else {
 			units = append(units, repo_model.RepoUnit{
 				RepoID: repo.ID,
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 56d6158bd8..bc8eb0b756 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -90,6 +90,7 @@ type Repository struct {
 	ExternalWiki                  *ExternalWiki    `json:"external_wiki,omitempty"`
 	HasPullRequests               bool             `json:"has_pull_requests"`
 	HasProjects                   bool             `json:"has_projects"`
+	ProjectsMode                  string           `json:"projects_mode"`
 	HasReleases                   bool             `json:"has_releases"`
 	HasPackages                   bool             `json:"has_packages"`
 	HasActions                    bool             `json:"has_actions"`
@@ -180,6 +181,8 @@ type EditRepoOption struct {
 	HasPullRequests *bool `json:"has_pull_requests,omitempty"`
 	// either `true` to enable project unit, or `false` to disable them.
 	HasProjects *bool `json:"has_projects,omitempty"`
+	// `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
+	ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
 	// either `true` to enable releases unit, or `false` to disable them.
 	HasReleases *bool `json:"has_releases,omitempty"`
 	// either `true` to enable packages unit, or `false` to disable them.
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 8c4dae753b..c8c8f2dfeb 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2090,7 +2090,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af
 settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
 settings.releases_desc = Enable Repository Releases
 settings.packages_desc = Enable Repository Packages Registry
-settings.projects_desc = Enable Repository Projects
+settings.projects_desc = Enable Projects
+settings.projects_mode_desc = Projects Mode (which kinds of projects to show)
+settings.projects_mode_repo = Repo projects only
+settings.projects_mode_owner = Only user or org projects
+settings.projects_mode_all = All projects
 settings.actions_desc = Enable Repository Actions
 settings.admin_settings = Administrator Settings
 settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 6fde73a4e8..5f1af92041 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -944,13 +944,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 		}
 	}
 
-	if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
-		if *opts.HasProjects {
+	currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
+	newHasProjects := currHasProjects
+	if opts.HasProjects != nil {
+		newHasProjects = *opts.HasProjects
+	}
+	if currHasProjects || newHasProjects {
+		if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
+			unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
+			var config *repo_model.ProjectsConfig
+			if err != nil {
+				config = &repo_model.ProjectsConfig{
+					ProjectsMode: repo_model.ProjectsModeAll,
+				}
+			} else {
+				config = unit.ProjectsConfig()
+			}
+
+			if opts.ProjectsMode != nil {
+				config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
+			}
+
 			units = append(units, repo_model.RepoUnit{
 				RepoID: repo.ID,
 				Type:   unit_model.TypeProjects,
+				Config: config,
 			})
-		} else {
+		} else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
 			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
 		}
 	}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 1abd5e2ba5..b8c7f70aa6 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -587,52 +587,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 	if repo.Owner.IsOrganization() {
 		repoOwnerType = project_model.TypeOrganization
 	}
+
+	projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
+
+	var openProjects []*project_model.Project
+	var closedProjects []*project_model.Project
 	var err error
-	projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		RepoID:      repo.ID,
-		IsClosed:    optional.Some(false),
-		Type:        project_model.TypeRepository,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
-	}
-	projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		OwnerID:     repo.OwnerID,
-		IsClosed:    optional.Some(false),
-		Type:        repoOwnerType,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
+
+	if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+		openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			RepoID:      repo.ID,
+			IsClosed:    optional.Some(false),
+			Type:        project_model.TypeRepository,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
+		closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			RepoID:      repo.ID,
+			IsClosed:    optional.Some(true),
+			Type:        project_model.TypeRepository,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
 	}
 
-	ctx.Data["OpenProjects"] = append(projects, projects2...)
-
-	projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		RepoID:      repo.ID,
-		IsClosed:    optional.Some(true),
-		Type:        project_model.TypeRepository,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
-	}
-	projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		OwnerID:     repo.OwnerID,
-		IsClosed:    optional.Some(true),
-		Type:        repoOwnerType,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
+	if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
+		openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			OwnerID:     repo.OwnerID,
+			IsClosed:    optional.Some(false),
+			Type:        repoOwnerType,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
+		openProjects = append(openProjects, openProjects2...)
+		closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			OwnerID:     repo.OwnerID,
+			IsClosed:    optional.Some(true),
+			Type:        repoOwnerType,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
+		closedProjects = append(closedProjects, closedProjects2...)
 	}
 
-	ctx.Data["ClosedProjects"] = append(projects, projects2...)
+	ctx.Data["OpenProjects"] = openProjects
+	ctx.Data["ClosedProjects"] = closedProjects
 }
 
 // repoReviewerSelection items to bee shown
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 4c171defbd..86909b5fd0 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -14,7 +14,7 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/perm"
 	project_model "code.gitea.io/gitea/models/project"
-	attachment_model "code.gitea.io/gitea/models/repo"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/json"
@@ -33,16 +33,17 @@ const (
 	tplProjectsView base.TplName = "repo/projects/view"
 )
 
-// MustEnableProjects check if projects are enabled in settings
-func MustEnableProjects(ctx *context.Context) {
+// MustEnableRepoProjects check if repo projects are enabled in settings
+func MustEnableRepoProjects(ctx *context.Context) {
 	if unit.TypeProjects.UnitGlobalDisabled() {
 		ctx.NotFound("EnableKanbanBoard", nil)
 		return
 	}
 
 	if ctx.Repo.Repository != nil {
-		if !ctx.Repo.CanRead(unit.TypeProjects) {
-			ctx.NotFound("MustEnableProjects", nil)
+		projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
+		if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+			ctx.NotFound("MustEnableRepoProjects", nil)
 			return
 		}
 	}
@@ -325,10 +326,10 @@ func ViewProject(ctx *context.Context) {
 	}
 
 	if project.CardType != project_model.CardTypeTextOnly {
-		issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
+		issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
 		for _, issuesList := range issuesMap {
 			for _, issue := range issuesList {
-				if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
+				if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
 					issuesAttachmentMap[issue.ID] = issueAttachment
 				}
 			}
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 0f649acba3..3af0ddb578 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -533,6 +533,9 @@ func SettingsPost(ctx *context.Context) {
 			units = append(units, repo_model.RepoUnit{
 				RepoID: repo.ID,
 				Type:   unit_model.TypeProjects,
+				Config: &repo_model.ProjectsConfig{
+					ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+				},
 			})
 		} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
 			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
diff --git a/routers/web/web.go b/routers/web/web.go
index 9de652fba5..14d31b3a90 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1344,7 +1344,7 @@ func registerRoutes(m *web.Route) {
 					})
 				})
 			}, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
-		}, reqRepoProjectsReader, repo.MustEnableProjects)
+		}, reqRepoProjectsReader, repo.MustEnableRepoProjects)
 
 		m.Group("/actions", func() {
 			m.Get("", actions.List)
diff --git a/services/convert/repository.go b/services/convert/repository.go
index 9184bc05c7..39efd304a9 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -113,8 +113,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
 	}
 	hasProjects := false
-	if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
+	projectsMode := repo_model.ProjectsModeAll
+	if unit, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
 		hasProjects = true
+		config := unit.ProjectsConfig()
+		projectsMode = config.ProjectsMode
 	}
 
 	hasReleases := false
@@ -211,6 +214,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		InternalTracker:               internalTracker,
 		HasWiki:                       hasWiki,
 		HasProjects:                   hasProjects,
+		ProjectsMode:                  string(projectsMode),
 		HasReleases:                   hasReleases,
 		HasPackages:                   hasPackages,
 		HasActions:                    hasActions,
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index e40bcf4eea..8c3e458d2f 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -142,6 +142,7 @@ type RepoSettingForm struct {
 	ExternalTrackerRegexpPattern          string
 	EnableCloseIssuesViaCommitInAnyBranch bool
 	EnableProjects                        bool
+	ProjectsMode                          string
 	EnableReleases                        bool
 	EnablePackages                        bool
 	EnablePulls                           bool
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index ee46af4236..b692c851ee 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -174,7 +174,8 @@
 					</a>
 				{{end}}
 
-				{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
+				{{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
+				{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
 					<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
 						{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}}
 						{{if .Repository.NumOpenProjects}}
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 6d01b227ff..5a85192a43 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -446,13 +446,45 @@
 
 				{{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
 				{{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
+				{{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
 				<div class="inline field">
 					<label>{{ctx.Locale.Tr "repo.project_board"}}</label>
 					<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
-						<input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}>
+						<input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
 						<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
 					</div>
 				</div>
+				<div class="field {{if not $isProjectsEnabled}} disabled{{end}} gt-pl-4" id="projects_box">
+					<p>
+						{{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
+					</p>
+					<div class="ui dropdown selection">
+						<select name="projects_mode">
+							<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
+							<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
+							<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
+						</select>
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="default text">
+							{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}
+								{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
+							{{end}}
+							{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}
+								{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
+							{{end}}
+							{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}
+								{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
+							{{end}}
+						</div>
+						<div class="menu">
+							<div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div>
+							<div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div>
+							<div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div>
+						</div>
+					</div>
+				</div>
+
+				<div class="divider"></div>
 
 				{{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}}
 				{{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index fa7cd60eb3..9aba84a023 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -19570,6 +19570,11 @@
           "type": "boolean",
           "x-go-name": "Private"
         },
+        "projects_mode": {
+          "description": "`repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.",
+          "type": "string",
+          "x-go-name": "ProjectsMode"
+        },
         "template": {
           "description": "either `true` to make this repository a template or `false` to make it a normal repository",
           "type": "boolean",
@@ -22491,6 +22496,10 @@
           "type": "boolean",
           "x-go-name": "Private"
         },
+        "projects_mode": {
+          "type": "string",
+          "x-go-name": "ProjectsMode"
+        },
         "release_counter": {
           "type": "integer",
           "format": "int64",

From a2e90014ec20a1085449a66061389cfe0d12260f Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 4 Mar 2024 04:33:20 +0100
Subject: [PATCH 257/679] Replace some `gt-` classes with `tw-` (#29570)

Replace 18 `gt-` prefixes with `tw-` with perl replacement. I manually
checked them all with `rg` afterwards.
---
 templates/admin/notice.tmpl                   |  2 +-
 templates/org/header.tmpl                     |  2 +-
 templates/package/view.tmpl                   |  2 +-
 templates/projects/list.tmpl                  |  2 +-
 templates/projects/new.tmpl                   |  2 +-
 templates/projects/view.tmpl                  |  4 ++--
 templates/repo/branch/list.tmpl               |  2 +-
 templates/repo/commit_page.tmpl               |  2 +-
 templates/repo/commits_list_small.tmpl        |  2 +-
 templates/repo/commits_table.tmpl             |  2 +-
 templates/repo/diff/box.tmpl                  |  4 ++--
 templates/repo/diff/comment_form.tmpl         |  2 +-
 templates/repo/diff/compare.tmpl              |  2 +-
 templates/repo/empty.tmpl                     |  2 +-
 templates/repo/header.tmpl                    |  2 +-
 templates/repo/home.tmpl                      |  4 ++--
 templates/repo/issue/list.tmpl                |  2 +-
 templates/repo/issue/milestone_new.tmpl       |  2 +-
 templates/repo/issue/view_content.tmpl        |  4 ++--
 .../repo/issue/view_content/comments.tmpl     |  2 +-
 .../view_content/comments_delete_time.tmpl    |  2 +-
 templates/repo/migrate/migrate.tmpl           |  4 ++--
 templates/repo/release/new.tmpl               |  2 +-
 templates/repo/settings/branches.tmpl         |  2 +-
 templates/repo/settings/githooks.tmpl         |  2 +-
 templates/repo/settings/lfs_pointers.tmpl     |  2 +-
 templates/repo/settings/options.tmpl          |  2 +-
 templates/repo/settings/tags.tmpl             |  2 +-
 templates/repo/unicode_escape_prompt.tmpl     |  4 ++--
 templates/repo/view_file.tmpl                 |  4 ++--
 templates/repo/wiki/view.tmpl                 |  6 +++---
 templates/status/500.tmpl                     |  2 +-
 templates/user/auth/captcha.tmpl              |  2 +-
 templates/user/dashboard/issues.tmpl          |  2 +-
 .../user/notification/notification_div.tmpl   |  2 +-
 templates/user/settings/applications.tmpl     |  4 ++--
 templates/user/settings/repos.tmpl            |  2 +-
 web_src/css/helpers.css                       | 20 -------------------
 .../js/components/RepoBranchTagSelector.vue   |  2 +-
 web_src/js/features/common-global.js          |  2 +-
 web_src/js/modules/fomantic.js                |  2 +-
 41 files changed, 50 insertions(+), 70 deletions(-)

diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index ed410425b5..e0abe4f8c0 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -31,7 +31,7 @@
 						<tr>
 							<th></th>
 							<th colspan="5">
-								<form class="gt-float-right" method="post" action="{{AppSubUrl}}/admin/notices/empty">
+								<form class="tw-float-right" method="post" action="{{AppSubUrl}}/admin/notices/empty">
 									{{.CsrfTokenHtml}}
 									<button type="submit" class="ui red small button">{{ctx.Locale.Tr "admin.notices.delete_all"}}</button>
 								</form>
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index efbbc43b1d..943557b1ca 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -7,7 +7,7 @@
 				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
 				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
 			</span>
-			<span class="gt-df gt-ac gt-gap-2 gt-ml-auto gt-font-16 gt-whitespace-nowrap">
+			<span class="gt-df gt-ac gt-gap-2 gt-ml-auto gt-font-16 tw-whitespace-nowrap">
 				{{if .EnableFeed}}
 					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
 						{{svg "octicon-rss" 24}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 0fa23d67fd..54af71126f 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -87,7 +87,7 @@
 				{{end}}
 				<div class="divider"></div>
 				<strong>{{ctx.Locale.Tr "packages.versions"}} ({{.TotalVersionCount}})</strong>
-				<a class="gt-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a>
+				<a class="tw-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a>
 				<div class="ui relaxed list">
 				{{range .LatestVersions}}
 					<div class="item gt-df">
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index 30fbd498a4..54a41221bf 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -10,7 +10,7 @@
 				{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 			</a>
 		</div>
-		<div class="gt-text-right">
+		<div class="tw-text-right">
 			<a class="ui small primary button" href="{{$.Link}}/new">{{ctx.Locale.Tr "repo.projects.new"}}</a>
 		</div>
 	</div>
diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl
index 711dbe842a..92ee36c1c4 100644
--- a/templates/projects/new.tmpl
+++ b/templates/projects/new.tmpl
@@ -55,7 +55,7 @@
 		</div>
 	</div>
 	<div class="divider"></div>
-	<div class="gt-text-right">
+	<div class="tw-text-right">
 		<a class="ui cancel button" href="{{$.CancelLink}}">
 			{{ctx.Locale.Tr "repo.milestones.cancel"}}
 		</a>
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 3792ccca0e..1d03477a9f 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -165,9 +165,9 @@
 
 				<div class="divider"></div>
 
-				<div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
+				<div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
 					{{range (index $.IssuesMap .ID)}}
-						<div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}">
+						<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
 							{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
 						</div>
 					{{end}}
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 46503cb5df..916111faca 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -72,7 +72,7 @@
 				<div class="gt-df gt-ac">
 					{{ctx.Locale.Tr "repo.branches"}}
 				</div>
-				<div class="gt-whitespace-nowrap">
+				<div class="tw-whitespace-nowrap">
 					<form class="ignore-dirty" method="get">
 						<div class="ui tiny search input">
 							<input name="q" placeholder="{{ctx.Locale.Tr "repo.branch.search"}}" value="{{.Keyword}}" autofocus>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 7892a57163..1d900cdefb 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -184,7 +184,7 @@
 				</div>
 		</div>
 		{{if .Commit.Signature}}
-			<div class="ui bottom attached message gt-text-left gt-df gt-ac gt-sb commit-header-row gt-fw gt-mb-0 {{$class}}">
+			<div class="ui bottom attached message tw-text-left gt-df gt-ac gt-sb commit-header-row gt-fw gt-mb-0 {{$class}}">
 				<div class="gt-df gt-ac">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index 79e1bd6309..86e6b7225e 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -13,7 +13,7 @@
 
 		{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
 
-		<span class="shabox gt-df gt-ac gt-float-right">
+		<span class="shabox gt-df gt-ac tw-float-right">
 			{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
 			{{$class := "ui sha label"}}
 			{{if .Signature}}
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index 054a3f6bec..70f673e27e 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -8,7 +8,7 @@
 			{{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}}
 		{{end}}
 	</div>
-	<div class="commits-table-right gt-whitespace-nowrap">
+	<div class="commits-table-right tw-whitespace-nowrap">
 		{{if .PageIsCommits}}
 			<form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search">
 				<div class="ui tiny search input">
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index c24500a149..39cf8755f2 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -25,7 +25,7 @@
 		</div>
 		<div class="diff-detail-actions">
 			{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}}
-				<div class="not-mobile gt-df gt-ac gt-fc gt-whitespace-nowrap gt-mr-2">
+				<div class="not-mobile gt-df gt-ac gt-fc tw-whitespace-nowrap gt-mr-2">
 					<label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{ctx.Locale.Tr "repo.pulls.viewed_files_label"}}">
 						{{ctx.Locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
 					</label>
@@ -202,7 +202,7 @@
 							</div>
 							{{if $showFileViewToggle}}
 								{{/* for image or CSV, it can have a horizontal scroll bar, there won't be review comment context menu (position absolute) which would be clipped by "overflow" */}}
-								<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} gt-overflow-x-scroll">
+								<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} tw-overflow-x-scroll">
 									<table class="chroma tw-w-full">
 										{{if $isImage}}
 											{{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}}
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl
index 54817d4740..6005ea28ef 100644
--- a/templates/repo/diff/comment_form.tmpl
+++ b/templates/repo/diff/comment_form.tmpl
@@ -27,7 +27,7 @@
 
 		<div class="field footer gt-mx-3">
 			<span class="markup-info">{{svg "octicon-markup"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
-			<div class="gt-text-right">
+			<div class="tw-text-right">
 				{{if $.reply}}
 					<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>
 					<input type="hidden" name="reply" value="{{$.reply}}">
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index e460d4da3b..970e29b476 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -208,7 +208,7 @@
 					<button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button>
 				</div>
 			{{else if .Repository.IsArchived}}
-				<div class="ui warning message gt-text-center">
+				<div class="ui warning message tw-text-center">
 					{{if .Repository.ArchivedUnix.IsZero}}
 						{{ctx.Locale.Tr "repo.archive.title"}}
 					{{else}}
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index f171cd8d5c..a858a728e9 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -6,7 +6,7 @@
 			<div class="sixteen wide column content">
 				{{template "base/alert" .}}
 				{{if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{if .Repository.ArchivedUnix.IsZero}}
 							{{ctx.Locale.Tr "repo.archive.title"}}
 						{{else}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index b692c851ee..0f64380337 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -93,7 +93,7 @@
 								<div class="header">
 									{{ctx.Locale.Tr "repo.already_forked" .Name}}
 								</div>
-								<div class="content gt-text-left">
+								<div class="content tw-text-left">
 									<div class="ui list">
 										{{range $.UserAndOrgForks}}
 											<div class="ui item gt-py-3">
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 2c08fb02d5..a98dcb8c28 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -41,7 +41,7 @@
 					<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
 					{{range .Topics}}
 						{{/* keey the same layout as Fomantic UI generated labels */}}
-						<a class="ui label transition visible gt-cursor-default gt-dib" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
+						<a class="ui label transition visible tw-cursor-default gt-dib" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
 					{{end}}
 					<div class="text"></div>
 				</div>
@@ -53,7 +53,7 @@
 		</div>
 		{{end}}
 		{{if .Repository.IsArchived}}
-			<div class="ui warning message gt-text-center">
+			<div class="ui warning message tw-text-center">
 				{{if .Repository.ArchivedUnix.IsZero}}
 					{{ctx.Locale.Tr "repo.archive.title"}}
 				{{else}}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 012b613fbf..62c1d00f00 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -6,7 +6,7 @@
 	{{if .PinnedIssues}}
 		<div id="issue-pins" {{if .IsRepoAdmin}}data-is-repo-admin{{end}}>
 			{{range .PinnedIssues}}
-				<div class="issue-card gt-word-break {{if $.IsRepoAdmin}}gt-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}">
+				<div class="issue-card gt-word-break {{if $.IsRepoAdmin}}tw-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}">
 					{{template "repo/issue/card" (dict "Issue" . "Page" $ "isPinnedIssueCard" true)}}
 				</div>
 			{{end}}
diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl
index 3e79ee7ee9..7a56d73ac9 100644
--- a/templates/repo/issue/milestone_new.tmpl
+++ b/templates/repo/issue/milestone_new.tmpl
@@ -39,7 +39,7 @@
 					<textarea name="content">{{.content}}</textarea>
 				</div>
 				<div class="divider"></div>
-				<div class="gt-text-right">
+				<div class="tw-text-right">
 					{{if .PageIsEditMilestone}}
 						<a class="ui primary basic button" href="{{.RepoLink}}/milestones">
 							{{ctx.Locale.Tr "repo.milestones.cancel"}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index aa91764be4..747132931e 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -114,7 +114,7 @@
 					</div>
 				</div>
 				{{else if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{if .Issue.IsPull}}
 							{{ctx.Locale.Tr "repo.archive.pull.nocomment"}}
 						{{else}}
@@ -124,7 +124,7 @@
 				{{end}}
 			{{else}} {{/* not .IsSigned */}}
 				{{if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{if .Issue.IsPull}}
 							{{ctx.Locale.Tr "repo.archive.pull.nocomment"}}
 						{{else}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index b500cec91c..cb4950c18e 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -562,7 +562,7 @@
 					{{end}}
 				</span>
 				{{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}}
-				<span class="gt-float-right comparebox">
+				<span class="tw-float-right comparebox">
 					<a href="{{$.Issue.PullRequest.BaseRepo.Link}}/compare/{{PathEscape .OldCommit}}..{{PathEscape .NewCommit}}" rel="nofollow" class="ui compare label">{{ctx.Locale.Tr "repo.issues.force_push_compare"}}</a>
 				</span>
 				{{end}}
diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl
index 95121b0dc7..2377e7c4f0 100644
--- a/templates/repo/issue/view_content/comments_delete_time.tmpl
+++ b/templates/repo/issue/view_content/comments_delete_time.tmpl
@@ -1,7 +1,7 @@
 {{if and .comment.Time (.ctxData.Repository.IsTimetrackerEnabled ctx)}} {{/* compatibility with time comments made before v1.14 */}}
 	{{if (not .comment.Time.Deleted)}}
 		{{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}}
-			<span class="gt-float-right">
+			<span class="tw-float-right">
 				<div class="ui mini modal issue-delete-time-modal" data-id="{{.comment.Time.ID}}">
 					<form method="post" class="delete-time-form" action="{{.ctxData.RepoLink}}/issues/{{.ctxData.Issue.Index}}/times/{{.comment.TimeID}}/delete">
 						{{.ctxData.CsrfTokenHtml}}
diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl
index d1abb15374..8ba567ee6b 100644
--- a/templates/repo/migrate/migrate.tmpl
+++ b/templates/repo/migrate/migrate.tmpl
@@ -16,10 +16,10 @@
 							{{svg (printf "gitea-%s" .Name) 184}}
 						{{end}}
 						<div class="content">
-							<div class="header gt-text-center">
+							<div class="header tw-text-center">
 								{{.Title}}
 							</div>
-							<div class="description gt-text-center">
+							<div class="description tw-text-center">
 								{{ctx.Locale.Tr (printf "repo.migrate.%s.description" .Name)}}
 							</div>
 						</div>
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index 46b1c9b291..30e783167c 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -64,7 +64,7 @@
 						<div class="flex-text-inline gt-f1">
 							<input name="attachment-edit-{{.UUID}}"  class="attachment_edit" required value="{{.Name}}">
 							<input name="attachment-del-{{.UUID}}" type="hidden" value="false">
-							<span class="ui text grey gt-whitespace-nowrap">{{.Size | FileSize}}</span>
+							<span class="ui text grey tw-whitespace-nowrap">{{.Size | FileSize}}</span>
 							<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
 								{{svg "octicon-info"}}
 							</span>
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index fbdc12defb..f8896b504e 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -1,7 +1,7 @@
 {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
 	<div class="repo-setting-content">
 		{{if .Repository.IsArchived}}
-			<div class="ui warning message gt-text-center">
+			<div class="ui warning message tw-text-center">
 				{{ctx.Locale.Tr "repo.settings.archive.branchsettings_unavailable"}}
 			</div>
 		{{else}}
diff --git a/templates/repo/settings/githooks.tmpl b/templates/repo/settings/githooks.tmpl
index bdbfb40ca1..3fce29d545 100644
--- a/templates/repo/settings/githooks.tmpl
+++ b/templates/repo/settings/githooks.tmpl
@@ -12,7 +12,7 @@
 					<div class="item truncated-item-container">
 						<span class="text {{if .IsActive}}green{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span>
 						<span class="text truncate gt-f1 gt-mr-3">{{.Name}}</span>
-						<a class="muted gt-float-right gt-p-3" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}">
+						<a class="muted tw-float-right gt-p-3" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}">
 							{{svg "octicon-pencil"}}
 						</a>
 					</div>
diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl
index fdc6b536c2..fa2e376ff3 100644
--- a/templates/repo/settings/lfs_pointers.tmpl
+++ b/templates/repo/settings/lfs_pointers.tmpl
@@ -44,7 +44,7 @@
 							<td>{{if .InRepo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if .Exists}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if .Accessible}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
-							<td class="gt-text-right">
+							<td class="tw-text-right">
 								<a class="ui primary button" href="{{$.LFSFilesLink}}/find?oid={{.Oid}}&size={{.Size}}&sha={{.SHA}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a>
 							</td>
 						</tr>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 5a85192a43..376cfe7607 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -80,7 +80,7 @@
 			</h4>
 			<div class="ui attached segment">
 				{{if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{ctx.Locale.Tr "repo.settings.archive.mirrors_unavailable"}}
 					</div>
 				{{else}}
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl
index e4fcf2ee6b..31fb59e5e3 100644
--- a/templates/repo/settings/tags.tmpl
+++ b/templates/repo/settings/tags.tmpl
@@ -1,7 +1,7 @@
 {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
 	<div class="repo-setting-content">
 		{{if .Repository.IsArchived}}
-			<div class="ui warning message gt-text-center">
+			<div class="ui warning message tw-text-center">
 				{{ctx.Locale.Tr "repo.settings.archive.tagsettings_unavailable"}}
 			</div>
 		{{else}}
diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl
index c9f8cd38c1..8bceafa8bb 100644
--- a/templates/repo/unicode_escape_prompt.tmpl
+++ b/templates/repo/unicode_escape_prompt.tmpl
@@ -1,6 +1,6 @@
 {{if .EscapeStatus}}
 	{{if .EscapeStatus.HasInvisible}}
-		<div class="ui warning message unicode-escape-prompt gt-text-left">
+		<div class="ui warning message unicode-escape-prompt tw-text-left">
 			<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
 			<div class="header">
 				{{ctx.Locale.Tr "repo.invisible_runes_header"}}
@@ -11,7 +11,7 @@
 			{{end}}
 		</div>
 	{{else if .EscapeStatus.HasAmbiguous}}
-		<div class="ui warning message unicode-escape-prompt gt-text-left">
+		<div class="ui warning message unicode-escape-prompt tw-text-left">
 			<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
 			<div class="header">
 				{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 1f9e0b5028..75f45b293f 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -1,12 +1,12 @@
 <div {{if .ReadmeInList}}id="readme" {{end}}class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
 	{{- if .FileError}}
 		<div class="ui error message">
-			<div class="text left gt-whitespace-pre">{{.FileError}}</div>
+			<div class="text left tw-whitespace-pre">{{.FileError}}</div>
 		</div>
 	{{end}}
 	{{- if .FileWarning}}
 		<div class="ui warning message">
-			<div class="text left gt-whitespace-pre">{{.FileWarning}}</div>
+			<div class="text left tw-whitespace-pre">{{.FileWarning}}</div>
 		</div>
 	{{end}}
 
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 9fa05b5b5c..541b1e9b42 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -79,19 +79,19 @@
 			{{if .sidebarPresent}}
 			<div class="markup wiki-content-sidebar">
 				{{if and .CanWriteWiki (not .Repository.IsMirror)}}
-					<a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
+					<a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
 				{{end}}
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
 				{{.sidebarContent | SafeHTML}}
 			</div>
 			{{end}}
 
-			<div class="gt-clear-both"></div>
+			<div class="tw-clear-both"></div>
 
 			{{if .footerPresent}}
 			<div class="markup wiki-content-footer">
 				{{if and .CanWriteWiki (not .Repository.IsMirror)}}
-					<a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
+					<a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
 				{{end}}
 				{{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}}
 				{{.footerContent | SafeHTML}}
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index a92933c153..58795e4bc0 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -38,7 +38,7 @@
 			<div class="ui container gt-my-5">
 				{{if .ErrorMsg}}
 					<p>{{ctx.Locale.Tr "error.occurred"}}:</p>
-					<pre class="gt-whitespace-pre-wrap gt-break-all">{{.ErrorMsg}}</pre>
+					<pre class="tw-whitespace-pre-wrap gt-break-all">{{.ErrorMsg}}</pre>
 				{{end}}
 				<div class="center gt-mt-5">
 					{{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index 1c3379e629..d4d1a82418 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -24,7 +24,7 @@
 		<div id="captcha" data-captcha-type="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
 	</div>
 {{else if eq .CaptchaType "cfturnstile"}}
-	<div class="inline field gt-text-center">
+	<div class="inline field tw-text-center">
 		<div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
 	</div>
 	<script src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script>
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 82622366e7..fd5960c31e 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -57,7 +57,7 @@
 					</form>
 					<!-- Sort -->
 					<div class="list-header-sort ui small dropdown type jump item">
-						<span class="text gt-whitespace-nowrap">
+						<span class="text tw-whitespace-nowrap">
 							{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						</span>
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index d8f8d462d3..431aca0975 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -36,7 +36,7 @@
 				{{else}}
 					{{range $notification := .Notifications}}
 						<div class="notifications-item gt-df gt-ac gt-fw gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
-							<div class="notifications-icon gt-ml-3 gt-mr-2 gt-self-start gt-mt-2">
+							<div class="notifications-icon gt-ml-3 gt-mr-2 tw-self-start gt-mt-2">
 								{{if .Issue}}
 									{{template "shared/issueicon" .Issue}}
 								{{else}}
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 7ce9a4b70f..e43cf2ebbe 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -61,11 +61,11 @@
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
-					<label class="gt-cursor-pointer">
+					<label class="tw-cursor-pointer">
 						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
 						{{ctx.Locale.Tr "settings.permissions_public_only"}}
 					</label>
-					<label class="gt-cursor-pointer">
+					<label class="tw-cursor-pointer">
 						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="" checked>
 						{{ctx.Locale.Tr "settings.permissions_access_all"}}
 					</label>
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index 5aabec547a..eeb2b6cbdd 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -32,7 +32,7 @@
 									{{else}}
 										<span class="icon gt-dib gt-pt-3">{{svg "octicon-file-directory-fill"}}</span>
 										<span class="name gt-dib gt-pt-3">{{$.ContextUser.Name}}/{{$dir}}</span>
-										<div class="gt-float-right">
+										<div class="tw-float-right">
 											{{if $.allowAdopt}}
 												<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
 												<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 3579c193b1..71f3a619b9 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -52,34 +52,14 @@ Gitea's private styles use `g-` prefix.
 /* below class names match Tailwind CSS */
 .gt-break-all { word-break: break-all !important; }
 .gt-content-center { align-content: center !important; }
-.gt-cursor-default { cursor: default !important; }
-.gt-cursor-pointer { cursor: pointer !important; }
-.gt-cursor-grab { cursor: grab !important; }
 .gt-invisible { visibility: hidden !important; }
 .gt-items-start { align-items: flex-start !important; }
 .gt-pointer-events-none { pointer-events: none !important; }
 .gt-relative { position: relative !important; }
-.gt-whitespace-nowrap { white-space: nowrap !important; }
-.gt-whitespace-pre { white-space: pre !important; }
-.gt-whitespace-pre-wrap { white-space: pre-wrap !important; }
 .gt-object-contain { object-fit: contain !important; }
-.gt-self-center { align-self: center !important; }
-.gt-self-start { align-self: flex-start !important; }
-.gt-self-end { align-self: flex-end !important; }
 .gt-no-underline { text-decoration-line: none !important; }
 .gt-normal-case { text-transform: none !important; }
 .gt-italic { font-style: italic !important; }
-.gt-overflow-x-auto { overflow-x: auto !important; }
-.gt-overflow-x-scroll { overflow-x: scroll !important; }
-.gt-overflow-y-hidden { overflow-y: hidden !important; }
-
-.gt-float-left { float: left !important; }
-.gt-float-right { float: right !important; }
-.gt-clear-both { clear: both !important; }
-
-.gt-text-center { text-align: center !important; }
-.gt-text-left { text-align: left !important; }
-.gt-text-right { text-align: right !important; }
 
 .gt-font-light { font-weight: var(--font-weight-light) !important; }
 .gt-font-normal { font-weight: var(--font-weight-normal) !important; }
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index bc7d979d99..70ce9bd6a0 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -278,7 +278,7 @@ export default sfc; // activate IDE's Vue plugin
           <div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'">
             {{ textDefaultBranchLabel }}
           </div>
-          <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon gt-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
+          <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon tw-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
             <!-- creating a lot of Vue component is pretty slow, so we use a static SVG here -->
             <svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg>
           </a>
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index c53d43cbb2..211253ef9a 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -234,7 +234,7 @@ export function initDropzone(el) {
         // Create a "Copy Link" element, to conveniently copy the image
         // or file link as Markdown to the clipboard
         const copyLinkElement = document.createElement('div');
-        copyLinkElement.className = 'gt-text-center';
+        copyLinkElement.className = 'tw-text-center';
         // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
         copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
         copyLinkElement.addEventListener('click', async (e) => {
diff --git a/web_src/js/modules/fomantic.js b/web_src/js/modules/fomantic.js
index 0c7a7ae641..6aafdd5ddc 100644
--- a/web_src/js/modules/fomantic.js
+++ b/web_src/js/modules/fomantic.js
@@ -17,7 +17,7 @@ export function initGiteaFomantic() {
   // By default, use "exact match" for full text search
   $.fn.dropdown.settings.fullTextSearch = 'exact';
   // Do not use "cursor: pointer" for dropdown labels
-  $.fn.dropdown.settings.className.label += ' gt-cursor-default';
+  $.fn.dropdown.settings.className.label += ' tw-cursor-default';
   // Always use Gitea's SVG icons
   $.fn.dropdown.settings.templates.label = function(_value, text, preserveHTML, className) {
     const escape = $.fn.dropdown.settings.templates.escape;

From d769b664dedb5f63b73146b58b21c0a772c2630d Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 4 Mar 2024 06:55:17 +0100
Subject: [PATCH 258/679] Inline the `css-variables-parser` dependency (#29571)

Get rid of the `postcss@7` dependency by inlining this simple function.
---
 package-lock.json  | 30 ------------------------------
 package.json       |  1 -
 tailwind.config.js | 34 ++++++++++++++++++++++++----------
 3 files changed, 24 insertions(+), 41 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 1189c90db9..6a6eb4b947 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,6 @@
         "chartjs-plugin-zoom": "2.0.1",
         "clippie": "4.0.7",
         "css-loader": "6.10.0",
-        "css-variables-parser": "1.0.1",
         "dayjs": "1.11.10",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
@@ -4032,35 +4031,6 @@
         "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
       }
     },
-    "node_modules/css-variables-parser": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/css-variables-parser/-/css-variables-parser-1.0.1.tgz",
-      "integrity": "sha512-GWaqrwGtAWVr/yjjE17iyvbcy+W3voe0vko1/xLCwFeYd3kTLstzUdVH+g5TTXejrtlsb1FS4L9rP6PmeTa8wQ==",
-      "dependencies": {
-        "postcss": "^7.0.36"
-      }
-    },
-    "node_modules/css-variables-parser/node_modules/picocolors": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
-      "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
-    },
-    "node_modules/css-variables-parser/node_modules/postcss": {
-      "version": "7.0.39",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
-      "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
-      "dependencies": {
-        "picocolors": "^0.2.1",
-        "source-map": "^0.6.1"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
-      }
-    },
     "node_modules/css-what": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
diff --git a/package.json b/package.json
index 1152bfef72..d5e8170228 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,6 @@
     "chartjs-plugin-zoom": "2.0.1",
     "clippie": "4.0.7",
     "css-loader": "6.10.0",
-    "css-variables-parser": "1.0.1",
     "dayjs": "1.11.10",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
diff --git a/tailwind.config.js b/tailwind.config.js
index 7f36822001..fb17980568 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,9 +1,28 @@
 import {readFileSync} from 'node:fs';
 import {env} from 'node:process';
-import {parse} from 'css-variables-parser';
+import {parse} from 'postcss';
 
 const isProduction = env.NODE_ENV !== 'development';
 
+function extractRootVars(css) {
+  const root = parse(css);
+  const vars = new Set();
+  root.walkRules((rule) => {
+    if (rule.selector !== ':root') return;
+    rule.each((decl) => {
+      if (decl.value && decl.prop.startsWith('--')) {
+        vars.add(decl.prop.substring(2));
+      }
+    });
+  });
+  return Array.from(vars);
+}
+
+const vars = extractRootVars([
+  readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'),
+  readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'),
+].join('\n'));
+
 export default {
   prefix: 'tw-',
   important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles
@@ -23,15 +42,10 @@ export default {
   theme: {
     colors: {
       // make `tw-bg-red` etc work with our CSS variables
-      ...Object.fromEntries(
-        Object.keys(parse([
-          readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'),
-          readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'),
-        ].join('\n'), {})).filter((prop) => prop.startsWith('color-')).map((prop) => {
-          const color = prop.substring(6);
-          return [color, `var(--color-${color})`];
-        })
-      ),
+      ...Object.fromEntries(vars.filter((prop) => prop.startsWith('color-')).map((prop) => {
+        const color = prop.substring(6);
+        return [color, `var(--color-${color})`];
+      })),
       inherit: 'inherit',
       current: 'currentcolor',
       transparent: 'transparent',

From 8e12ba34bab7e728ac93ccfaecbe91e053ef1c89 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 4 Mar 2024 15:50:21 +0800
Subject: [PATCH 259/679] Allow options to disable user ssh keys configuration
 from the interface on app.ini (#29447)

Follow #29275
Extract from #20549
Fix #24716

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 custom/conf/app.example.ini                      |  3 ++-
 .../administration/config-cheat-sheet.en-us.md   |  5 +++--
 .../administration/config-cheat-sheet.zh-cn.md   |  5 +++--
 modules/setting/admin.go                         |  1 +
 routers/api/v1/user/key.go                       | 11 +++++++++++
 routers/web/user/setting/keys.go                 | 16 ++++++++++++++++
 templates/user/settings/keys.tmpl                |  4 +++-
 7 files changed, 39 insertions(+), 6 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index dc5aa691ee..17d6cd3a35 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1480,8 +1480,9 @@ LEVEL = Info
 ;;
 ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 ;DEFAULT_EMAIL_NOTIFICATIONS = enabled
-;; Disabled features for users, could be "deletion","manage_gpg_keys" more features can be disabled in future
+;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
 ;; - deletion: a user cannot delete their own account
+;; - manage_ssh_keys: a user cannot configure ssh keys
 ;; - manage_gpg_keys: a user cannot configure gpg keys
 ;USER_DISABLED_FEATURES =
 
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index ea6e1eb1a4..8a01711949 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -518,9 +518,10 @@ And the following unique queues:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
-- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_gpg_keys` and more features can be added in future.
+- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future.
   - `deletion`: User cannot delete their own account.
-  - `manage_gpg_keys`: User cannot configure gpg keys
+  - `manage_ssh_keys`: User cannot configure ssh keys.
+  - `manage_gpg_keys`: User cannot configure gpg keys.
 
 ## Security (`security`)
 
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 5cc5734359..7b102eda8e 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -497,9 +497,10 @@ Gitea 创建以下非唯一队列:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
-- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。
+- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。
   - `deletion`: 用户不能通过界面或者API删除他自己。
-  - `manage_gpg_keys`: 用户不能配置 GPG 密钥
+  - `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。
+  - `manage_gpg_keys`: 用户不能配置 GPG 密钥。
 
 ## 安全性 (`security`)
 
diff --git a/modules/setting/admin.go b/modules/setting/admin.go
index 29bb947bc4..be214a58ce 100644
--- a/modules/setting/admin.go
+++ b/modules/setting/admin.go
@@ -21,5 +21,6 @@ func loadAdminFrom(rootCfg ConfigProvider) {
 
 const (
 	UserFeatureDeletion      = "deletion"
+	UserFeatureManageSSHKeys = "manage_ssh_keys"
 	UserFeatureManageGPGKeys = "manage_gpg_keys"
 )
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index ada6759f8e..bcbfd93bd3 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -5,6 +5,7 @@ package user
 
 import (
 	std_ctx "context"
+	"fmt"
 	"net/http"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -198,6 +199,11 @@ func GetPublicKey(ctx *context.APIContext) {
 
 // CreateUserPublicKey creates new public key to given user by ID.
 func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) {
+	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+		return
+	}
+
 	content, err := asymkey_model.CheckPublicKeyString(form.Key)
 	if err != nil {
 		repo.HandleCheckKeyStringError(ctx, err)
@@ -263,6 +269,11 @@ func DeletePublicKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
+	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+		return
+	}
+
 	id := ctx.ParamsInt64(":id")
 	externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id)
 	if err != nil {
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index cb01913bda..d2b60fc809 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -159,6 +159,11 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "ssh":
+		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+			return
+		}
+
 		content, err := asymkey_model.CheckPublicKeyString(form.Content)
 		if err != nil {
 			if db.IsErrSSHDisabled(err) {
@@ -198,6 +203,11 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "verify_ssh":
+		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+			return
+		}
+
 		token := asymkey_model.VerificationToken(ctx.Doer, 1)
 		lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
 
@@ -240,6 +250,11 @@ func DeleteKey(ctx *context.Context) {
 			ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
 		}
 	case "ssh":
+		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+			return
+		}
+
 		keyID := ctx.FormInt64("id")
 		external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID)
 		if err != nil {
@@ -318,4 +333,5 @@ func loadKeysData(ctx *context.Context) {
 
 	ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
 	ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
+	ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures
 }
diff --git a/templates/user/settings/keys.tmpl b/templates/user/settings/keys.tmpl
index a44bf50048..e0f5e426ae 100644
--- a/templates/user/settings/keys.tmpl
+++ b/templates/user/settings/keys.tmpl
@@ -1,6 +1,8 @@
 {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings sshkeys")}}
 	<div class="user-setting-content">
-		{{template "user/settings/keys_ssh" .}}
+		{{if not ($.UserDisabledFeatures.Contains "manage_ssh_keys")}}
+			{{template "user/settings/keys_ssh" .}}
+		{{end}}
 		{{template "user/settings/keys_principal" .}}
 		{{if not ($.UserDisabledFeatures.Contains "manage_gpg_keys")}}
 		{{template "user/settings/keys_gpg" .}}

From c337ff0ec70618ef2ead7850f90ab2a8458db192 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Mon, 4 Mar 2024 09:16:03 +0100
Subject: [PATCH 260/679] Add user blocking (#29028)

Fixes #17453

This PR adds the abbility to block a user from a personal account or
organization to restrict how the blocked user can interact with the
blocker. The docs explain what's the consequence of blocking a user.

Screenshots:


![grafik](https://github.com/go-gitea/gitea/assets/1666336/4ed884f3-e06a-4862-afd3-3b8aa2488dc6)


![grafik](https://github.com/go-gitea/gitea/assets/1666336/ae6d4981-f252-4f50-a429-04f0f9f1cdf1)


![grafik](https://github.com/go-gitea/gitea/assets/1666336/ca153599-5b0f-4b4a-90fe-18bdfd6f0b6b)

---------

Co-authored-by: Lauris BH <lauris@nix.lv>
---
 docs/content/usage/blocking-users.en-us.md    |  56 ++++
 models/fixtures/access.yml                    |  52 +--
 models/fixtures/collaboration.yml             |  12 +
 models/fixtures/issue_assignees.yml           |   4 +
 models/fixtures/repo_transfer.yml             |  16 +
 models/fixtures/repository.yml                |   8 +-
 models/fixtures/star.yml                      |  10 +
 models/fixtures/user.yml                      |   2 +-
 models/fixtures/user_blocking.yml             |  19 ++
 models/fixtures/watch.yml                     |  12 +
 models/issues/assignees.go                    |  21 ++
 models/issues/issue_update.go                 |   9 +
 models/issues/issue_xref.go                   |   4 +
 models/issues/reaction.go                     |  19 --
 models/migrations/migrations.go               |   2 +
 models/migrations/v1_22/v288.go               |  26 ++
 models/org.go                                 |  36 +-
 models/org_team.go                            |  79 ++---
 models/org_team_test.go                       | 100 +++---
 models/org_test.go                            |  49 ++-
 models/organization/org.go                    |   1 +
 models/organization/team_user.go              |   8 -
 models/perm/access/access.go                  |   4 +-
 models/repo/collaboration.go                  |  55 +++-
 models/repo/collaboration_test.go             |  37 +--
 models/repo/repo_test.go                      |  17 +-
 models/repo/star.go                           |  20 +-
 models/repo/star_test.go                      |  40 ++-
 models/repo/user_repo.go                      | 103 ++++--
 models/repo/user_repo_test.go                 |   6 +-
 models/repo/watch.go                          |  28 +-
 models/repo/watch_test.go                     |  26 +-
 models/repo_transfer.go                       |  37 ++-
 models/user/block.go                          | 123 +++++++
 models/user/follow.go                         |  14 +-
 models/user/user.go                           |   2 +-
 models/user/user_test.go                      |  17 +-
 modules/repository/collaborator.go            |   8 +
 modules/repository/create.go                  |   2 +-
 options/locale/locale_en-US.ini               |  33 ++
 routers/api/v1/api.go                         |  20 +-
 routers/api/v1/org/block.go                   | 116 +++++++
 routers/api/v1/org/member.go                  |   2 +-
 routers/api/v1/org/team.go                    |  13 +-
 routers/api/v1/repo/collaborators.go          |  24 +-
 routers/api/v1/repo/fork.go                   |   2 +
 routers/api/v1/repo/issue.go                  |  14 +-
 routers/api/v1/repo/issue_comment.go          |  14 +-
 .../api/v1/repo/issue_comment_attachment.go   |  10 +-
 routers/api/v1/repo/issue_reaction.go         |  12 +-
 routers/api/v1/repo/pull.go                   |  10 +-
 routers/api/v1/repo/transfer.go               |   7 +-
 routers/api/v1/shared/block.go                |  98 ++++++
 routers/api/v1/user/block.go                  |  96 ++++++
 routers/api/v1/user/follower.go               |  11 +-
 routers/api/v1/user/star.go                   |  27 +-
 routers/api/v1/user/watch.go                  |  27 +-
 routers/web/org/block.go                      |  38 +++
 routers/web/org/members.go                    |  22 +-
 routers/web/org/teams.go                      |  17 +-
 routers/web/repo/issue.go                     |  37 ++-
 routers/web/repo/pull.go                      |  20 +-
 routers/web/repo/repo.go                      |  16 +-
 routers/web/repo/setting/collaboration.go     |  26 +-
 routers/web/repo/setting/setting.go           |   3 +
 routers/web/shared/user/block.go              |  76 +++++
 routers/web/shared/user/header.go             |   8 +
 routers/web/user/profile.go                   |   2 +-
 routers/web/user/setting/block.go             |  38 +++
 routers/web/web.go                            |  10 +
 services/auth/source/source_group_sync.go     |   4 +-
 services/forms/user_form.go                   |  11 +
 services/issue/comments.go                    |  26 ++
 services/issue/commit.go                      |   4 +
 services/issue/content.go                     |  13 +-
 services/issue/issue.go                       |  41 ++-
 services/issue/reaction.go                    |  50 +++
 .../issue}/reaction_test.go                   |  90 +++--
 services/pull/pull.go                         |   8 +
 services/repository/collaboration.go          |  16 +-
 services/repository/collaboration_test.go     |  13 +-
 services/repository/delete.go                 |  14 +-
 services/repository/fork.go                   |   8 +
 services/repository/transfer.go               |  12 +-
 services/user/block.go                        | 308 ++++++++++++++++++
 services/user/block_test.go                   |  66 ++++
 services/user/delete.go                       |   2 +
 services/user/user.go                         |   2 +-
 services/user/user_test.go                    |   3 +-
 templates/org/settings/blocked_users.tmpl     |   5 +
 templates/org/settings/navbar.tmpl            |   3 +
 templates/repo/diff/box.tmpl                  |   1 +
 templates/repo/issue/view_content.tmpl        |   1 +
 .../repo/issue/view_content/context_menu.tmpl |  35 +-
 templates/shared/user/block_user_dialog.tmpl  |  23 ++
 templates/shared/user/blocked_users.tmpl      |  83 +++++
 templates/shared/user/profile_big_avatar.tmpl |  37 ++-
 templates/swagger/v1_json.tmpl                | 283 ++++++++++++++++
 templates/user/settings/blocked_users.tmpl    |   5 +
 templates/user/settings/navbar.tmpl           |   3 +
 tests/integration/api_comment_test.go         |  26 ++
 tests/integration/api_issue_reaction_test.go  |   7 +
 tests/integration/api_issue_test.go           |  10 +-
 .../integration/api_repo_collaborator_test.go |   7 +
 tests/integration/api_user_block_test.go      | 243 ++++++++++++++
 tests/integration/api_user_follow_test.go     |   8 +
 tests/integration/api_user_star_test.go       |   8 +
 tests/integration/api_user_watch_test.go      |   8 +
 tests/integration/auth_ldap_test.go           |   6 +-
 109 files changed, 2878 insertions(+), 548 deletions(-)
 create mode 100644 docs/content/usage/blocking-users.en-us.md
 create mode 100644 models/fixtures/user_blocking.yml
 create mode 100644 models/migrations/v1_22/v288.go
 create mode 100644 models/user/block.go
 create mode 100644 routers/api/v1/org/block.go
 create mode 100644 routers/api/v1/shared/block.go
 create mode 100644 routers/api/v1/user/block.go
 create mode 100644 routers/web/org/block.go
 create mode 100644 routers/web/shared/user/block.go
 create mode 100644 routers/web/user/setting/block.go
 create mode 100644 services/issue/reaction.go
 rename {models/issues => services/issue}/reaction_test.go (65%)
 create mode 100644 services/user/block.go
 create mode 100644 services/user/block_test.go
 create mode 100644 templates/org/settings/blocked_users.tmpl
 create mode 100644 templates/shared/user/block_user_dialog.tmpl
 create mode 100644 templates/shared/user/blocked_users.tmpl
 create mode 100644 templates/user/settings/blocked_users.tmpl
 create mode 100644 tests/integration/api_user_block_test.go

diff --git a/docs/content/usage/blocking-users.en-us.md b/docs/content/usage/blocking-users.en-us.md
new file mode 100644
index 0000000000..b59bbe4d62
--- /dev/null
+++ b/docs/content/usage/blocking-users.en-us.md
@@ -0,0 +1,56 @@
+---
+date: "2024-01-31T00:00:00+00:00"
+title: "Blocking a user"
+slug: "blocking-user"
+sidebar_position: 25
+toc: false
+draft: false
+aliases:
+  - /en-us/webhooks
+menu:
+  sidebar:
+    parent: "usage"
+    name: "Blocking a user"
+    sidebar_position: 30
+    identifier: "blocking-user"
+---
+
+# Blocking a user
+
+Gitea supports blocking of users to restrict how they can interact with you and your content.
+
+You can block a user in your account settings, from the user's profile or from comments created by the user.
+The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you.
+Organization owners can block anyone who is not a member of the organization too.
+If a blocked user has admin permissions, they can still perform all actions even if blocked.
+
+### When you block a user
+
+- the user stops following you
+- you stop following the user
+- the user's stars are removed from your repositories
+- your stars are removed from their repositories
+- the user stops watching your repositories
+- you stop watching their repositories
+- the user's issue assignments are removed from your repositories
+- your issue assignments are removed from their repositories
+- the user is removed as a collaborator on your repositories
+- you are removed as a collaborator on their repositories
+- any pending repository transfers to or from the blocked user are canceled
+
+### When you block a user, the user cannot
+
+- follow you
+- watch your repositories
+- star your repositories
+- fork your repositories
+- transfer repositories to you
+- open issues or pull requests on your repositories
+- comment on issues or pull requests you've created
+- comment on issues or pull requests on your repositories
+- react to your comments on issues or pull requests
+- react to comments on issues or pull requests on your repositories
+- assign you to issues or pull requests
+- add you as a collaborator on their repositories
+- send you notifications by @mentioning your username
+- be added as team member (if blocked by an organization)
diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml
index 641c453eb7..4171e31fef 100644
--- a/models/fixtures/access.yml
+++ b/models/fixtures/access.yml
@@ -42,120 +42,132 @@
 
 -
   id: 8
-  user_id: 15
+  user_id: 10
   repo_id: 21
   mode: 2
 
 -
   id: 9
-  user_id: 15
-  repo_id: 22
+  user_id: 10
+  repo_id: 32
   mode: 2
 
 -
   id: 10
   user_id: 15
+  repo_id: 21
+  mode: 2
+
+-
+  id: 11
+  user_id: 15
+  repo_id: 22
+  mode: 2
+
+-
+  id: 12
+  user_id: 15
   repo_id: 23
   mode: 4
 
 -
-  id: 11
+  id: 13
   user_id: 15
   repo_id: 24
   mode: 4
 
 -
-  id: 12
+  id: 14
   user_id: 15
   repo_id: 32
   mode: 2
 
 -
-  id: 13
+  id: 15
   user_id: 18
   repo_id: 21
   mode: 2
 
 -
-  id: 14
+  id: 16
   user_id: 18
   repo_id: 22
   mode: 2
 
 -
-  id: 15
+  id: 17
   user_id: 18
   repo_id: 23
   mode: 4
 
 -
-  id: 16
+  id: 18
   user_id: 18
   repo_id: 24
   mode: 4
 
 -
-  id: 17
+  id: 19
   user_id: 20
   repo_id: 24
   mode: 1
 
 -
-  id: 18
+  id: 20
   user_id: 20
   repo_id: 27
   mode: 4
 
 -
-  id: 19
+  id: 21
   user_id: 20
   repo_id: 28
   mode: 4
 
 -
-  id: 20
+  id: 22
   user_id: 29
   repo_id: 4
   mode: 2
 
 -
-  id: 21
+  id: 23
   user_id: 29
   repo_id: 24
   mode: 1
 
 -
-  id: 22
+  id: 24
   user_id: 31
   repo_id: 27
   mode: 4
 
 -
-  id: 23
+  id: 25
   user_id: 31
   repo_id: 28
   mode: 4
 
 -
-  id: 24
+  id: 26
   user_id: 38
   repo_id: 60
   mode: 2
 
 -
-  id: 25
+  id: 27
   user_id: 38
   repo_id: 61
   mode: 1
 
 -
-  id: 26
+  id: 28
   user_id: 39
   repo_id: 61
   mode: 1
 
 -
-  id: 27
+  id: 29
   user_id: 40
   repo_id: 61
   mode: 4
diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml
index 7603bdad32..4c3ac367f6 100644
--- a/models/fixtures/collaboration.yml
+++ b/models/fixtures/collaboration.yml
@@ -51,3 +51,15 @@
   repo_id: 60
   user_id: 38
   mode: 2 # write
+
+-
+  id: 10
+  repo_id: 21
+  user_id: 10
+  mode: 2 # write
+
+-
+  id: 11
+  repo_id: 32
+  user_id: 10
+  mode: 2 # write
diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml
index e5d36f921a..c40ecad676 100644
--- a/models/fixtures/issue_assignees.yml
+++ b/models/fixtures/issue_assignees.yml
@@ -14,3 +14,7 @@
   id: 4
   assignee_id: 2
   issue_id: 17
+-
+  id: 5
+  assignee_id: 10
+  issue_id: 6
diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml
index b841b5e983..db92c95248 100644
--- a/models/fixtures/repo_transfer.yml
+++ b/models/fixtures/repo_transfer.yml
@@ -5,3 +5,19 @@
   repo_id: 3
   created_unix: 1553610671
   updated_unix: 1553610671
+
+-
+  id: 2
+  doer_id: 16
+  recipient_id: 10
+  repo_id: 21
+  created_unix: 1553610671
+  updated_unix: 1553610671
+
+-
+  id: 3
+  doer_id: 3
+  recipient_id: 10
+  repo_id: 32
+  created_unix: 1553610671
+  updated_unix: 1553610671
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index d094fe82d8..e5c6224c96 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -614,8 +614,8 @@
   owner_name: user16
   lower_name: big_test_public_3
   name: big_test_public_3
-  num_watches: 0
-  num_stars: 0
+  num_watches: 1
+  num_stars: 1
   num_forks: 0
   num_issues: 0
   num_closed_issues: 0
@@ -945,8 +945,8 @@
   owner_name: org3
   lower_name: repo21
   name: repo21
-  num_watches: 0
-  num_stars: 0
+  num_watches: 1
+  num_stars: 1
   num_forks: 0
   num_issues: 2
   num_closed_issues: 0
diff --git a/models/fixtures/star.yml b/models/fixtures/star.yml
index 860f26b8e2..39b51b3736 100644
--- a/models/fixtures/star.yml
+++ b/models/fixtures/star.yml
@@ -7,3 +7,13 @@
   id: 2
   uid: 2
   repo_id: 4
+
+-
+  id: 3
+  uid: 10
+  repo_id: 21
+
+-
+  id: 4
+  uid: 10
+  repo_id: 32
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 16b687ae04..a3de535508 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -361,7 +361,7 @@
   use_custom_avatar: false
   num_followers: 0
   num_following: 0
-  num_stars: 0
+  num_stars: 2
   num_repos: 3
   num_teams: 0
   num_members: 0
diff --git a/models/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml
new file mode 100644
index 0000000000..2ec9d99df5
--- /dev/null
+++ b/models/fixtures/user_blocking.yml
@@ -0,0 +1,19 @@
+-
+  id: 1
+  blocker_id: 2
+  blockee_id: 29
+
+-
+  id: 2
+  blocker_id: 17
+  blockee_id: 28
+
+-
+  id: 3
+  blocker_id: 2
+  blockee_id: 34
+
+-
+  id: 4
+  blocker_id: 50
+  blockee_id: 34
diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml
index 1950ac99e7..18bcd2ed2b 100644
--- a/models/fixtures/watch.yml
+++ b/models/fixtures/watch.yml
@@ -27,3 +27,15 @@
   user_id: 11
   repo_id: 1
   mode: 3 # auto
+
+-
+  id: 6
+  user_id: 10
+  repo_id: 21
+  mode: 1 # normal
+
+-
+  id: 7
+  user_id: 10
+  repo_id: 32
+  mode: 1 # normal
diff --git a/models/issues/assignees.go b/models/issues/assignees.go
index 60f32d9557..30234be07a 100644
--- a/models/issues/assignees.go
+++ b/models/issues/assignees.go
@@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U
 	return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
 }
 
+type AssignedIssuesOptions struct {
+	db.ListOptions
+	AssigneeID  int64
+	RepoOwnerID int64
+}
+
+func (opts *AssignedIssuesOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.AssigneeID != 0 {
+		cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID})))
+	}
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID})))
+	}
+	return cond
+}
+
+func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) {
+	return db.FindAndCount[Issue](ctx, opts)
+}
+
 // ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
 func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
 	ctx, committer, err := db.TxContext(ctx)
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index b258dc882c..ef96e1ee50 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo
 	if err != nil {
 		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 	}
+
+	notBlocked := make([]*user_model.User, 0, len(mentions))
+	for _, user := range mentions {
+		if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
+			notBlocked = append(notBlocked, user)
+		}
+	}
+	mentions = notBlocked
+
 	if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
 		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 	}
diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go
index cfc3c1683c..e2e35859df 100644
--- a/models/issues/issue_xref.go
+++ b/models/issues/issue_xref.go
@@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
 		if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
 			return nil, references.XRefActionNone, nil
 		}
+		if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) {
+			return nil, references.XRefActionNone, nil
+		}
+
 		// Accept close/reopening actions only if the poster is able to close the
 		// referenced issue manually at this moment. The only exception is
 		// the poster of a new PR referencing an issue on the same repo: then the merger
diff --git a/models/issues/reaction.go b/models/issues/reaction.go
index bb47cf24ca..d5448636fe 100644
--- a/models/issues/reaction.go
+++ b/models/issues/reaction.go
@@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro
 	return reaction, nil
 }
 
-// CreateIssueReaction creates a reaction on issue.
-func CreateIssueReaction(ctx context.Context, doerID, issueID int64, content string) (*Reaction, error) {
-	return CreateReaction(ctx, &ReactionOptions{
-		Type:    content,
-		DoerID:  doerID,
-		IssueID: issueID,
-	})
-}
-
-// CreateCommentReaction creates a reaction on comment.
-func CreateCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) (*Reaction, error) {
-	return CreateReaction(ctx, &ReactionOptions{
-		Type:      content,
-		DoerID:    doerID,
-		IssueID:   issueID,
-		CommentID: commentID,
-	})
-}
-
 // DeleteReaction deletes reaction for issue or comment.
 func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
 	reaction := &Reaction{
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 516eb53f62..9d288ec2bd 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -560,6 +560,8 @@ var migrations = []Migration{
 	NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
 	// v287 -> v288
 	NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
+	// v288 -> v289
+	NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go
new file mode 100644
index 0000000000..7c93bfcc66
--- /dev/null
+++ b/models/migrations/v1_22/v288.go
@@ -0,0 +1,26 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/xorm"
+)
+
+type Blocking struct {
+	ID          int64 `xorm:"pk autoincr"`
+	BlockerID   int64 `xorm:"UNIQUE(block)"`
+	BlockeeID   int64 `xorm:"UNIQUE(block)"`
+	Note        string
+	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+	return "user_blocking"
+}
+
+func AddUserBlockingTable(x *xorm.Engine) error {
+	return x.Sync(&Blocking{})
+}
diff --git a/models/org.go b/models/org.go
index 5f61f05b16..69cc47137e 100644
--- a/models/org.go
+++ b/models/org.go
@@ -12,15 +12,16 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 )
 
 // RemoveOrgUser removes user from given organization.
-func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
+func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
 	ou := new(organization.OrgUser)
 
 	has, err := db.GetEngine(ctx).
-		Where("uid=?", userID).
-		And("org_id=?", orgID).
+		Where("uid=?", user.ID).
+		And("org_id=?", org.ID).
 		Get(ou)
 	if err != nil {
 		return fmt.Errorf("get org-user: %w", err)
@@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 		return nil
 	}
 
-	org, err := organization.GetOrgByID(ctx, orgID)
-	if err != nil {
-		return fmt.Errorf("GetUserByID [%d]: %w", orgID, err)
-	}
-
 	// Check if the user to delete is the last member in owner team.
-	if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil {
+	if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
 		return err
 	} else if isOwner {
 		t, err := organization.GetOwnerTeam(ctx, org.ID)
@@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 			if err := t.LoadMembers(ctx); err != nil {
 				return err
 			}
-			if t.Members[0].ID == userID {
-				return organization.ErrLastOrgOwner{UID: userID}
+			if t.Members[0].ID == user.ID {
+				return organization.ErrLastOrgOwner{UID: user.ID}
 			}
 		}
 	}
@@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 
 	if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
 		return err
-	} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
+	} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
 		return err
 	}
 
 	// Delete all repository accesses and unwatch them.
-	env, err := organization.AccessibleReposEnv(ctx, org, userID)
+	env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
 	if err != nil {
 		return fmt.Errorf("AccessibleReposEnv: %w", err)
 	}
 	repoIDs, err := env.RepoIDs(1, org.NumRepos)
 	if err != nil {
-		return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err)
+		return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
 	}
 	for _, repoID := range repoIDs {
-		if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil {
+		repo, err := repo_model.GetRepositoryByID(ctx, repoID)
+		if err != nil {
+			return err
+		}
+		if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 			return err
 		}
 	}
 
 	if len(repoIDs) > 0 {
 		if _, err = db.GetEngine(ctx).
-			Where("user_id = ?", userID).
+			Where("user_id = ?", user.ID).
 			In("repo_id", repoIDs).
 			Delete(new(access_model.Access)); err != nil {
 			return err
@@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 	}
 
 	// Delete member in their teams.
-	teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID)
+	teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
 	if err != nil {
 		return err
 	}
 	for _, t := range teams {
-		if err = removeTeamMember(ctx, t, userID); err != nil {
+		if err = removeTeamMember(ctx, t, user); err != nil {
 			return err
 		}
 	}
diff --git a/models/org_team.go b/models/org_team.go
index 1a452436c3..aecf0d80fd 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R
 			return fmt.Errorf("getMembers: %w", err)
 		}
 		for _, u := range t.Members {
-			if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil {
+			if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
 				return fmt.Errorf("watchRepo: %w", err)
 			}
 		}
@@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
 				continue
 			}
 
-			if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil {
+			if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 				return err
 			}
 
@@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
 	}
 
 	for _, tm := range t.Members {
-		if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil {
+		if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
 			return err
 		}
 	}
@@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
 
 // AddTeamMember adds new membership of given team to given organization,
 // the user will have membership to given organization automatically when needed.
-func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
-	isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
+	if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
+		return user_model.ErrBlockedUser
+	}
+
+	isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 	if err != nil || isAlreadyMember {
 		return err
 	}
 
-	if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil {
+	if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
 		return err
 	}
 
 	err = db.WithTx(ctx, func(ctx context.Context) error {
 		// check in transaction
-		isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+		isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 		if err != nil || isAlreadyMember {
 			return err
 		}
@@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 		sess := db.GetEngine(ctx)
 
 		if err := db.Insert(ctx, &organization.TeamUser{
-			UID:    userID,
+			UID:    user.ID,
 			OrgID:  team.OrgID,
 			TeamID: team.ID,
 		}); err != nil {
@@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 		subQuery := builder.Select("repo_id").From("team_repo").
 			Where(builder.Eq{"team_id": team.ID})
 
-		if _, err := sess.Where("user_id=?", userID).
+		if _, err := sess.Where("user_id=?", user.ID).
 			In("repo_id", subQuery).
 			And("mode < ?", team.AccessMode).
 			SetExpr("mode", team.AccessMode).
@@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 
 		// for not exist access
 		var repoIDs []int64
-		accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID})
+		accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
 		if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
 			return fmt.Errorf("select id accesses: %w", err)
 		}
 
 		accesses := make([]*access_model.Access, 0, 100)
 		for i, repoID := range repoIDs {
-			accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode})
+			accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
 			if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
 				if err = db.Insert(ctx, accesses); err != nil {
 					return fmt.Errorf("insert new user accesses: %w", err)
@@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 		if err := team.LoadRepositories(ctx); err != nil {
 			log.Error("team.LoadRepositories failed: %v", err)
 		}
+
 		// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
 		go func(repos []*repo_model.Repository) {
 			for _, repo := range repos {
-				if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil {
+				if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
 					log.Error("watch repo failed: %v", err)
 				}
 			}
@@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 	return nil
 }
 
-func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
 	e := db.GetEngine(ctx)
-	isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+	isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 	if err != nil || !isMember {
 		return err
 	}
 
 	// Check if the user to delete is the last member in owner team.
 	if team.IsOwnerTeam() && team.NumMembers == 1 {
-		return organization.ErrLastOrgOwner{UID: userID}
+		return organization.ErrLastOrgOwner{UID: user.ID}
 	}
 
 	team.NumMembers--
@@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
 	}
 
 	if _, err := e.Delete(&organization.TeamUser{
-		UID:    userID,
+		UID:    user.ID,
 		OrgID:  team.OrgID,
 		TeamID: team.ID,
 	}); err != nil {
@@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
 
 	// Delete access to team repositories.
 	for _, repo := range team.Repos {
-		if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil {
+		if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
 			return err
 		}
 
 		// Remove watches from now unaccessible
-		if err := ReconsiderWatches(ctx, repo, userID); err != nil {
+		if err := ReconsiderWatches(ctx, repo, user); err != nil {
 			return err
 		}
 
 		// Remove issue assignments from now unaccessible
-		if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
+		if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
 			return err
 		}
 	}
 
-	return removeInvalidOrgUser(ctx, userID, team.OrgID)
+	return removeInvalidOrgUser(ctx, team.OrgID, user)
 }
 
-func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error {
+func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
 	// Check if the user is a member of any team in the organization.
 	if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
-		UID:   userID,
+		UID:   user.ID,
 		OrgID: orgID,
 	}); err != nil {
 		return err
 	} else if count == 0 {
-		return RemoveOrgUser(ctx, orgID, userID)
+		org, err := organization.GetOrgByID(ctx, orgID)
+		if err != nil {
+			return err
+		}
+
+		return RemoveOrgUser(ctx, org, user)
 	}
 	return nil
 }
 
 // RemoveTeamMember removes member from given team of given organization.
-func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
 	}
 	defer committer.Close()
-	if err := removeTeamMember(ctx, team, userID); err != nil {
+	if err := removeTeamMember(ctx, team, user); err != nil {
 		return err
 	}
 	return committer.Commit()
 }
 
-func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
-	user, err := user_model.GetUserByID(ctx, uid)
-	if err != nil {
-		return err
-	}
-
+func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
 	if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
 		return err
 	}
 
-	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
+	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
 		In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
 		Delete(&issues_model.IssueAssignees{}); err != nil {
-		return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
+		return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
 	}
 	return nil
 }
 
-func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
-	if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
+func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
+	if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has {
 		return err
 	}
-	if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+	if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 		return err
 	}
 
 	// Remove all IssueWatches a user has subscribed to in the repository
-	return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
+	return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
 }
diff --git a/models/org_team_test.go b/models/org_team_test.go
index e4b7b917e8..cf2c8be536 100644
--- a/models/org_team_test.go
+++ b/models/org_team_test.go
@@ -21,33 +21,42 @@ import (
 func TestTeam_AddMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	test := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+	test := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
 	}
-	test(1, 2)
-	test(1, 4)
-	test(3, 2)
+
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	test(team1, user2)
+	test(team1, user4)
+	test(team3, user2)
 }
 
 func TestTeam_RemoveMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testSuccess := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+	testSuccess := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
 	}
-	testSuccess(1, 4)
-	testSuccess(2, 2)
-	testSuccess(3, 2)
-	testSuccess(3, unittest.NonexistentID)
 
-	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
-	err := RemoveTeamMember(db.DefaultContext, team, 2)
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	testSuccess(team1, user4)
+	testSuccess(team2, user2)
+	testSuccess(team3, user2)
+
+	err := RemoveTeamMember(db.DefaultContext, team1, user2)
 	assert.True(t, organization.IsErrLastOrgOwner(err))
 }
 
@@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) {
 func TestAddTeamMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	test := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+	test := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
 	}
-	test(1, 2)
-	test(1, 4)
-	test(3, 2)
+
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	test(team1, user2)
+	test(team1, user4)
+	test(team3, user2)
 }
 
 func TestRemoveTeamMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testSuccess := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+	testSuccess := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
 	}
-	testSuccess(1, 4)
-	testSuccess(2, 2)
-	testSuccess(3, 2)
-	testSuccess(3, unittest.NonexistentID)
 
-	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
-	err := RemoveTeamMember(db.DefaultContext, team, 2)
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	testSuccess(team1, user4)
+	testSuccess(team2, user2)
+	testSuccess(team3, user2)
+
+	err := RemoveTeamMember(db.DefaultContext, team1, user2)
 	assert.True(t, organization.IsErrLastOrgOwner(err))
 }
 
@@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) {
 	team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
 	user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
 
-	has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+	has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
 	assert.NoError(t, err)
 	assert.False(t, has)
 
 	// adding user29 to team5 should add an explicit access row for repo 23
 	// even though repo 23 is public
-	assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID))
+	assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
 
-	has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+	has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
 	assert.NoError(t, err)
 	assert.True(t, has)
 }
diff --git a/models/org_test.go b/models/org_test.go
index d10a1dc218..247530406d 100644
--- a/models/org_test.go
+++ b/models/org_test.go
@@ -16,22 +16,27 @@ import (
 
 func TestUser_RemoveMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
+
 	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 
 	// remove a user that is a member
-	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
+	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
 	prevNumMembers := org.NumMembers
-	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4))
-	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
-	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
+	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
+
+	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 	assert.Equal(t, prevNumMembers-1, org.NumMembers)
 
 	// remove a user that is not a member
-	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
+	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
 	prevNumMembers = org.NumMembers
-	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5))
-	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
-	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
+	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
+
+	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 	assert.Equal(t, prevNumMembers, org.NumMembers)
 
 	unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
@@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) {
 
 func TestRemoveOrgUser(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	testSuccess := func(orgID, userID int64) {
-		org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+
+	testSuccess := func(org *organization.Organization, user *user_model.User) {
 		expectedNumMembers := org.NumMembers
-		if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
+		if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
 			expectedNumMembers--
 		}
-		assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID))
-		unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID})
-		org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+		assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
+		unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
+		org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 		assert.EqualValues(t, expectedNumMembers, org.NumMembers)
 	}
-	testSuccess(3, 4)
-	testSuccess(3, 4)
 
-	err := RemoveOrgUser(db.DefaultContext, 7, 5)
+	org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+	testSuccess(org3, user4)
+
+	org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	testSuccess(org3, user4)
+
+	err := RemoveOrgUser(db.DefaultContext, org7, user5)
 	assert.Error(t, err)
 	assert.True(t, organization.IsErrLastOrgOwner(err))
-	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5})
+	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
 	unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 }
diff --git a/models/organization/org.go b/models/organization/org.go
index b4919defb4..a3082e9ac7 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -400,6 +400,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
 		&TeamUnit{OrgID: org.ID},
 		&TeamInvite{OrgID: org.ID},
 		&secret_model.Secret{OwnerID: org.ID},
+		&user_model.Blocking{BlockerID: org.ID},
 	); err != nil {
 		return fmt.Errorf("DeleteBeans: %w", err)
 	}
diff --git a/models/organization/team_user.go b/models/organization/team_user.go
index ab767db200..d6d0a5054d 100644
--- a/models/organization/team_user.go
+++ b/models/organization/team_user.go
@@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error
 		Exist()
 }
 
-// GetTeamUsersByTeamID returns team users for a team
-func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) {
-	teamUsers := make([]*TeamUser, 0, 10)
-	return teamUsers, db.GetEngine(ctx).
-		Where("team_id=?", teamID).
-		Find(&teamUsers)
-}
-
 // SearchMembersOptions holds the search options
 type SearchMembersOptions struct {
 	db.ListOptions
diff --git a/models/perm/access/access.go b/models/perm/access/access.go
index 3e2568b4b4..b422a08614 100644
--- a/models/perm/access/access.go
+++ b/models/perm/access/access.go
@@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
 
 // refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
 func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
-	collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{})
+	collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID})
 	if err != nil {
-		return fmt.Errorf("getCollaborations: %w", err)
+		return fmt.Errorf("GetCollaborators: %w", err)
 	}
 	for _, c := range collaborators {
 		if c.User.IsGhost() {
diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go
index 7288082614..272c6ac05b 100644
--- a/models/repo/collaboration.go
+++ b/models/repo/collaboration.go
@@ -36,14 +36,44 @@ type Collaborator struct {
 	Collaboration *Collaboration
 }
 
+type FindCollaborationOptions struct {
+	db.ListOptions
+	RepoID         int64
+	RepoOwnerID    int64
+	CollaboratorID int64
+}
+
+func (opts *FindCollaborationOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.RepoID != 0 {
+		cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
+	}
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
+	}
+	if opts.CollaboratorID != 0 {
+		cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
+	}
+	return cond
+}
+
+func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
+	if opts.RepoOwnerID != 0 {
+		return []db.JoinFunc{
+			func(e db.Engine) error {
+				e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
+				return nil
+			},
+		}
+	}
+	return nil
+}
+
 // GetCollaborators returns the collaborators for a repository
-func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
-	collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
-		ListOptions: listOptions,
-		RepoID:      repoID,
-	})
+func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
+	collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
 	if err != nil {
-		return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
+		return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
 	}
 
 	collaborators := make([]*Collaborator, 0, len(collaborations))
@@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
 
 	usersMap := make(map[int64]*user_model.User)
 	if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
-		return nil, fmt.Errorf("Find users map by user ids: %w", err)
+		return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
 	}
 
 	for _, c := range collaborations {
@@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
 			Collaboration: c,
 		})
 	}
-	return collaborators, nil
+	return collaborators, total, nil
 }
 
 // GetCollaboration get collaboration for a repository id with a user id
@@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
 	return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
 }
 
-type FindCollaborationOptions struct {
-	db.ListOptions
-	RepoID int64
-}
-
-func (opts FindCollaborationOptions) ToConds() builder.Cond {
-	return builder.And(builder.Eq{"repo_id": opts.RepoID})
-}
-
 // ChangeCollaborationAccessMode sets new access mode for the collaboration.
 func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
 	// Discard invalid input
diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go
index 21a99dd557..639050f5fd 100644
--- a/models/repo/collaboration_test.go
+++ b/models/repo/collaboration_test.go
@@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
-		collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{})
+		collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
 		assert.NoError(t, err)
 		expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
 		assert.NoError(t, err)
@@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) {
 	// Test db.ListOptions
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
 
-	collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1})
+	collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+		ListOptions: db.ListOptions{PageSize: 1, Page: 1},
+		RepoID:      repo.ID,
+	})
 	assert.NoError(t, err)
 	assert.Len(t, collaborators1, 1)
 
-	collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2})
+	collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+		ListOptions: db.ListOptions{PageSize: 1, Page: 2},
+		RepoID:      repo.ID,
+	})
 	assert.NoError(t, err)
 	assert.Len(t, collaborators2, 1)
 
@@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
 	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 }
 
-func TestRepository_CountCollaborators(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
-	count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
-		RepoID: repo1.ID,
-	})
-	assert.NoError(t, err)
-	assert.EqualValues(t, 2, count)
-
-	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
-	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
-		RepoID: repo2.ID,
-	})
-	assert.NoError(t, err)
-	assert.EqualValues(t, 2, count)
-
-	// Non-existent repository.
-	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
-		RepoID: unittest.NonexistentID,
-	})
-	assert.NoError(t, err)
-	assert.EqualValues(t, 0, count)
-}
-
 func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index 1a870224bf..c13b698abf 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) {
 
 func TestWatchRepo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	const repoID = 3
-	const userID = 2
 
-	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
-	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false))
-	unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
+
+	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
+	unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 }
 
 func TestMetas(t *testing.T) {
diff --git a/models/repo/star.go b/models/repo/star.go
index 60737149da..4c66855525 100644
--- a/models/repo/star.go
+++ b/models/repo/star.go
@@ -24,26 +24,30 @@ func init() {
 }
 
 // StarRepo or unstar repository.
-func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
+func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
 	}
 	defer committer.Close()
-	staring := IsStaring(ctx, userID, repoID)
+	staring := IsStaring(ctx, doer.ID, repo.ID)
 
 	if star {
+		if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+			return user_model.ErrBlockedUser
+		}
+
 		if staring {
 			return nil
 		}
 
-		if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+		if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
 			return err
 		}
 	} else {
@@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
 			return nil
 		}
 
-		if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+		if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
 			return err
 		}
 	}
diff --git a/models/repo/star_test.go b/models/repo/star_test.go
index 62eac4e29a..aaac89d975 100644
--- a/models/repo/star_test.go
+++ b/models/repo/star_test.go
@@ -9,21 +9,24 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestStarRepo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	const userID = 2
-	const repoID = 1
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 }
 
 func TestIsStaring(t *testing.T) {
@@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) {
 
 func TestClearRepoStars(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	const userID = 2
-	const repoID = 1
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID))
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID))
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+
 	gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
 	assert.NoError(t, err)
 	assert.Len(t, gazers, 0)
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index 30c9db7474..6862247657 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -16,47 +16,82 @@ import (
 	"xorm.io/builder"
 )
 
+type StarredReposOptions struct {
+	db.ListOptions
+	StarrerID      int64
+	RepoOwnerID    int64
+	IncludePrivate bool
+}
+
+func (opts *StarredReposOptions) ToConds() builder.Cond {
+	var cond builder.Cond = builder.Eq{
+		"star.uid": opts.StarrerID,
+	}
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.Eq{
+			"repository.owner_id": opts.RepoOwnerID,
+		})
+	}
+	if !opts.IncludePrivate {
+		cond = cond.And(builder.Eq{
+			"repository.is_private": false,
+		})
+	}
+	return cond
+}
+
+func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
+	return []db.JoinFunc{
+		func(e db.Engine) error {
+			e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
+			return nil
+		},
+	}
+}
+
 // GetStarredRepos returns the repos starred by a particular user
-func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
-	sess := db.GetEngine(ctx).
-		Where("star.uid=?", userID).
-		Join("LEFT", "star", "`repository`.id=`star`.repo_id")
-	if !private {
-		sess = sess.And("is_private=?", false)
+func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
+	return db.Find[Repository](ctx, opts)
+}
+
+type WatchedReposOptions struct {
+	db.ListOptions
+	WatcherID      int64
+	RepoOwnerID    int64
+	IncludePrivate bool
+}
+
+func (opts *WatchedReposOptions) ToConds() builder.Cond {
+	var cond builder.Cond = builder.Eq{
+		"watch.user_id": opts.WatcherID,
 	}
-
-	if listOptions.Page != 0 {
-		sess = db.SetSessionPagination(sess, &listOptions)
-
-		repos := make([]*Repository, 0, listOptions.PageSize)
-		return repos, sess.Find(&repos)
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.Eq{
+			"repository.owner_id": opts.RepoOwnerID,
+		})
 	}
+	if !opts.IncludePrivate {
+		cond = cond.And(builder.Eq{
+			"repository.is_private": false,
+		})
+	}
+	return cond.And(builder.Neq{
+		"watch.mode": WatchModeDont,
+	})
+}
 
-	repos := make([]*Repository, 0, 10)
-	return repos, sess.Find(&repos)
+func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
+	return []db.JoinFunc{
+		func(e db.Engine) error {
+			e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
+			return nil
+		},
+	}
 }
 
 // GetWatchedRepos returns the repos watched by a particular user
-func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
-	sess := db.GetEngine(ctx).
-		Where("watch.user_id=?", userID).
-		And("`watch`.mode<>?", WatchModeDont).
-		Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
-	if !private {
-		sess = sess.And("is_private=?", false)
-	}
-
-	if listOptions.Page != 0 {
-		sess = db.SetSessionPagination(sess, &listOptions)
-
-		repos := make([]*Repository, 0, listOptions.PageSize)
-		total, err := sess.FindAndCount(&repos)
-		return repos, total, err
-	}
-
-	repos := make([]*Repository, 0, 10)
-	total, err := sess.FindAndCount(&repos)
-	return repos, total, err
+func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
+	return db.FindAndCount[Repository](ctx, opts)
 }
 
 // GetRepoAssignees returns all users that have write access and can be assigned to issues
diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go
index 7816b0262a..591dcea5b5 100644
--- a/models/repo/user_repo_test.go
+++ b/models/repo/user_repo_test.go
@@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) {
 	repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
 	users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
 	assert.NoError(t, err)
-	assert.Len(t, users, 3)
-	assert.Equal(t, users[0].ID, int64(15))
-	assert.Equal(t, users[1].ID, int64(18))
-	assert.Equal(t, users[2].ID, int64(16))
+	assert.Len(t, users, 4)
+	assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
 }
 
 func TestRepoGetReviewers(t *testing.T) {
diff --git a/models/repo/watch.go b/models/repo/watch.go
index 80da4030cb..a616544cae 100644
--- a/models/repo/watch.go
+++ b/models/repo/watch.go
@@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)
 	return err
 }
 
-// WatchRepoMode watch repository in specific mode.
-func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) {
-	var watch Watch
-	if watch, err = GetWatch(ctx, userID, repoID); err != nil {
-		return err
-	}
-	return watchRepoMode(ctx, watch, mode)
-}
-
 // WatchRepo watch or unwatch repository.
-func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
-	var watch Watch
-	if watch, err = GetWatch(ctx, userID, repoID); err != nil {
+func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
+	watch, err := GetWatch(ctx, doer.ID, repo.ID)
+	if err != nil {
 		return err
 	}
 	if !doWatch && watch.Mode == WatchModeAuto {
-		err = watchRepoMode(ctx, watch, WatchModeDont)
+		return watchRepoMode(ctx, watch, WatchModeDont)
 	} else if !doWatch {
-		err = watchRepoMode(ctx, watch, WatchModeNone)
-	} else {
-		err = watchRepoMode(ctx, watch, WatchModeNormal)
+		return watchRepoMode(ctx, watch, WatchModeNone)
 	}
-	return err
+
+	if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+		return user_model.ErrBlockedUser
+	}
+
+	return watchRepoMode(ctx, watch, WatchModeNormal)
 }
 
 // GetWatchers returns all watchers of given repository.
diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go
index 7aa899291c..a95a267961 100644
--- a/models/repo/watch_test.go
+++ b/models/repo/watch_test.go
@@ -9,6 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/stretchr/testify/assert"
@@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
+
 	watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 	assert.NoError(t, err)
 	assert.Len(t, watchers, repo.NumWatches)
@@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) {
 	assert.Len(t, watchers, prevCount+1)
 
 	// Should remove watch, inhibit from adding auto
-	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false))
+	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false))
 	watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 	assert.NoError(t, err)
 	assert.Len(t, watchers, prevCount)
@@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, watchers, prevCount)
 }
-
-func TestWatchRepoMode(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-}
diff --git a/models/repo_transfer.go b/models/repo_transfer.go
index 676e2dbb63..747ec2f248 100644
--- a/models/repo_transfer.go
+++ b/models/repo_transfer.go
@@ -13,6 +13,8 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/builder"
 )
 
 // RepoTransfer is used to manage repository transfers
@@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.
 	return allowed
 }
 
+type PendingRepositoryTransferOptions struct {
+	RepoID      int64
+	SenderID    int64
+	RecipientID int64
+}
+
+func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.RepoID != 0 {
+		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+	}
+	if opts.SenderID != 0 {
+		cond = cond.And(builder.Eq{"doer_id": opts.SenderID})
+	}
+	if opts.RecipientID != 0 {
+		cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID})
+	}
+	return cond
+}
+
+func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) {
+	transfers := make([]*RepoTransfer, 0, 10)
+	return transfers, db.GetEngine(ctx).
+		Where(opts.ToConds()).
+		Find(&transfers)
+}
+
 // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
 // process for the repository
 func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) {
-	transfer := new(RepoTransfer)
-
-	has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer)
+	transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
 	if err != nil {
 		return nil, err
 	}
 
-	if !has {
+	if len(transfers) != 1 {
 		return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
 	}
 
-	return transfer, nil
+	return transfers[0], nil
 }
 
 func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
diff --git a/models/user/block.go b/models/user/block.go
new file mode 100644
index 0000000000..5f2b65a199
--- /dev/null
+++ b/models/user/block.go
@@ -0,0 +1,123 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
+
+	"xorm.io/builder"
+)
+
+var (
+	ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
+	ErrCanNotBlock       = util.NewInvalidArgumentErrorf("cannot block the user")
+	ErrCanNotUnblock     = util.NewInvalidArgumentErrorf("cannot unblock the user")
+	ErrBlockedUser       = util.NewPermissionDeniedErrorf("user is blocked")
+)
+
+type Blocking struct {
+	ID          int64 `xorm:"pk autoincr"`
+	BlockerID   int64 `xorm:"UNIQUE(block)"`
+	Blocker     *User `xorm:"-"`
+	BlockeeID   int64 `xorm:"UNIQUE(block)"`
+	Blockee     *User `xorm:"-"`
+	Note        string
+	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+	return "user_blocking"
+}
+
+func init() {
+	db.RegisterModel(new(Blocking))
+}
+
+func UpdateBlockingNote(ctx context.Context, id int64, note string) error {
+	_, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note})
+	return err
+}
+
+func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool {
+	if len(blockerIDs) == 0 {
+		return false
+	}
+
+	if blockee.IsAdmin {
+		return false
+	}
+
+	cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}.
+		And(builder.In("user_blocking.blocker_id", blockerIDs))
+
+	has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{})
+	return has
+}
+
+type FindBlockingOptions struct {
+	db.ListOptions
+	BlockerID int64
+	BlockeeID int64
+}
+
+func (opts *FindBlockingOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.BlockerID != 0 {
+		cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID})
+	}
+	if opts.BlockeeID != 0 {
+		cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID})
+	}
+	return cond
+}
+
+func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) {
+	return db.FindAndCount[Blocking](ctx, opts)
+}
+
+func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) {
+	blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{
+		BlockerID: blockerID,
+		BlockeeID: blockeeID,
+	})
+	if err != nil {
+		return nil, err
+	}
+	if len(blocks) == 0 {
+		return nil, nil
+	}
+	return blocks[0], nil
+}
+
+type BlockingList []*Blocking
+
+func (blocks BlockingList) LoadAttributes(ctx context.Context) error {
+	ids := make(container.Set[int64], len(blocks)*2)
+	for _, b := range blocks {
+		ids.Add(b.BlockerID)
+		ids.Add(b.BlockeeID)
+	}
+
+	userList, err := GetUsersByIDs(ctx, ids.Values())
+	if err != nil {
+		return err
+	}
+
+	userMap := make(map[int64]*User, len(userList))
+	for _, u := range userList {
+		userMap[u.ID] = u
+	}
+
+	for _, b := range blocks {
+		b.Blocker = userMap[b.BlockerID]
+		b.Blockee = userMap[b.BlockeeID]
+	}
+
+	return nil
+}
diff --git a/models/user/follow.go b/models/user/follow.go
index f4dd2891ff..cf9672109a 100644
--- a/models/user/follow.go
+++ b/models/user/follow.go
@@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
 }
 
 // FollowUser marks someone be another's follower.
-func FollowUser(ctx context.Context, userID, followID int64) (err error) {
-	if userID == followID || IsFollowing(ctx, userID, followID) {
+func FollowUser(ctx context.Context, user, follow *User) (err error) {
+	if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
 		return nil
 	}
 
+	if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) {
+		return ErrBlockedUser
+	}
+
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
 	}
 	defer committer.Close()
 
-	if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil {
+	if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil {
 		return err
 	}
 
-	if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
+	if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil {
 		return err
 	}
 
-	if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
+	if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil {
 		return err
 	}
 	return committer.Commit()
diff --git a/models/user/user.go b/models/user/user.go
index a898e71a2d..2e1d6af176 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -1167,7 +1167,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
 			return false
 		}
 
-		// If they follow - they see each over
+		// If they follow - they see each other
 		follower := IsFollowing(ctx, u.ID, viewer.ID)
 		if follower {
 			return true
diff --git a/models/user/user_test.go b/models/user/user_test.go
index f522f743d5..f4efd071ea 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -399,14 +399,19 @@ func TestGetUserByOpenID(t *testing.T) {
 func TestFollowUser(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testSuccess := func(followerID, followedID int64) {
-		assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
-		unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
+	testSuccess := func(follower, followed *user_model.User) {
+		assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed))
+		unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
 	}
-	testSuccess(4, 2)
-	testSuccess(5, 2)
 
-	assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+	testSuccess(user4, user2)
+	testSuccess(user5, user2)
+
+	assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2))
 
 	unittest.CheckConsistencyFor(t, &user_model.User{})
 }
diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go
index ebe14e3a4c..f71c58fbdf 100644
--- a/modules/repository/collaborator.go
+++ b/modules/repository/collaborator.go
@@ -16,6 +16,14 @@ import (
 )
 
 func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
+	if err := repo.LoadOwner(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
+		return user_model.ErrBlockedUser
+	}
+
 	return db.WithTx(ctx, func(ctx context.Context) error {
 		has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
 			"repo_id": repo.ID,
diff --git a/modules/repository/create.go b/modules/repository/create.go
index f009c0880d..4f18b9b3fa 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -153,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
 	}
 
 	if setting.Service.AutoWatchNewRepos {
-		if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+		if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
 			return fmt.Errorf("WatchRepo: %w", err)
 		}
 	}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index c8c8f2dfeb..255fed28ad 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -632,6 +632,30 @@ form.name_reserved = The username "%s" is reserved.
 form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
 form.name_chars_not_allowed = User name "%s" contains invalid characters.
 
+block.block = Block
+block.block.user = Block user
+block.block.org = Block user for organization
+block.block.failure = Failed to block user: %s
+block.unblock = Unblock
+block.unblock.failure = Failed to unblock user: %s
+block.blocked = You have blocked this user.
+block.title = Block a user
+block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
+block.info_1 = Blocking a user prevents the following actions on your account and your repositories:
+block.info_2 = following your account
+block.info_3 = send you notifications by @mentioning your username
+block.info_4 = inviting you as a collaborator to their repositories
+block.info_5 = starring, forking or watching on repositories
+block.info_6 = opening and commenting on issues or pull requests
+block.info_7 = reacting on your comments in issues or pull requests
+block.user_to_block = User to block
+block.note = Note
+block.note.title = Optional note:
+block.note.info = The note is not visible to the blocked user.
+block.note.edit = Edit note
+block.list = Blocked users
+block.list.none = You have not blocked any users. 
+
 [settings]
 profile = Profile
 account = Account
@@ -969,6 +993,7 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed
 fork_branch = Branch to be cloned to the fork
 all_branches = All branches
 fork_no_valid_owners = This repository can not be forked because there are no valid owners.
+fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner.
 use_template = Use this template
 open_with_editor = Open with %s
 download_zip = Download ZIP
@@ -1144,6 +1169,7 @@ watch = Watch
 unstar = Unstar
 star = Star
 fork = Fork
+action.blocked_user = Cannot perform action because you are blocked by the repository owner.
 download_archive = Download Repository
 more_operations = More Operations
 
@@ -1394,6 +1420,8 @@ issues.new.assignees = Assignees
 issues.new.clear_assignees = Clear assignees
 issues.new.no_assignees = No Assignees
 issues.new.no_reviewers = No reviewers
+issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
+issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
 issues.choose.get_started = Get Started
 issues.choose.open_external_link = Open
 issues.choose.blank = Default
@@ -1509,6 +1537,7 @@ issues.close_comment_issue = Comment and Close
 issues.reopen_issue = Reopen
 issues.reopen_comment_issue = Comment and Reopen
 issues.create_comment = Comment
+issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
 issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@@ -1707,6 +1736,7 @@ compare.compare_head = compare
 
 pulls.desc = Enable pull requests and code reviews.
 pulls.new = New Pull Request
+pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
 pulls.view = View Pull Request
 pulls.compare_changes = New Pull Request
 pulls.allow_edits_from_maintainers = Allow edits from maintainers
@@ -2120,6 +2150,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos
 settings.transfer = Transfer Ownership
 settings.transfer.rejected = Repository transfer was rejected.
 settings.transfer.success = Repository transfer was successful.
+settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner.
 settings.transfer_abort = Cancel transfer
 settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer.
 settings.transfer_abort_success = The repository transfer to %s was successfully canceled.
@@ -2165,6 +2196,7 @@ settings.add_collaborator_success = The collaborator has been added.
 settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator.
 settings.add_collaborator_owner = Cannot add an owner as a collaborator.
 settings.add_collaborator_duplicate = The collaborator is already added to this repository.
+settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa.
 settings.delete_collaborator = Remove
 settings.collaborator_deletion = Remove Collaborator
 settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
@@ -2731,6 +2763,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist,
 teams.add_duplicate_users = User is already a team member.
 teams.repos.none = No repositories could be accessed by this team.
 teams.members.none = No members on this team.
+teams.members.blocked_user = Cannot add the user because it is blocked by the organization.
 teams.specific_repositories = Specific repositories
 teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
 teams.all_repositories = All repositories
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 1587d413f5..c65650c388 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1027,7 +1027,16 @@ func Routes() *web.Route {
 			m.Group("/avatar", func() {
 				m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
 				m.Delete("", user.DeleteAvatar)
-			}, reqToken())
+			})
+
+			m.Group("/blocks", func() {
+				m.Get("", user.ListBlocks)
+				m.Group("/{username}", func() {
+					m.Get("", user.CheckUserBlock)
+					m.Put("", user.BlockUser)
+					m.Delete("", user.UnblockUser)
+				}, context.UserAssignmentAPI())
+			})
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 
 		// Repositories (requires repo scope, org scope)
@@ -1477,6 +1486,15 @@ func Routes() *web.Route {
 				m.Delete("", org.DeleteAvatar)
 			}, reqToken(), reqOrgOwnership())
 			m.Get("/activities/feeds", org.ListOrgActivityFeeds)
+
+			m.Group("/blocks", func() {
+				m.Get("", org.ListBlocks)
+				m.Group("/{username}", func() {
+					m.Get("", org.CheckUserBlock)
+					m.Put("", org.BlockUser)
+					m.Delete("", org.UnblockUser)
+				})
+			}, reqToken(), reqOrgOwnership())
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
 		m.Group("/teams/{teamid}", func() {
 			m.Combo("").Get(reqToken(), org.GetTeam).
diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go
new file mode 100644
index 0000000000..69a5222a20
--- /dev/null
+++ b/routers/api/v1/org/block.go
@@ -0,0 +1,116 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
+	// ---
+	// summary: List users blocked by the organization
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// produces:
+	// - application/json
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/UserList"
+
+	shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
+	// ---
+	// summary: Check if a user is blocked by the organization
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: username
+	//   in: path
+	//   description: user to check
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
+}
+
+func BlockUser(ctx *context.APIContext) {
+	// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
+	// ---
+	// summary: Block a user
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: username
+	//   in: path
+	//   description: user to block
+	//   type: string
+	//   required: true
+	// - name: note
+	//   in: query
+	//   description: optional note for the block
+	//   type: string
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
+}
+
+func UnblockUser(ctx *context.APIContext) {
+	// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
+	// ---
+	// summary: Unblock a user
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: username
+	//   in: path
+	//   description: user to unblock
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
+}
diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go
index fb66d4c3f5..9db9ad964b 100644
--- a/routers/api/v1/org/member.go
+++ b/routers/api/v1/org/member.go
@@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
 	if ctx.Written() {
 		return
 	}
-	if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
+	if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
 		ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
 	}
 	ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index b62a386fd7..015af774e3 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -15,6 +15,7 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
@@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
@@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
 	if ctx.Written() {
 		return
 	}
-	if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
-		ctx.Error(http.StatusInternalServerError, "AddMember", err)
+	if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "AddTeamMember", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
+		}
 		return
 	}
 	ctx.Status(http.StatusNoContent)
@@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
+	if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
 		ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
 		return
 	}
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index 7d48d71516..4ce14f7d01 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -54,15 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{
-		RepoID: ctx.Repo.Repository.ID,
+	collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
+		ListOptions: utils.GetListOptions(ctx),
+		RepoID:      ctx.Repo.Repository.ID,
 	})
-	if err != nil {
-		ctx.InternalServerError(err)
-		return
-	}
-
-	collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
 		return
@@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
 		users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
 	}
 
-	ctx.SetTotalCountHeader(count)
+	ctx.SetTotalCountHeader(total)
 	ctx.JSON(http.StatusOK, users)
 }
 
@@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 	//   "422":
@@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
 	}
 
 	if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
-		ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "AddCollaborator", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+		}
 		return
 	}
 
@@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
 		return
 	}
 
-	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
+	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
 		return
 	}
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 212cc7a93b..a1e3c9804b 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
 	if err != nil {
 		if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
 			ctx.Error(http.StatusConflict, "ForkRepository", err)
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "ForkRepository", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
 		}
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 1b2ecd474b..d43711e362 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -5,6 +5,7 @@
 package repo
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -653,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 	//   "423":
 	//     "$ref": "#/responses/repoArchivedError"
+
 	form := web.GetForm(ctx).(*api.CreateIssueOption)
 	var deadlineUnix timeutil.TimeStamp
 	if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
@@ -710,9 +712,11 @@ func CreateIssue(ctx *context.APIContext) {
 	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
-			return
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "NewIssue", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "NewIssue", err)
 		}
-		ctx.Error(http.StatusInternalServerError, "NewIssue", err)
 		return
 	}
 
@@ -848,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
 
 		err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
 		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+			if errors.Is(err, user_model.ErrBlockedUser) {
+				ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
+			} else {
+				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+			}
 			return
 		}
 	}
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 6209e960af..21aabadf3d 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -382,6 +382,7 @@ func CreateIssueComment(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 	//   "423":
 	//     "$ref": "#/responses/repoArchivedError"
+
 	form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
@@ -401,7 +402,11 @@ func CreateIssueComment(ctx *context.APIContext) {
 
 	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+		}
 		return
 	}
 
@@ -522,6 +527,7 @@ func EditIssueComment(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 	//   "423":
 	//     "$ref": "#/responses/repoArchivedError"
+
 	form := web.GetForm(ctx).(*api.EditIssueCommentOption)
 	editIssueComment(ctx, *form)
 }
@@ -610,7 +616,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
 	oldContent := comment.Content
 	comment.Content = form.Body
 	if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
-		ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "UpdateComment", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+		}
 		return
 	}
 
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index e7436db798..4096cbf07b 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -4,10 +4,12 @@
 package repo
 
 import (
+	"errors"
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
@@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 	//     "$ref": "#/responses/Attachment"
 	//   "400":
 	//     "$ref": "#/responses/error"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/error"
 	//   "423":
@@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 	}
 
 	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
-		ctx.ServerError("UpdateComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "UpdateComment", err)
+		} else {
+			ctx.ServerError("UpdateComment", err)
+		}
 		return
 	}
 
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
index 799c687812..3ff3d19f13 100644
--- a/routers/api/v1/repo/issue_reaction.go
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -8,11 +8,13 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
+	issue_service "code.gitea.io/gitea/services/issue"
 )
 
 // GetIssueCommentReactions list reactions of a comment from an issue
@@ -218,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
 
 	if isCreateType {
 		// PostIssueCommentReaction part
-		reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
+		reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.Error(http.StatusForbidden, err.Error(), err)
 			} else if issues_model.IsErrReactionAlreadyExist(err) {
 				ctx.JSON(http.StatusOK, api.Reaction{
@@ -434,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 
 	if isCreateType {
 		// PostIssueReaction part
-		reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
+		reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.Error(http.StatusForbidden, err.Error(), err)
 			} else if issues_model.IsErrReactionAlreadyExist(err) {
 				ctx.JSON(http.StatusOK, api.Reaction{
@@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 					Created:  reaction.CreatedUnix.AsTime(),
 				})
 			} else {
-				ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
+				ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
 			}
 			return
 		}
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 8f9848f71d..4cb94b11a2 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -362,6 +362,8 @@ func CreatePullRequest(ctx *context.APIContext) {
 	// responses:
 	//   "201":
 	//     "$ref": "#/responses/PullRequest"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 	//   "409":
@@ -510,9 +512,11 @@ func CreatePullRequest(ctx *context.APIContext) {
 	if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
-			return
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
 		}
-		ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
 		return
 	}
 
@@ -630,6 +634,8 @@ func EditPullRequest(ctx *context.APIContext) {
 		if err != nil {
 			if user_model.IsErrUserNotExist(err) {
 				ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
+			} else if errors.Is(err, user_model.ErrBlockedUser) {
+				ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
 			} else {
 				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
 			}
diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go
index 4f05c0df51..776b336761 100644
--- a/routers/api/v1/repo/transfer.go
+++ b/routers/api/v1/repo/transfer.go
@@ -4,6 +4,7 @@
 package repo
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 
@@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) {
 			return
 		}
 
-		ctx.InternalServerError(err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.InternalServerError(err)
+		}
 		return
 	}
 
diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go
new file mode 100644
index 0000000000..a1e65625ed
--- /dev/null
+++ b/routers/api/v1/shared/block.go
@@ -0,0 +1,98 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package shared
+
+import (
+	"errors"
+	"net/http"
+
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/convert"
+	user_service "code.gitea.io/gitea/services/user"
+)
+
+func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
+	blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+		ListOptions: utils.GetListOptions(ctx),
+		BlockerID:   blocker.ID,
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindBlockings", err)
+		return
+	}
+
+	if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+		return
+	}
+
+	users := make([]*api.User, 0, len(blocks))
+	for _, b := range blocks {
+		users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
+	}
+
+	ctx.SetTotalCountHeader(total)
+	ctx.JSON(http.StatusOK, &users)
+}
+
+func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
+	blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+	if err != nil {
+		ctx.NotFound("GetUserByName", err)
+		return
+	}
+
+	status := http.StatusNotFound
+	blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetBlocking", err)
+		return
+	}
+	if blocking != nil {
+		status = http.StatusNoContent
+	}
+
+	ctx.Status(status)
+}
+
+func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
+	blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+	if err != nil {
+		ctx.NotFound("GetUserByName", err)
+		return
+	}
+
+	if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
+		if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+			ctx.Error(http.StatusBadRequest, "BlockUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "BlockUser", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
+	blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+	if err != nil {
+		ctx.NotFound("GetUserByName", err)
+		return
+	}
+
+	if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
+		if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+			ctx.Error(http.StatusBadRequest, "UnblockUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UnblockUser", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go
new file mode 100644
index 0000000000..7231e9add7
--- /dev/null
+++ b/routers/api/v1/user/block.go
@@ -0,0 +1,96 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+	// swagger:operation GET /user/blocks user userListBlocks
+	// ---
+	// summary: List users blocked by the authenticated user
+	// parameters:
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// produces:
+	// - application/json
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/UserList"
+
+	shared.ListBlocks(ctx, ctx.Doer)
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+	// swagger:operation GET /user/blocks/{username} user userCheckUserBlock
+	// ---
+	// summary: Check if a user is blocked by the authenticated user
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: user to check
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	shared.CheckUserBlock(ctx, ctx.Doer)
+}
+
+func BlockUser(ctx *context.APIContext) {
+	// swagger:operation PUT /user/blocks/{username} user userBlockUser
+	// ---
+	// summary: Block a user
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: user to block
+	//   type: string
+	//   required: true
+	// - name: note
+	//   in: query
+	//   description: optional note for the block
+	//   type: string
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.BlockUser(ctx, ctx.Doer)
+}
+
+func UnblockUser(ctx *context.APIContext) {
+	// swagger:operation DELETE /user/blocks/{username} user userUnblockUser
+	// ---
+	// summary: Unblock a user
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: user to unblock
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.UnblockUser(ctx, ctx.Doer, ctx.Doer)
+}
diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go
index 398c6b2567..6abb70de19 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -5,6 +5,7 @@
 package user
 
 import (
+	"errors"
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
@@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
-		ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+	if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "FollowUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+		}
 		return
 	}
 	ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index e624884db3..ad9ed9548d 100644
--- a/routers/api/v1/user/star.go
+++ b/routers/api/v1/user/star.go
@@ -5,10 +5,9 @@
 package user
 
 import (
-	std_context "context"
+	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models/db"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -20,8 +19,12 @@ import (
 
 // getStarredRepos returns the repos that the user with the specified userID has
 // starred
-func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
-	starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
+func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
+	starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
+		ListOptions:    utils.GetListOptions(ctx),
+		StarrerID:      user.ID,
+		IncludePrivate: private,
+	})
 	if err != nil {
 		return nil, err
 	}
@@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	private := ctx.ContextUser.ID == ctx.Doer.ID
-	repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+	repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 		return
@@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/RepositoryList"
 
-	repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+	repos, err := getStarredRepos(ctx, ctx.Doer, true)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 	}
@@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+	err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+		}
 		return
 	}
 	ctx.Status(http.StatusNoContent)
@@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+	err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "StarRepo", err)
 		return
diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go
index 706f4cc66b..2cc23ae476 100644
--- a/routers/api/v1/user/watch.go
+++ b/routers/api/v1/user/watch.go
@@ -4,10 +4,9 @@
 package user
 
 import (
-	std_context "context"
+	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models/db"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -18,8 +17,12 @@ import (
 )
 
 // getWatchedRepos returns the repos that the user with the specified userID is watching
-func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
-	watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions)
+func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
+	watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
+		ListOptions:    utils.GetListOptions(ctx),
+		WatcherID:      user.ID,
+		IncludePrivate: private,
+	})
 	if err != nil {
 		return nil, 0, err
 	}
@@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	private := ctx.ContextUser.ID == ctx.Doer.ID
-	repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+	repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 	}
@@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/RepositoryList"
 
-	repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+	repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 	}
@@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) {
 	// responses:
 	//   "200":
 	//     "$ref": "#/responses/WatchInfo"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+	err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+		}
 		return
 	}
 	ctx.JSON(http.StatusOK, api.WatchInfo{
@@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+	err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
 		return
diff --git a/routers/web/org/block.go b/routers/web/org/block.go
new file mode 100644
index 0000000000..d40458e250
--- /dev/null
+++ b/routers/web/org/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
+)
+
+const (
+	tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("user.block.list")
+	ctx.Data["PageIsOrgSettings"] = true
+	ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+	shared_user.BlockedUsers(ctx, ctx.ContextUser)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+	shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
+}
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 9a3d60e122..63ac57cf0d 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/organization"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -78,40 +79,43 @@ func Members(ctx *context.Context) {
 
 // MembersAction response for operation to a member of organization
 func MembersAction(ctx *context.Context) {
-	uid := ctx.FormInt64("uid")
-	if uid == 0 {
+	member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+	if err != nil {
+		log.Error("GetUserByID: %v", err)
+	}
+	if member == nil {
 		ctx.Redirect(ctx.Org.OrgLink + "/members")
 		return
 	}
 
 	org := ctx.Org.Organization
-	var err error
+
 	switch ctx.Params(":action") {
 	case "private":
-		if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+		if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
+		err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
 	case "public":
-		if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+		if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
+		err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
 	case "remove":
 		if !ctx.Org.IsOwner {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = models.RemoveOrgUser(ctx, org.ID, uid)
+		err = models.RemoveOrgUser(ctx, org, member)
 		if organization.IsErrLastOrgOwner(err) {
 			ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 			ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
 			return
 		}
 	case "leave":
-		err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
+		err = models.RemoveOrgUser(ctx, org, ctx.Doer)
 		if err == nil {
 			ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
 			ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index fd7486cacd..144d9b1b43 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -5,6 +5,7 @@
 package org
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -77,9 +78,9 @@ func TeamsAction(ctx *context.Context) {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+		err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
 	case "leave":
-		err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+		err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
 		if err != nil {
 			if org_model.IsErrLastOrgOwner(err) {
 				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -100,13 +101,13 @@ func TeamsAction(ctx *context.Context) {
 			return
 		}
 
-		uid := ctx.FormInt64("uid")
-		if uid == 0 {
+		user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+		if user == nil {
 			ctx.Redirect(ctx.Org.OrgLink + "/teams")
 			return
 		}
 
-		err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
+		err = models.RemoveTeamMember(ctx, ctx.Org.Team, user)
 		if err != nil {
 			if org_model.IsErrLastOrgOwner(err) {
 				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -161,7 +162,7 @@ func TeamsAction(ctx *context.Context) {
 		if ctx.Org.Team.IsMember(ctx, u.ID) {
 			ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
 		} else {
-			err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
+			err = models.AddTeamMember(ctx, ctx.Org.Team, u)
 		}
 
 		page = "team"
@@ -189,6 +190,8 @@ func TeamsAction(ctx *context.Context) {
 	if err != nil {
 		if org_model.IsErrLastOrgOwner(err) {
 			ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
 		} else {
 			log.Error("Action(%s): %v", ctx.Params(":action"), err)
 			ctx.JSON(http.StatusOK, map[string]any{
@@ -590,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) {
 		return
 	}
 
-	if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil {
+	if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil {
 		ctx.ServerError("AddTeamMember", err)
 		return
 	}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index b8c7f70aa6..45fd01f4da 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -57,6 +57,7 @@ import (
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
+	user_service "code.gitea.io/gitea/services/user"
 )
 
 const (
@@ -1258,9 +1259,11 @@ func NewIssuePost(ctx *context.Context) {
 	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
-			return
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
+		} else {
+			ctx.ServerError("NewIssue", err)
 		}
-		ctx.ServerError("NewIssue", err)
 		return
 	}
 
@@ -2047,6 +2050,10 @@ func ViewIssue(ctx *context.Context) {
 	}
 	ctx.Data["Tags"] = tags
 
+	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+	}
+
 	ctx.HTML(http.StatusOK, tplIssueView)
 }
 
@@ -2250,7 +2257,11 @@ func UpdateIssueContent(ctx *context.Context) {
 	}
 
 	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
-		ctx.ServerError("ChangeContent", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
+		} else {
+			ctx.ServerError("ChangeContent", err)
+		}
 		return
 	}
 
@@ -3108,7 +3119,11 @@ func NewComment(ctx *context.Context) {
 
 	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
 	if err != nil {
-		ctx.ServerError("CreateIssueComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+		} else {
+			ctx.ServerError("CreateIssueComment", err)
+		}
 		return
 	}
 
@@ -3152,7 +3167,11 @@ func UpdateCommentContent(ctx *context.Context) {
 		return
 	}
 	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
-		ctx.ServerError("UpdateComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+		} else {
+			ctx.ServerError("UpdateComment", err)
+		}
 		return
 	}
 
@@ -3260,9 +3279,9 @@ func ChangeIssueReaction(ctx *context.Context) {
 
 	switch ctx.Params(":action") {
 	case "react":
-		reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content)
+		reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.ServerError("ChangeIssueReaction", err)
 				return
 			}
@@ -3367,9 +3386,9 @@ func ChangeCommentReaction(ctx *context.Context) {
 
 	switch ctx.Params(":action") {
 	case "react":
-		reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
+		reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.ServerError("ChangeIssueReaction", err)
 				return
 			}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index bf52d76e95..ed063715e5 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -47,6 +47,7 @@ import (
 	notify_service "code.gitea.io/gitea/services/notify"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
+	user_service "code.gitea.io/gitea/services/user"
 
 	"github.com/gobwas/glob"
 )
@@ -308,6 +309,8 @@ func ForkPost(ctx *context.Context) {
 			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
 		case db.IsErrNamePatternNotAllowed(err):
 			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+		case errors.Is(err, user_model.ErrBlockedUser):
+			ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
 		default:
 			ctx.ServerError("ForkPost", err)
 		}
@@ -1065,6 +1068,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 	}
 	upload.AddUploadContext(ctx, "comment")
 
+	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+	}
+
 	ctx.HTML(http.StatusOK, tplPullFiles)
 }
 
@@ -1483,7 +1490,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 	if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
-			return
 		} else if git.IsErrPushRejected(err) {
 			pushrejErr := err.(*git.ErrPushRejected)
 			message := pushrejErr.Message
@@ -1501,9 +1507,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 				return
 			}
 			ctx.JSONError(flashError)
-			return
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+				"Message": ctx.Tr("repo.pulls.push_rejected"),
+				"Summary": ctx.Tr("repo.pulls.new.blocked_user"),
+			})
+			if err != nil {
+				ctx.ServerError("CompareAndPullRequest.HTMLString", err)
+				return
+			}
+			ctx.JSONError(flashError)
 		}
-		ctx.ServerError("NewPullRequest", err)
 		return
 	}
 
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 49779efa37..f0caf199a2 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -313,13 +313,13 @@ func Action(ctx *context.Context) {
 	var err error
 	switch ctx.Params(":action") {
 	case "watch":
-		err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+		err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	case "unwatch":
-		err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+		err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	case "star":
-		err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	case "unstar":
-		err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	case "accept_transfer":
 		err = acceptOrRejectRepoTransfer(ctx, true)
 	case "reject_transfer":
@@ -336,8 +336,12 @@ func Action(ctx *context.Context) {
 	}
 
 	if err != nil {
-		ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
-		return
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
+		} else {
+			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
+			return
+		}
 	}
 
 	switch ctx.Params(":action") {
diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go
index 6bfd485566..31f9f76d0f 100644
--- a/routers/web/repo/setting/collaboration.go
+++ b/routers/web/repo/setting/collaboration.go
@@ -4,10 +4,10 @@
 package setting
 
 import (
+	"errors"
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
 	ctx.Data["PageIsSettingsCollaboration"] = true
 
-	users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
+	users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID})
 	if err != nil {
 		ctx.ServerError("GetCollaborators", err)
 		return
@@ -101,7 +101,12 @@ func CollaborationPost(ctx *context.Context) {
 	}
 
 	if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
-		ctx.ServerError("AddCollaborator", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
+			ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+		} else {
+			ctx.ServerError("AddCollaborator", err)
+		}
 		return
 	}
 
@@ -126,10 +131,19 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
 
 // DeleteCollaboration delete a collaboration for a repository
 func DeleteCollaboration(ctx *context.Context) {
-	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
-		ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+	if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
+		if user_model.IsErrUserNotExist(err) {
+			ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
+		} else {
+			ctx.ServerError("GetUserByName", err)
+			return
+		}
 	} else {
-		ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+		if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
+			ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+		} else {
+			ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+		}
 	}
 
 	ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 3af0ddb578..992a980d9e 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -5,6 +5,7 @@
 package setting
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -782,6 +783,8 @@ func SettingsPost(ctx *context.Context) {
 				ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
 			} else if models.IsErrRepoTransferInProgress(err) {
 				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+			} else if errors.Is(err, user_model.ErrBlockedUser) {
+				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
 			} else {
 				ctx.ServerError("TransferOwnership", err)
 			}
diff --git a/routers/web/shared/user/block.go b/routers/web/shared/user/block.go
new file mode 100644
index 0000000000..8a2357623f
--- /dev/null
+++ b/routers/web/shared/user/block.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"errors"
+
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/forms"
+	user_service "code.gitea.io/gitea/services/user"
+)
+
+func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
+	blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+		BlockerID: blocker.ID,
+	})
+	if err != nil {
+		ctx.ServerError("FindBlockings", err)
+		return
+	}
+	if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+		ctx.ServerError("LoadAttributes", err)
+		return
+	}
+	ctx.Data["UserBlocks"] = blocks
+}
+
+func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
+	form := web.GetForm(ctx).(*forms.BlockUserForm)
+	if ctx.HasError() {
+		ctx.ServerError("FormValidation", nil)
+		return
+	}
+
+	blockee, err := user_model.GetUserByName(ctx, form.Blockee)
+	if err != nil {
+		ctx.ServerError("GetUserByName", nil)
+		return
+	}
+
+	switch form.Action {
+	case "block":
+		if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
+			if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+				ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
+			} else {
+				ctx.ServerError("BlockUser", err)
+				return
+			}
+		}
+	case "unblock":
+		if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
+			if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+				ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
+			} else {
+				ctx.ServerError("UnblockUser", err)
+				return
+			}
+		}
+	case "note":
+		block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+		if err != nil {
+			ctx.ServerError("GetBlocking", err)
+			return
+		}
+		if block != nil {
+			if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
+				ctx.ServerError("UpdateBlockingNote", err)
+				return
+			}
+		}
+	}
+}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 3bc1adae99..2d6d9ad98d 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -72,6 +72,14 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 	if _, ok := ctx.Data["NumFollowing"]; !ok {
 		_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
 	}
+
+	if ctx.Doer != nil {
+		if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
+			ctx.ServerError("GetBlocking", err)
+		} else {
+			ctx.Data["UserBlocking"] = block
+		}
+	}
 }
 
 func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 833312c501..9851ea90a6 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -339,7 +339,7 @@ func Action(ctx *context.Context) {
 	var err error
 	switch ctx.FormString("action") {
 	case "follow":
-		err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+		err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser)
 	case "unfollow":
 		err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 	}
diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go
new file mode 100644
index 0000000000..94fc380cee
--- /dev/null
+++ b/routers/web/user/setting/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/setting"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
+)
+
+const (
+	tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("user.block.list")
+	ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+	shared_user.BlockedUsers(ctx, ctx.Doer)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+	shared_user.BlockedUsersPost(ctx, ctx.Doer)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 14d31b3a90..8710f6e3e5 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -647,6 +647,11 @@ func registerRoutes(m *web.Route) {
 			})
 			addWebhookEditRoutes()
 		}, webhooksEnabled)
+
+		m.Group("/blocked_users", func() {
+			m.Get("", user_setting.BlockedUsers)
+			m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
+		})
 	}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 
 	m.Group("/user", func() {
@@ -945,6 +950,11 @@ func registerRoutes(m *web.Route) {
 						m.Post("/rebuild", org.RebuildCargoIndex)
 					})
 				}, packagesEnabled)
+
+				m.Group("/blocked_users", func() {
+					m.Get("", org.BlockedUsers)
+					m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
+				})
 			}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
 		}, context.OrgAssignment(true, true))
 	}, reqSignIn)
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
index 3a2411ec55..05293f202f 100644
--- a/services/auth/source/source_group_sync.go
+++ b/services/auth/source/source_group_sync.go
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
 			}
 
 			if action == syncAdd && !isMember {
-				if err := models.AddTeamMember(ctx, team, user.ID); err != nil {
+				if err := models.AddTeamMember(ctx, team, user); err != nil {
 					log.Error("group sync: Could not add user to team: %v", err)
 					return err
 				}
 			} else if action == syncRemove && isMember {
-				if err := models.RemoveTeamMember(ctx, team, user.ID); err != nil {
+				if err := models.RemoveTeamMember(ctx, team, user); err != nil {
 					log.Error("group sync: Could not remove user from team: %v", err)
 					return err
 				}
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 186aa4a878..416592bfda 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -449,3 +449,14 @@ func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) bi
 	ctx := context.GetValidateContext(req)
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 }
+
+type BlockUserForm struct {
+	Action  string `binding:"Required;In(block,unblock,note)"`
+	Blockee string `binding:"Required"`
+	Note    string
+}
+
+func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+	ctx := context.GetValidateContext(req)
+	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 8d8c575c14..d68623aff6 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
 		return fmt.Errorf("cannot create reference with empty commit SHA")
 	}
 
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	// Check if same reference from same commit has already existed.
 	has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
 		Type:      issues_model.CommentTypeCommitRef,
@@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
 
 // CreateIssueComment creates a plain issue comment.
 func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+			return nil, user_model.ErrBlockedUser
+		}
+	}
+
 	comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
 		Type:        issues_model.CommentTypeComment,
 		Doer:        doer,
@@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
 
 // UpdateComment updates information of comment.
 func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
+	if err := c.LoadIssue(ctx); err != nil {
+		return err
+	}
+	if err := c.Issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
 	if needsContentHistory {
 		hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
diff --git a/services/issue/commit.go b/services/issue/commit.go
index e493a03211..0a59088d12 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -5,6 +5,7 @@ package issue
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"html"
 	"net/url"
@@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
 
 			message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
 			if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
+				if errors.Is(err, user_model.ErrBlockedUser) {
+					continue
+				}
 				return err
 			}
 
diff --git a/services/issue/content.go b/services/issue/content.go
index 6e56714ddf..2f9bee806a 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -7,12 +7,23 @@ import (
 	"context"
 
 	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
 	user_model "code.gitea.io/gitea/models/user"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
 // ChangeContent changes issue content, as the given user.
-func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
+func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
+	if err := issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	oldContent := issue.Content
 
 	if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
diff --git a/services/issue/issue.go b/services/issue/issue.go
index b1f418c32e..27a106009c 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -15,6 +15,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	system_model "code.gitea.io/gitea/models/system"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/storage"
 	notify_service "code.gitea.io/gitea/services/notify"
@@ -22,6 +23,14 @@ import (
 
 // NewIssue creates new issue with labels for repository.
 func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
+	if err := issue.LoadPoster(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+		return user_model.ErrBlockedUser
+	}
+
 	if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
 		return err
 	}
@@ -57,6 +66,16 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 		return nil
 	}
 
+	if err := issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
 		return err
 	}
@@ -93,31 +112,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
 // Pass one or more user logins to replace the set of assignees on this Issue.
 // Send an empty array ([]) to clear all assignees from the Issue.
 func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
-	var allNewAssignees []*user_model.User
+	uniqueAssignees := container.SetOf(multipleAssignees...)
 
 	// Keep the old assignee thingy for compatibility reasons
 	if oneAssignee != "" {
-		// Prevent double adding assignees
-		var isDouble bool
-		for _, assignee := range multipleAssignees {
-			if assignee == oneAssignee {
-				isDouble = true
-				break
-			}
-		}
-
-		if !isDouble {
-			multipleAssignees = append(multipleAssignees, oneAssignee)
-		}
+		uniqueAssignees.Add(oneAssignee)
 	}
 
 	// Loop through all assignees to add them
-	for _, assigneeName := range multipleAssignees {
+	allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
+	for _, assigneeName := range uniqueAssignees.Values() {
 		assignee, err := user_model.GetUserByName(ctx, assigneeName)
 		if err != nil {
 			return err
 		}
 
+		if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
+			return user_model.ErrBlockedUser
+		}
+
 		allNewAssignees = append(allNewAssignees, assignee)
 	}
 
diff --git a/services/issue/reaction.go b/services/issue/reaction.go
new file mode 100644
index 0000000000..deb99169e1
--- /dev/null
+++ b/services/issue/reaction.go
@@ -0,0 +1,50 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+	"context"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	user_model "code.gitea.io/gitea/models/user"
+)
+
+// CreateIssueReaction creates a reaction on an issue.
+func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
+	if err := issue.LoadRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+		return nil, user_model.ErrBlockedUser
+	}
+
+	return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+		Type:    content,
+		DoerID:  doer.ID,
+		IssueID: issue.ID,
+	})
+}
+
+// CreateCommentReaction creates a reaction on a comment.
+func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
+	if err := comment.LoadIssue(ctx); err != nil {
+		return nil, err
+	}
+
+	if err := comment.Issue.LoadRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
+		return nil, user_model.ErrBlockedUser
+	}
+
+	return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+		Type:      content,
+		DoerID:    doer.ID,
+		IssueID:   comment.Issue.ID,
+		CommentID: comment.ID,
+	})
+}
diff --git a/models/issues/reaction_test.go b/services/issue/reaction_test.go
similarity index 65%
rename from models/issues/reaction_test.go
rename to services/issue/reaction_test.go
index 5dc8e1a5f3..7734860fc0 100644
--- a/models/issues/reaction_test.go
+++ b/services/issue/reaction_test.go
@@ -1,7 +1,7 @@
 // Copyright 2017 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package issues_test
+package issue
 
 import (
 	"testing"
@@ -16,13 +16,13 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
+func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) {
 	var reaction *issues_model.Reaction
 	var err error
-	if commentID == 0 {
-		reaction, err = issues_model.CreateIssueReaction(db.DefaultContext, doerID, issueID, content)
+	if comment == nil {
+		reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content)
 	} else {
-		reaction, err = issues_model.CreateCommentReaction(db.DefaultContext, doerID, issueID, commentID, content)
+		reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content)
 	}
 	assert.NoError(t, err)
 	assert.NotNil(t, reaction)
@@ -32,32 +32,26 @@ func TestIssueAddReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 
-	var issue1ID int64 = 1
+	addReaction(t, user1, issue, nil, "heart")
 
-	addReaction(t, user1.ID, issue1ID, 0, "heart")
-
-	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 }
 
 func TestIssueAddDuplicateReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 
-	var issue1ID int64 = 1
+	addReaction(t, user1, issue, nil, "heart")
 
-	addReaction(t, user1.ID, issue1ID, 0, "heart")
-
-	reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
-		DoerID:  user1.ID,
-		IssueID: issue1ID,
-		Type:    "heart",
-	})
+	reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart")
 	assert.Error(t, err)
 	assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
 
-	existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 	assert.Equal(t, existingR.ID, reaction.ID)
 }
 
@@ -65,15 +59,14 @@ func TestIssueDeleteReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 
-	var issue1ID int64 = 1
+	addReaction(t, user1, issue, nil, "heart")
 
-	addReaction(t, user1.ID, issue1ID, 0, "heart")
-
-	err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue1ID, "heart")
+	err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart")
 	assert.NoError(t, err)
 
-	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 }
 
 func TestIssueReactionCount(t *testing.T) {
@@ -87,19 +80,19 @@ func TestIssueReactionCount(t *testing.T) {
 	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 	ghost := user_model.NewGhostUser()
 
-	var issueID int64 = 2
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 
-	addReaction(t, user1.ID, issueID, 0, "heart")
-	addReaction(t, user2.ID, issueID, 0, "heart")
-	addReaction(t, org3.ID, issueID, 0, "heart")
-	addReaction(t, org3.ID, issueID, 0, "+1")
-	addReaction(t, user4.ID, issueID, 0, "+1")
-	addReaction(t, user4.ID, issueID, 0, "heart")
-	addReaction(t, ghost.ID, issueID, 0, "-1")
+	addReaction(t, user1, issue, nil, "heart")
+	addReaction(t, user2, issue, nil, "heart")
+	addReaction(t, org3, issue, nil, "heart")
+	addReaction(t, org3, issue, nil, "+1")
+	addReaction(t, user4, issue, nil, "+1")
+	addReaction(t, user4, issue, nil, "heart")
+	addReaction(t, ghost, issue, nil, "-1")
 
 	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
-		IssueID: issueID,
+		IssueID: issue.ID,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, reactionsList, 7)
@@ -122,13 +115,11 @@ func TestIssueCommentAddReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 
-	var issue1ID int64 = 1
-	var comment1ID int64 = 1
+	addReaction(t, user1, nil, comment, "heart")
 
-	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-
-	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
 }
 
 func TestIssueCommentDeleteReaction(t *testing.T) {
@@ -139,17 +130,16 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
 	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 
-	var issue1ID int64 = 1
-	var comment1ID int64 = 1
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 
-	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-	addReaction(t, user2.ID, issue1ID, comment1ID, "heart")
-	addReaction(t, org3.ID, issue1ID, comment1ID, "heart")
-	addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
+	addReaction(t, user1, nil, comment, "heart")
+	addReaction(t, user2, nil, comment, "heart")
+	addReaction(t, org3, nil, comment, "heart")
+	addReaction(t, user4, nil, comment, "+1")
 
 	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
-		IssueID:   issue1ID,
-		CommentID: comment1ID,
+		IssueID:   comment.IssueID,
+		CommentID: comment.ID,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, reactionsList, 4)
@@ -163,12 +153,10 @@ func TestIssueCommentReactionCount(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 
-	var issue1ID int64 = 1
-	var comment1ID int64 = 1
+	addReaction(t, user1, nil, comment, "heart")
+	assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart"))
 
-	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-	assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, issue1ID, comment1ID, "heart"))
-
-	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
 }
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 42363f886d..be3d25d20a 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -40,6 +40,14 @@ var pullWorkingPool = sync.NewExclusivePool()
 
 // NewPullRequest creates new pull request with labels for repository.
 func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
+	if err := issue.LoadPoster(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+		return user_model.ErrBlockedUser
+	}
+
 	prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
 	if err != nil {
 		if !git_model.IsErrBranchNotExist(err) {
diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go
index dccc124748..4a43ae2a28 100644
--- a/services/repository/collaboration.go
+++ b/services/repository/collaboration.go
@@ -11,13 +11,14 @@ import (
 	"code.gitea.io/gitea/models/db"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 )
 
 // DeleteCollaboration removes collaboration relation between the user and repository.
-func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
+func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
 	collaboration := &repo_model.Collaboration{
 		RepoID: repo.ID,
-		UserID: uid,
+		UserID: collaborator.ID,
 	}
 
 	ctx, committer, err := db.TxContext(ctx)
@@ -31,20 +32,25 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i
 	} else if has == 0 {
 		return committer.Commit()
 	}
+
+	if err := repo.LoadOwner(ctx); err != nil {
+		return err
+	}
+
 	if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
 		return err
 	}
 
-	if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+	if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil {
 		return err
 	}
 
-	if err = models.ReconsiderWatches(ctx, repo, uid); err != nil {
+	if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
 		return err
 	}
 
 	// Unassign a user from any issue (s)he has been assigned to in the repository
-	if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil {
+	if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
 		return err
 	}
 
diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go
index c3d006bfd8..a2eb06b81a 100644
--- a/services/repository/collaboration_test.go
+++ b/services/repository/collaboration_test.go
@@ -9,6 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -16,13 +17,15 @@ import (
 func TestRepository_DeleteCollaboration(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
-	assert.NoError(t, repo.LoadOwner(db.DefaultContext))
-	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
-	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
 
-	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
-	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
+	assert.NoError(t, repo.LoadOwner(db.DefaultContext))
+	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
+
+	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
 
 	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 }
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 08d6800ee7..1eeec27660 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -365,24 +365,26 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r
 		}
 	}
 
-	teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
+	teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
+		TeamID: t.ID,
+	})
 	if err != nil {
-		return fmt.Errorf("getTeamUsersByTeamID: %w", err)
+		return fmt.Errorf("GetTeamMembers: %w", err)
 	}
-	for _, teamUser := range teamUsers {
-		has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
+	for _, member := range teamMembers {
+		has, err := access_model.HasAccess(ctx, member.ID, repo)
 		if err != nil {
 			return err
 		} else if has {
 			continue
 		}
 
-		if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
+		if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
 			return err
 		}
 
 		// Remove all IssueWatches a user has subscribed to in the repositories
-		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
+		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
 			return err
 		}
 	}
diff --git a/services/repository/fork.go b/services/repository/fork.go
index f9c13a109e..f074fd1082 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -53,6 +53,14 @@ type ForkRepoOptions struct {
 
 // ForkRepository forks a repository
 func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
+	if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
+		return nil, err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
+		return nil, user_model.ErrBlockedUser
+	}
+
 	// Fork is prohibited, if user has reached maximum limit of repositories
 	if !owner.CanForkRepo() {
 		return nil, repo_model.ErrReachLimitOfRepo{
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index 59a4eb260e..83d3032188 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -139,9 +139,9 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 	}
 
 	// Remove redundant collaborators.
-	collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{})
+	collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
 	if err != nil {
-		return fmt.Errorf("getCollaborators: %w", err)
+		return fmt.Errorf("GetCollaborators: %w", err)
 	}
 
 	// Dummy object.
@@ -201,13 +201,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 		return fmt.Errorf("decrease old owner repository count: %w", err)
 	}
 
-	if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+	if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
 		return fmt.Errorf("watchRepo: %w", err)
 	}
 
 	// Remove watch for organization.
 	if oldOwner.IsOrganization() {
-		if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil {
+		if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
 			return fmt.Errorf("watchRepo [false]: %w", err)
 		}
 	}
@@ -371,6 +371,10 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
 		return TransferOwnership(ctx, doer, newOwner, repo, teams)
 	}
 
+	if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
+		return user_model.ErrBlockedUser
+	}
+
 	// If new owner is an org and user can create repos he can transfer directly too
 	if newOwner.IsOrganization() {
 		allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
diff --git a/services/user/block.go b/services/user/block.go
new file mode 100644
index 0000000000..0b3b618aae
--- /dev/null
+++ b/services/user/block.go
@@ -0,0 +1,308 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	org_model "code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	repo_service "code.gitea.io/gitea/services/repository"
+)
+
+func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+	if blocker.ID == blockee.ID {
+		return false
+	}
+	if doer.ID == blockee.ID {
+		return false
+	}
+
+	if blockee.IsOrganization() {
+		return false
+	}
+
+	if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+		return false
+	}
+
+	if blocker.IsOrganization() {
+		org := org_model.OrgFromUser(blocker)
+		if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
+			return false
+		}
+		if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+			return false
+		}
+	} else if !doer.IsAdmin && doer.ID != blocker.ID {
+		return false
+	}
+
+	return true
+}
+
+func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+	if doer.ID == blockee.ID {
+		return false
+	}
+
+	if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+		return false
+	}
+
+	if blocker.IsOrganization() {
+		org := org_model.OrgFromUser(blocker)
+		if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+			return false
+		}
+	} else if !doer.IsAdmin && doer.ID != blocker.ID {
+		return false
+	}
+
+	return true
+}
+
+func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
+	if blockee.IsOrganization() {
+		return user_model.ErrBlockOrganization
+	}
+
+	if !CanBlockUser(ctx, doer, blocker, blockee) {
+		return user_model.ErrCanNotBlock
+	}
+
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		// unfollow each other
+		if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
+			return err
+		}
+		if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
+			return err
+		}
+
+		// unstar each other
+		if err := unstarRepos(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := unstarRepos(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// unwatch each others repositories
+		if err := unwatchRepos(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := unwatchRepos(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// unassign each other from issues
+		if err := unassignIssues(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := unassignIssues(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// remove each other from repository collaborations
+		if err := removeCollaborations(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := removeCollaborations(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// cancel each other repository transfers
+		if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		return db.Insert(ctx, &user_model.Blocking{
+			BlockerID: blocker.ID,
+			BlockeeID: blockee.ID,
+			Note:      note,
+		})
+	})
+}
+
+func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
+	opts := &repo_model.StarredReposOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		StarrerID:   starrer.ID,
+		RepoOwnerID: repoOwner.ID,
+	}
+
+	for {
+		repos, err := repo_model.GetStarredRepos(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(repos) == 0 {
+			return nil
+		}
+
+		for _, repo := range repos {
+			if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
+	opts := &repo_model.WatchedReposOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		WatcherID:   watcher.ID,
+		RepoOwnerID: repoOwner.ID,
+	}
+
+	for {
+		repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(repos) == 0 {
+			return nil
+		}
+
+		for _, repo := range repos {
+			if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
+	transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
+		SenderID:    sender.ID,
+		RecipientID: recipient.ID,
+	})
+	if err != nil {
+		return err
+	}
+
+	for _, transfer := range transfers {
+		repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
+		if err != nil {
+			return err
+		}
+
+		if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
+	opts := &issues_model.AssignedIssuesOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		AssigneeID:  assignee.ID,
+		RepoOwnerID: repoOwner.ID,
+	}
+
+	for {
+		issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(issues) == 0 {
+			return nil
+		}
+
+		for _, issue := range issues {
+			if err := issue.LoadAssignees(ctx); err != nil {
+				return err
+			}
+
+			if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
+	opts := &repo_model.FindCollaborationOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		CollaboratorID: collaborator.ID,
+		RepoOwnerID:    repoOwner.ID,
+	}
+
+	for {
+		collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(collaborations) == 0 {
+			return nil
+		}
+
+		for _, collaboration := range collaborations {
+			repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
+			if err != nil {
+				return err
+			}
+
+			if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
+	if blockee.IsOrganization() {
+		return user_model.ErrBlockOrganization
+	}
+
+	if !CanUnblockUser(ctx, doer, blocker, blockee) {
+		return user_model.ErrCanNotUnblock
+	}
+
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+		if err != nil {
+			return err
+		}
+		if block != nil {
+			_, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
+			return err
+		}
+		return nil
+	})
+}
diff --git a/services/user/block_test.go b/services/user/block_test.go
new file mode 100644
index 0000000000..aec3e03cf3
--- /dev/null
+++ b/services/user/block_test.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCanBlockUser(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+
+	// Doer can't self block
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1))
+	// Blocker can't be blockee
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2))
+	// Can't block already blocked user
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29))
+	// Blockee can't be an organization
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3))
+	// Doer must be blocker or admin
+	assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29))
+	// Organization can't block a member
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4))
+	// Doer must be organization owner or admin if blocker is an organization
+	assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2))
+
+	assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4))
+	assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4))
+	assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29))
+}
+
+func TestCanUnblockUser(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
+	user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+	org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
+
+	// Doer can't self unblock
+	assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1))
+	// Can't unblock not blocked user
+	assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28))
+	// Doer must be blocker or admin
+	assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29))
+	// Doer must be organization owner or admin if blocker is an organization
+	assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28))
+
+	assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29))
+	assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29))
+	assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28))
+}
diff --git a/services/user/delete.go b/services/user/delete.go
index 000910319a..212cb83e03 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -92,6 +92,8 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 		&pull_model.ReviewState{UserID: u.ID},
 		&user_model.Redirect{RedirectUserID: u.ID},
 		&actions_model.ActionRunner{OwnerID: u.ID},
+		&user_model.Blocking{BlockerID: u.ID},
+		&user_model.Blocking{BlockeeID: u.ID},
 	); err != nil {
 		return fmt.Errorf("deleteBeans: %w", err)
 	}
diff --git a/services/user/user.go b/services/user/user.go
index f2648db409..6604dba4d6 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -188,7 +188,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 				break
 			}
 			for _, org := range orgs {
-				if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
+				if err := models.RemoveOrgUser(ctx, org, u); err != nil {
 					if organization.IsErrLastOrgOwner(err) {
 						err = org_service.DeleteOrganization(ctx, org, true)
 						if err != nil {
diff --git a/services/user/user_test.go b/services/user/user_test.go
index 2ebcded925..f110bd26d0 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -41,7 +41,8 @@ func TestDeleteUser(t *testing.T) {
 		orgUsers := make([]*organization.OrgUser, 0, 10)
 		assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
 		for _, orgUser := range orgUsers {
-			if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil {
+			org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
+			if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
 				assert.True(t, organization.IsErrLastOrgOwner(err))
 				return
 			}
diff --git a/templates/org/settings/blocked_users.tmpl b/templates/org/settings/blocked_users.tmpl
new file mode 100644
index 0000000000..eab5ec0007
--- /dev/null
+++ b/templates/org/settings/blocked_users.tmpl
@@ -0,0 +1,5 @@
+{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked_users")}}
+<div class="org-setting-content">
+	{{template "shared/user/blocked_users" .}}
+</div>
+{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
index 64ae20f0a3..ce792f667c 100644
--- a/templates/org/settings/navbar.tmpl
+++ b/templates/org/settings/navbar.tmpl
@@ -17,6 +17,9 @@
 			{{ctx.Locale.Tr "settings.applications"}}
 		</a>
 		{{end}}
+		<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
+			{{ctx.Locale.Tr "user.block.list"}}
+		</a>
 		{{if .EnablePackages}}
 		<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages">
 			{{ctx.Locale.Tr "packages.title"}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 39cf8755f2..1cb3aaaa21 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -251,5 +251,6 @@
 	{{end}}
 	{{if (not .DiffNotAvailable)}}
 		{{template "repo/issue/view_content/reference_issue_dialog" .}}
+		{{template "shared/user/block_user_dialog" .}}
 	{{end}}
 </div>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 747132931e..edfa9c0bc5 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -170,6 +170,7 @@
 </template>
 
 {{template "repo/issue/view_content/reference_issue_dialog" .}}
+{{template "shared/user/block_user_dialog" .}}
 
 <div class="gt-hidden" id="no-content">
 	<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl
index 4afd73c371..17556d4e48 100644
--- a/templates/repo/issue/view_content/context_menu.tmpl
+++ b/templates/repo/issue/view_content/context_menu.tmpl
@@ -10,16 +10,33 @@
 			{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
 		{{end}}
 		<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
-		{{if and .ctxData.IsSigned (not .ctxData.Repository.IsArchived)}}
-			<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
-			{{if not .ctxData.UnitIssuesGlobalDisabled}}
-				<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
+		{{if .ctxData.IsSigned}}
+			{{$needDivider := false}}
+			{{if not .ctxData.Repository.IsArchived}}
+				{{$needDivider = true}}
+				<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
+				{{if not .ctxData.UnitIssuesGlobalDisabled}}
+					<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
+				{{end}}
+				{{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
+					<div class="divider"></div>
+					<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
+					{{if .delete}}
+						<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
+					{{end}}
+				{{end}}
 			{{end}}
-			{{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
-				<div class="divider"></div>
-				<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
-				{{if .delete}}
-					<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
+			{{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}}
+			{{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}}
+			{{if or $canOrgBlock $canUserBlock}}
+				{{if $needDivider}}
+					<div class="divider"></div>
+				{{end}}
+				{{if $canUserBlock}}
+				<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
+				{{end}}
+				{{if $canOrgBlock}}
+				<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{.ctxData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
 				{{end}}
 			{{end}}
 		{{end}}
diff --git a/templates/shared/user/block_user_dialog.tmpl b/templates/shared/user/block_user_dialog.tmpl
new file mode 100644
index 0000000000..c6db4ca1e4
--- /dev/null
+++ b/templates/shared/user/block_user_dialog.tmpl
@@ -0,0 +1,23 @@
+<div class="ui small modal" id="block-user-modal">
+	<div class="header">{{ctx.Locale.Tr "user.block.title"}}</div>
+	<div class="content">
+		<div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}}</div>
+		<form class="ui form modal-form" method="post">
+			{{.CsrfTokenHtml}}
+			<input type="hidden" name="action" value="block" />
+			<input type="hidden" name="blockee" class="modal-blockee" />
+			<div class="field">
+				<label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="text red modal-blockee-name"></span></label>
+			</div>
+			<div class="field">
+				<label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label>
+				<input id="block-note" name="note">
+				<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+			</div>
+			<div class="text right actions">
+				<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
+				<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
+			</div>
+		</form>
+	</div>
+</div>
diff --git a/templates/shared/user/blocked_users.tmpl b/templates/shared/user/blocked_users.tmpl
new file mode 100644
index 0000000000..b2f0957691
--- /dev/null
+++ b/templates/shared/user/blocked_users.tmpl
@@ -0,0 +1,83 @@
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "user.block.title"}}
+</h4>
+<div class="ui attached segment">
+	<p>{{ctx.Locale.Tr "user.block.info_1"}}</p>
+	<ul>
+		<li>{{ctx.Locale.Tr "user.block.info_2"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_3"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_4"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_5"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_6"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_7"}}</li>
+	</ul>
+</div>
+<div class="ui segment">
+	<form class="ui form ignore-dirty" action="{{$.Link}}" method="post">
+		{{.CsrfTokenHtml}}
+		<input type="hidden" name="action" value="block" />
+		<div id="search-user-box" class="field ui fluid search input">
+			<input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
+			<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
+		</div>
+		<div class="field">
+			<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
+			<input name="note">
+			<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+		</div>
+	</form>
+</div>
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "user.block.list"}}
+</h4>
+<div class="ui attached segment">
+	<div class="flex-list">
+		{{range .UserBlocks}}
+			<div class="flex-item">
+				<div class="flex-item-leading">
+					{{ctx.AvatarUtils.Avatar .Blockee}}
+				</div>
+				<div class="flex-item-main">
+					<div class="flex-item-title">
+						<a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a>
+					</div>
+					{{if .Note}}
+					<div class="flex-item-body">
+						<i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}}
+					</div>
+					{{end}}
+				</div>
+				<div class="flex-item-trailing">
+					<button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button>
+					<form action="{{$.Link}}" method="post">
+						{{$.CsrfTokenHtml}}
+						<input type="hidden" name="action" value="unblock" />
+						<input type="hidden" name="blockee" value="{{.Blockee.Name}}" />
+						<button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button>
+					</form>
+				</div>
+			</div>
+		{{else}}
+			<div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div>
+		{{end}}
+	</div>
+</div>
+<div class="ui small modal" id="block-user-note-modal">
+	<div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div>
+	<div class="content">
+		<form class="ui form" action="{{$.Link}}" method="post">
+			{{.CsrfTokenHtml}}
+			<input type="hidden" name="action" value="note" />
+			<input type="hidden" name="blockee" class="modal-blockee" />
+			<div class="field">
+				<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
+				<input name="note" class="modal-note" />
+				<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+			</div>
+			<div class="text right actions">
+				<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
+				<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
+			</div>
+		</form>
+	</div>
+</div>
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 88d3b9a6e5..a168e6903e 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -27,6 +27,12 @@
 	</div>
 	<div class="extra content gt-word-break">
 		<ul>
+			{{if .UserBlocking}}
+				<li class="text red">{{svg "octicon-circle-slash"}} {{ctx.Locale.Tr "user.block.blocked"}}</li>
+				{{if .UserBlocking.Note}}
+					<li class="text small red">{{ctx.Locale.Tr "user.block.note"}}: {{.UserBlocking.Note}}</li>
+				{{end}}
+			{{end}}
 			{{if .ContextUser.Location}}
 				<li>
 					{{svg "octicon-location"}}
@@ -109,18 +115,29 @@
 			</li>
 			{{end}}
 			{{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
-			<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card" >
-				{{if $.IsFollowing}}
-					<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
-						{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
-					</button>
-				{{else}}
-					<button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button">
-						{{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}}
-					</button>
+				{{if not .UserBlocking}}
+				<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card">
+					{{if $.IsFollowing}}
+						<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
+							{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
+						</button>
+					{{else}}
+						<button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button">
+							{{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}}
+						</button>
+					{{end}}
+				</li>
 				{{end}}
-			</li>
+				<li>
+					{{if not .UserBlocking}}
+						<a class="muted show-modal" href="#" data-modal="#block-user-modal" data-modal-modal-blockee="{{.ContextUser.Name}}" data-modal-modal-blockee-name="{{.ContextUser.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</a>
+					{{else}}
+						<a class="muted" href="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.unblock"}}</a>
+					{{end}}
+				</li>
 			{{end}}
 		</ul>
 	</div>
 </div>
+
+{{template "shared/user/block_user_dialog" .}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 9aba84a023..98198696bc 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1955,6 +1955,151 @@
         }
       }
     },
+    "/orgs/{org}/blocks": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "List users blocked by the organization",
+        "operationId": "organizationListBlocks",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/UserList"
+          }
+        }
+      }
+    },
+    "/orgs/{org}/blocks/{username}": {
+      "get": {
+        "tags": [
+          "organization"
+        ],
+        "summary": "Check if a user is blocked by the organization",
+        "operationId": "organizationCheckUserBlock",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "user to check",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "tags": [
+          "organization"
+        ],
+        "summary": "Block a user",
+        "operationId": "organizationBlockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "user to block",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "optional note for the block",
+            "name": "note",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "organization"
+        ],
+        "summary": "Unblock a user",
+        "operationId": "organizationUnblockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "user to unblock",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/orgs/{org}/hooks": {
       "get": {
         "produces": [
@@ -4340,6 +4485,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           },
@@ -6692,6 +6840,9 @@
           "400": {
             "$ref": "#/responses/error"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/error"
           },
@@ -10461,6 +10612,9 @@
           "201": {
             "$ref": "#/responses/PullRequest"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           },
@@ -12959,6 +13113,9 @@
           "200": {
             "$ref": "#/responses/WatchInfo"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
@@ -14513,6 +14670,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
@@ -15081,6 +15241,123 @@
         }
       }
     },
+    "/user/blocks": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "List users blocked by the authenticated user",
+        "operationId": "userListBlocks",
+        "parameters": [
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/UserList"
+          }
+        }
+      }
+    },
+    "/user/blocks/{username}": {
+      "get": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Check if a user is blocked by the authenticated user",
+        "operationId": "userCheckUserBlock",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "user to check",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Block a user",
+        "operationId": "userBlockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "user to block",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "optional note for the block",
+            "name": "note",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Unblock a user",
+        "operationId": "userUnblockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "user to unblock",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/user/emails": {
       "get": {
         "produces": [
@@ -15258,6 +15535,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
@@ -15965,6 +16245,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
diff --git a/templates/user/settings/blocked_users.tmpl b/templates/user/settings/blocked_users.tmpl
new file mode 100644
index 0000000000..e495b85f58
--- /dev/null
+++ b/templates/user/settings/blocked_users.tmpl
@@ -0,0 +1,5 @@
+{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}}
+	<div class="user-setting-content">
+		{{template "shared/user/blocked_users" .}}
+	</div>
+{{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl
index a690d00352..c360944814 100644
--- a/templates/user/settings/navbar.tmpl
+++ b/templates/user/settings/navbar.tmpl
@@ -13,6 +13,9 @@
 		<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
 			{{ctx.Locale.Tr "settings.security"}}
 		</a>
+		<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
+			{{ctx.Locale.Tr "user.block.list"}}
+		</a>
 		<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
 			{{ctx.Locale.Tr "settings.applications"}}
 		</a>
diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go
index a9c5228a16..255b8332b2 100644
--- a/tests/integration/api_comment_test.go
+++ b/tests/integration/api_comment_test.go
@@ -108,6 +108,32 @@ func TestAPICreateComment(t *testing.T) {
 	DecodeJSON(t, resp, &updatedComment)
 	assert.EqualValues(t, commentBody, updatedComment.Body)
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
+
+	t.Run("BlockedByRepoOwner", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
+			"body": commentBody,
+		}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
+	})
+
+	t.Run("BlockedByIssuePoster", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
+			"body": commentBody,
+		}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
+	})
 }
 
 func TestAPIGetComment(t *testing.T) {
diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go
index 4ca909f281..17e9f7aed5 100644
--- a/tests/integration/api_issue_reaction_test.go
+++ b/tests/integration/api_issue_reaction_test.go
@@ -58,6 +58,13 @@ func TestAPIIssuesReactions(t *testing.T) {
 	// Add existing reaction
 	MakeRequest(t, req, http.StatusForbidden)
 
+	// Blocked user can't react to comment
+	user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+	req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+		Reaction: "rocket",
+	}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
+	MakeRequest(t, req, http.StatusForbidden)
+
 	// Get end result of reaction list of issue #1
 	req = NewRequest(t, "GET", urlStr).
 		AddTokenAuth(token)
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index 650bac2e32..17b4e5bd71 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -84,7 +84,7 @@ func TestAPICreateIssue(t *testing.T) {
 
 	session := loginUser(t, owner.Name)
 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
-	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
 	req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
 		Body:     body,
 		Title:    title,
@@ -106,6 +106,12 @@ func TestAPICreateIssue(t *testing.T) {
 	repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues)
 	assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues)
+
+	user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+	req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
+		Title: title,
+	}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
+	MakeRequest(t, req, http.StatusForbidden)
 }
 
 func TestAPICreateIssueParallel(t *testing.T) {
@@ -117,7 +123,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
 
 	session := loginUser(t, owner.Name)
 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
-	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
 
 	var wg sync.WaitGroup
 	for i := 0; i < 10; i++ {
diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go
index 59cf85fef3..463db1dfb1 100644
--- a/tests/integration/api_repo_collaborator_test.go
+++ b/tests/integration/api_repo_collaborator_test.go
@@ -27,6 +27,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 		user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 		user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
 		user11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11})
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
 
 		testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository)
 
@@ -86,6 +87,12 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 			MakeRequest(t, req, http.StatusNotFound)
 		})
 
+		t.Run("CollaboratorBlocked", func(t *testing.T) {
+			ctx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository)
+			ctx.ExpectedCode = http.StatusForbidden
+			doAPIAddCollaborator(ctx, user34.Name, perm.AccessModeAdmin)(t)
+		})
+
 		t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) {
 			t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead))
 
diff --git a/tests/integration/api_user_block_test.go b/tests/integration/api_user_block_test.go
new file mode 100644
index 0000000000..2cc3895a71
--- /dev/null
+++ b/tests/integration/api_user_block_test.go
@@ -0,0 +1,243 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBlockUser(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	countStars := func(t *testing.T, repoOwnerID, starrerID int64) int64 {
+		count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.StarredReposOptions{
+			StarrerID:      starrerID,
+			RepoOwnerID:    repoOwnerID,
+			IncludePrivate: true,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	countWatches := func(t *testing.T, repoOwnerID, watcherID int64) int64 {
+		count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.WatchedReposOptions{
+			WatcherID:   watcherID,
+			RepoOwnerID: repoOwnerID,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	countRepositoryTransfers := func(t *testing.T, senderID, recipientID int64) int64 {
+		transfers, err := models.GetPendingRepositoryTransfers(db.DefaultContext, &models.PendingRepositoryTransferOptions{
+			SenderID:    senderID,
+			RecipientID: recipientID,
+		})
+		assert.NoError(t, err)
+		return int64(len(transfers))
+	}
+
+	countAssignedIssues := func(t *testing.T, repoOwnerID, assigneeID int64) int64 {
+		_, count, err := issues_model.GetAssignedIssues(db.DefaultContext, &issues_model.AssignedIssuesOptions{
+			AssigneeID:  assigneeID,
+			RepoOwnerID: repoOwnerID,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	countCollaborations := func(t *testing.T, repoOwnerID, collaboratorID int64) int64 {
+		count, err := db.Count[repo_model.Collaboration](db.DefaultContext, &repo_model.FindCollaborationOptions{
+			CollaboratorID: collaboratorID,
+			RepoOwnerID:    repoOwnerID,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	t.Run("User", func(t *testing.T) {
+		var blockerID int64 = 16
+		blockerName := "user16"
+		blockerToken := getUserToken(t, blockerName, auth_model.AccessTokenScopeWriteUser)
+
+		var blockeeID int64 = 10
+		blockeeName := "user10"
+
+		t.Run("Block", func(t *testing.T) {
+			req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNotFound)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s?reason=test", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block organization
+
+			req = NewRequest(t, "GET", "/api/v1/user/blocks")
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "GET", "/api/v1/user/blocks").
+				AddTokenAuth(blockerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Len(t, users, 1)
+			assert.Equal(t, blockeeName, users[0].UserName)
+		})
+
+		t.Run("Unblock", func(t *testing.T) {
+			req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "GET", "/api/v1/user/blocks").
+				AddTokenAuth(blockerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Empty(t, users)
+		})
+	})
+
+	t.Run("Organization", func(t *testing.T) {
+		var blockerID int64 = 3
+		blockerName := "org3"
+
+		doerToken := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteOrganization)
+
+		var blockeeID int64 = 10
+		blockeeName := "user10"
+
+		t.Run("Block", func(t *testing.T) {
+			req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "user4")).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block member
+
+			assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countAssignedIssues(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNotFound)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s?reason=test", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countAssignedIssues(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block organization
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)).
+				AddTokenAuth(doerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Len(t, users, 1)
+			assert.Equal(t, blockeeName, users[0].UserName)
+		})
+
+		t.Run("Unblock", func(t *testing.T) {
+			req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)).
+				AddTokenAuth(doerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Empty(t, users)
+		})
+	})
+}
diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go
index 1762732c10..fe20af6769 100644
--- a/tests/integration/api_user_follow_test.go
+++ b/tests/integration/api_user_follow_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
@@ -33,6 +35,12 @@ func TestAPIFollow(t *testing.T) {
 		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/following/%s", user1)).
 			AddTokenAuth(token2)
 		MakeRequest(t, req, http.StatusNoContent)
+
+		// blocked user can't follow blocker
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		req = NewRequest(t, "PUT", "/api/v1/user/following/user2").
+			AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteUser))
+		MakeRequest(t, req, http.StatusForbidden)
 	})
 
 	t.Run("ListFollowing", func(t *testing.T) {
diff --git a/tests/integration/api_user_star_test.go b/tests/integration/api_user_star_test.go
index 50423c80e7..0062889a92 100644
--- a/tests/integration/api_user_star_test.go
+++ b/tests/integration/api_user_star_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
@@ -31,6 +33,12 @@ func TestAPIStar(t *testing.T) {
 		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
 			AddTokenAuth(tokenWithUserScope)
 		MakeRequest(t, req, http.StatusNoContent)
+
+		// blocked user can't star a repo
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
+			AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
 	})
 
 	t.Run("GetStarredRepos", func(t *testing.T) {
diff --git a/tests/integration/api_user_watch_test.go b/tests/integration/api_user_watch_test.go
index 953e00551d..71dc57453e 100644
--- a/tests/integration/api_user_watch_test.go
+++ b/tests/integration/api_user_watch_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
@@ -31,6 +33,12 @@ func TestAPIWatch(t *testing.T) {
 		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)).
 			AddTokenAuth(tokenWithRepoScope)
 		MakeRequest(t, req, http.StatusOK)
+
+		// blocked user can't watch a repo
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)).
+			AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
 	})
 
 	t.Run("GetWatchedRepos", func(t *testing.T) {
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 3a5fdb97a6..0d733f663a 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -428,9 +428,9 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
 			isMember, err := organization.IsTeamMember(db.DefaultContext, usersOrgs[0].ID, team.ID, user.ID)
 			assert.NoError(t, err)
 			assert.True(t, isMember, "Membership should be added to the right team")
-			err = models.RemoveTeamMember(db.DefaultContext, team, user.ID)
+			err = models.RemoveTeamMember(db.DefaultContext, team, user)
 			assert.NoError(t, err)
-			err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0].ID, user.ID)
+			err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0], user)
 			assert.NoError(t, err)
 		} else {
 			// assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
@@ -460,7 +460,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
 	})
 	err = organization.AddOrgUser(db.DefaultContext, org.ID, user.ID)
 	assert.NoError(t, err)
-	err = models.AddTeamMember(db.DefaultContext, team, user.ID)
+	err = models.AddTeamMember(db.DefaultContext, team, user)
 	assert.NoError(t, err)
 	isMember, err := organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
 	assert.NoError(t, err)

From e2277d07ca5112a797f8c86f825060027ff1c2da Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 4 Mar 2024 16:57:39 +0800
Subject: [PATCH 261/679] Move some asymkey functions to service layer (#28894)

After the moving, all models will not depend on `util.Rename` so that I
can do next step refactoring.
---
 cmd/admin_regenerate.go                       |  4 +-
 models/asymkey/ssh_key_authorized_keys.go     | 66 ++--------------
 models/asymkey/ssh_key_principals.go          | 40 ----------
 routers/init.go                               |  4 +-
 routers/web/user/setting/keys.go              |  2 +-
 services/asymkey/deploy_key.go                |  3 +-
 services/asymkey/ssh_key.go                   |  4 +-
 services/asymkey/ssh_key_authorized_keys.go   | 79 +++++++++++++++++++
 .../asymkey/ssh_key_authorized_principals.go  | 30 +++----
 services/asymkey/ssh_key_principals.go        | 54 +++++++++++++
 .../auth/source/ldap/source_authenticate.go   |  5 +-
 services/auth/source/ldap/source_sync.go      |  5 +-
 services/cron/tasks_extended.go               |  6 +-
 services/doctor/authorizedkeys.go             |  5 +-
 services/repository/delete.go                 |  3 +-
 services/user/user.go                         |  6 +-
 16 files changed, 176 insertions(+), 140 deletions(-)
 create mode 100644 services/asymkey/ssh_key_authorized_keys.go
 rename {models => services}/asymkey/ssh_key_authorized_principals.go (73%)
 create mode 100644 services/asymkey/ssh_key_principals.go

diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go
index 0db505ff9c..ab769f6d0c 100644
--- a/cmd/admin_regenerate.go
+++ b/cmd/admin_regenerate.go
@@ -4,8 +4,8 @@
 package cmd
 
 import (
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/modules/graceful"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	repo_service "code.gitea.io/gitea/services/repository"
 
 	"github.com/urfave/cli/v2"
@@ -42,5 +42,5 @@ func runRegenerateKeys(_ *cli.Context) error {
 	if err := initDB(ctx); err != nil {
 		return err
 	}
-	return asymkey_model.RewriteAllPublicKeys(ctx)
+	return asymkey_service.RewriteAllPublicKeys(ctx)
 }
diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go
index 267ab252c8..9279db2020 100644
--- a/models/asymkey/ssh_key_authorized_keys.go
+++ b/models/asymkey/ssh_key_authorized_keys.go
@@ -12,7 +12,6 @@ import (
 	"path/filepath"
 	"strings"
 	"sync"
-	"time"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
@@ -44,6 +43,12 @@ const (
 
 var sshOpLocker sync.Mutex
 
+func WithSSHOpLocker(f func() error) error {
+	sshOpLocker.Lock()
+	defer sshOpLocker.Unlock()
+	return f()
+}
+
 // AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
 func AuthorizedStringForKey(key *PublicKey) string {
 	sb := &strings.Builder{}
@@ -114,65 +119,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
 	return nil
 }
 
-// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
-// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
-// outside any session scope independently.
-func RewriteAllPublicKeys(ctx context.Context) error {
-	// Don't rewrite key if internal server
-	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
-		return nil
-	}
-
-	sshOpLocker.Lock()
-	defer sshOpLocker.Unlock()
-
-	if setting.SSH.RootPath != "" {
-		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
-		// This of course doesn't guarantee that this is the right directory for authorized_keys
-		// but at least if it's supposed to be this directory and it doesn't exist and we're the
-		// right user it will at least be created properly.
-		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
-		if err != nil {
-			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
-			return err
-		}
-	}
-
-	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
-	tmpPath := fPath + ".tmp"
-	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		t.Close()
-		if err := util.Remove(tmpPath); err != nil {
-			log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
-		}
-	}()
-
-	if setting.SSH.AuthorizedKeysBackup {
-		isExist, err := util.IsExist(fPath)
-		if err != nil {
-			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
-			return err
-		}
-		if isExist {
-			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
-			if err = util.CopyFile(fPath, bakPath); err != nil {
-				return err
-			}
-		}
-	}
-
-	if err := RegeneratePublicKeys(ctx, t); err != nil {
-		return err
-	}
-
-	t.Close()
-	return util.Rename(tmpPath, fPath)
-}
-
 // RegeneratePublicKeys regenerates the authorized_keys file
 func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
 	if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
diff --git a/models/asymkey/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go
index 4e7dee2c91..e8b97d306e 100644
--- a/models/asymkey/ssh_key_principals.go
+++ b/models/asymkey/ssh_key_principals.go
@@ -9,51 +9,11 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
 
-// AddPrincipalKey adds new principal to database and authorized_principals file.
-func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*PublicKey, error) {
-	dbCtx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return nil, err
-	}
-	defer committer.Close()
-
-	// Principals cannot be duplicated.
-	has, err := db.GetEngine(dbCtx).
-		Where("content = ? AND type = ?", content, KeyTypePrincipal).
-		Get(new(PublicKey))
-	if err != nil {
-		return nil, err
-	} else if has {
-		return nil, ErrKeyAlreadyExist{0, "", content}
-	}
-
-	key := &PublicKey{
-		OwnerID:       ownerID,
-		Name:          content,
-		Content:       content,
-		Mode:          perm.AccessModeWrite,
-		Type:          KeyTypePrincipal,
-		LoginSourceID: authSourceID,
-	}
-	if err = db.Insert(dbCtx, key); err != nil {
-		return nil, fmt.Errorf("addKey: %w", err)
-	}
-
-	if err = committer.Commit(); err != nil {
-		return nil, err
-	}
-
-	committer.Close()
-
-	return key, RewriteAllPrincipalKeys(ctx)
-}
-
 // CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
 func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content string) (_ string, err error) {
 	if setting.SSH.Disabled {
diff --git a/routers/init.go b/routers/init.go
index 1dedbebeb5..aaf95920c2 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -9,7 +9,6 @@ import (
 	"runtime"
 
 	"code.gitea.io/gitea/models"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	authmodel "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/eventsource"
@@ -33,6 +32,7 @@ import (
 	"code.gitea.io/gitea/routers/private"
 	web_routers "code.gitea.io/gitea/routers/web"
 	actions_service "code.gitea.io/gitea/services/actions"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/automerge"
@@ -94,7 +94,7 @@ func syncAppConfForGit(ctx context.Context) error {
 		mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
 
 		log.Info("re-write ssh public keys ...")
-		mustInitCtx(ctx, asymkey_model.RewriteAllPublicKeys)
+		mustInitCtx(ctx, asymkey_service.RewriteAllPublicKeys)
 
 		return system.AppState.Set(ctx, runtimeState)
 	}
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index d2b60fc809..056fcc0ace 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -62,7 +62,7 @@ func KeysPost(ctx *context.Context) {
 			ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 			return
 		}
-		if _, err = asymkey_model.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
+		if _, err = asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
 			ctx.Data["HasPrincipalError"] = true
 			switch {
 			case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go
index e127cbfc6e..324688c534 100644
--- a/services/asymkey/deploy_key.go
+++ b/services/asymkey/deploy_key.go
@@ -7,7 +7,6 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 )
@@ -27,5 +26,5 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error
 		return err
 	}
 
-	return asymkey_model.RewriteAllPublicKeys(ctx)
+	return RewriteAllPublicKeys(ctx)
 }
diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go
index 83d7edafa3..da57059d4b 100644
--- a/services/asymkey/ssh_key.go
+++ b/services/asymkey/ssh_key.go
@@ -43,8 +43,8 @@ func DeletePublicKey(ctx context.Context, doer *user_model.User, id int64) (err
 	committer.Close()
 
 	if key.Type == asymkey_model.KeyTypePrincipal {
-		return asymkey_model.RewriteAllPrincipalKeys(ctx)
+		return RewriteAllPrincipalKeys(ctx)
 	}
 
-	return asymkey_model.RewriteAllPublicKeys(ctx)
+	return RewriteAllPublicKeys(ctx)
 }
diff --git a/services/asymkey/ssh_key_authorized_keys.go b/services/asymkey/ssh_key_authorized_keys.go
new file mode 100644
index 0000000000..5caa5bbfb6
--- /dev/null
+++ b/services/asymkey/ssh_key_authorized_keys.go
@@ -0,0 +1,79 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"time"
+
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+)
+
+// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
+// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
+// outside any session scope independently.
+func RewriteAllPublicKeys(ctx context.Context) error {
+	// Don't rewrite key if internal server
+	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
+		return nil
+	}
+
+	return asymkey_model.WithSSHOpLocker(func() error {
+		return rewriteAllPublicKeys(ctx)
+	})
+}
+
+func rewriteAllPublicKeys(ctx context.Context) error {
+	if setting.SSH.RootPath != "" {
+		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
+		// This of course doesn't guarantee that this is the right directory for authorized_keys
+		// but at least if it's supposed to be this directory and it doesn't exist and we're the
+		// right user it will at least be created properly.
+		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
+		if err != nil {
+			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
+			return err
+		}
+	}
+
+	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+	tmpPath := fPath + ".tmp"
+	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		t.Close()
+		if err := util.Remove(tmpPath); err != nil {
+			log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
+		}
+	}()
+
+	if setting.SSH.AuthorizedKeysBackup {
+		isExist, err := util.IsExist(fPath)
+		if err != nil {
+			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
+			return err
+		}
+		if isExist {
+			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
+			if err = util.CopyFile(fPath, bakPath); err != nil {
+				return err
+			}
+		}
+	}
+
+	if err := asymkey_model.RegeneratePublicKeys(ctx, t); err != nil {
+		return err
+	}
+
+	t.Close()
+	return util.Rename(tmpPath, fPath)
+}
diff --git a/models/asymkey/ssh_key_authorized_principals.go b/services/asymkey/ssh_key_authorized_principals.go
similarity index 73%
rename from models/asymkey/ssh_key_authorized_principals.go
rename to services/asymkey/ssh_key_authorized_principals.go
index 107d70c766..9154db7dbb 100644
--- a/models/asymkey/ssh_key_authorized_principals.go
+++ b/services/asymkey/ssh_key_authorized_principals.go
@@ -13,31 +13,22 @@ import (
 	"strings"
 	"time"
 
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
 
-//  _____          __  .__                 .__                  .___
-// /  _  \  __ ___/  |_|  |__   ___________|__|_______ ____   __| _/
-// /  /_\  \|  |  \   __\  |  \ /  _ \_  __ \  \___   // __ \ / __ |
-// /    |    \  |  /|  | |   Y  (  <_> )  | \/  |/    /\  ___// /_/ |
-// \____|__  /____/ |__| |___|  /\____/|__|  |__/_____ \\___  >____ |
-//         \/                 \/                      \/    \/     \/
-// __________       .__              .__             .__
-// \______   _______|__| ____   ____ |_____________  |  |   ______
-//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
-//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
-//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
-//                          \/     \/   |__|       \/          \/
-//
 // This file contains functions for creating authorized_principals files
 //
 // There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
 // The sshOpLocker is used from ssh_key_authorized_keys.go
 
-const authorizedPrincipalsFile = "authorized_principals"
+const (
+	authorizedPrincipalsFile = "authorized_principals"
+	tplCommentPrefix         = `# gitea public key`
+)
 
 // RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
 // Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
@@ -48,9 +39,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
 		return nil
 	}
 
-	sshOpLocker.Lock()
-	defer sshOpLocker.Unlock()
+	return asymkey_model.WithSSHOpLocker(func() error {
+		return rewriteAllPrincipalKeys(ctx)
+	})
+}
 
+func rewriteAllPrincipalKeys(ctx context.Context) error {
 	if setting.SSH.RootPath != "" {
 		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
 		// This of course doesn't guarantee that this is the right directory for authorized_keys
@@ -97,8 +91,8 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
 }
 
 func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
-	if err := db.GetEngine(ctx).Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
-		_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
+	if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) {
+		_, err = t.WriteString((bean.(*asymkey_model.PublicKey)).AuthorizedString())
 		return err
 	}); err != nil {
 		return err
diff --git a/services/asymkey/ssh_key_principals.go b/services/asymkey/ssh_key_principals.go
new file mode 100644
index 0000000000..5ed5cfa782
--- /dev/null
+++ b/services/asymkey/ssh_key_principals.go
@@ -0,0 +1,54 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"context"
+	"fmt"
+
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/perm"
+)
+
+// AddPrincipalKey adds new principal to database and authorized_principals file.
+func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*asymkey_model.PublicKey, error) {
+	dbCtx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	defer committer.Close()
+
+	// Principals cannot be duplicated.
+	has, err := db.GetEngine(dbCtx).
+		Where("content = ? AND type = ?", content, asymkey_model.KeyTypePrincipal).
+		Get(new(asymkey_model.PublicKey))
+	if err != nil {
+		return nil, err
+	} else if has {
+		return nil, asymkey_model.ErrKeyAlreadyExist{
+			Content: content,
+		}
+	}
+
+	key := &asymkey_model.PublicKey{
+		OwnerID:       ownerID,
+		Name:          content,
+		Content:       content,
+		Mode:          perm.AccessModeWrite,
+		Type:          asymkey_model.KeyTypePrincipal,
+		LoginSourceID: authSourceID,
+	}
+	if err = db.Insert(dbCtx, key); err != nil {
+		return nil, fmt.Errorf("addKey: %w", err)
+	}
+
+	if err = committer.Commit(); err != nil {
+		return nil, err
+	}
+
+	committer.Close()
+
+	return key, RewriteAllPrincipalKeys(ctx)
+}
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 68ecd16342..6ebd3ea50a 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -13,6 +13,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	auth_module "code.gitea.io/gitea/modules/auth"
 	"code.gitea.io/gitea/modules/optional"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -68,7 +69,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 
 	if user != nil {
 		if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sr.SSHPublicKey) {
-			if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+			if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 				return user, err
 			}
 		}
@@ -94,7 +95,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		}
 
 		if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.authSource, sr.SSHPublicKey) {
-			if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+			if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 				return user, err
 			}
 		}
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 62f052d68c..0c9491cd09 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -77,7 +78,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.authSource.Name)
 			// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 			if sshKeysNeedUpdate {
-				err = asymkey_model.RewriteAllPublicKeys(ctx)
+				err = asymkey_service.RewriteAllPublicKeys(ctx)
 				if err != nil {
 					log.Error("RewriteAllPublicKeys: %v", err)
 				}
@@ -195,7 +196,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 
 	// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 	if sshKeysNeedUpdate {
-		err = asymkey_model.RewriteAllPublicKeys(ctx)
+		err = asymkey_service.RewriteAllPublicKeys(ctx)
 		if err != nil {
 			log.Error("RewriteAllPublicKeys: %v", err)
 		}
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
index 1dd5d70a38..0018c5facc 100644
--- a/services/cron/tasks_extended.go
+++ b/services/cron/tasks_extended.go
@@ -8,13 +8,13 @@ import (
 	"time"
 
 	activities_model "code.gitea.io/gitea/models/activities"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/system"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/updatechecker"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	repo_service "code.gitea.io/gitea/services/repository"
 	archiver_service "code.gitea.io/gitea/services/repository/archiver"
 	user_service "code.gitea.io/gitea/services/user"
@@ -71,7 +71,7 @@ func registerRewriteAllPublicKeys() {
 		RunAtStart: false,
 		Schedule:   "@every 72h",
 	}, func(ctx context.Context, _ *user_model.User, _ Config) error {
-		return asymkey_model.RewriteAllPublicKeys(ctx)
+		return asymkey_service.RewriteAllPublicKeys(ctx)
 	})
 }
 
@@ -81,7 +81,7 @@ func registerRewriteAllPrincipalKeys() {
 		RunAtStart: false,
 		Schedule:   "@every 72h",
 	}, func(ctx context.Context, _ *user_model.User, _ Config) error {
-		return asymkey_model.RewriteAllPrincipalKeys(ctx)
+		return asymkey_service.RewriteAllPrincipalKeys(ctx)
 	})
 }
 
diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go
index 050a4e7974..d5a96605b9 100644
--- a/services/doctor/authorizedkeys.go
+++ b/services/doctor/authorizedkeys.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 const tplCommentPrefix = `# gitea public key`
@@ -33,7 +34,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
 			return fmt.Errorf("Unable to open authorized_keys file. ERROR: %w", err)
 		}
 		logger.Warn("Unable to open authorized_keys. (ERROR: %v). Attempting to rewrite...", err)
-		if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+		if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 			logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
 			return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
 		}
@@ -76,7 +77,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
 			return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "gitea admin regenerate keys" or "gitea doctor --run authorized-keys --fix"`)
 		}
 		logger.Warn("authorized_keys is out of date. Attempting rewrite...")
-		err = asymkey_model.RewriteAllPublicKeys(ctx)
+		err = asymkey_service.RewriteAllPublicKeys(ctx)
 		if err != nil {
 			logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
 			return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 1eeec27660..8d6729f31b 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -27,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 
 	"xorm.io/builder"
 )
@@ -277,7 +278,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 	committer.Close()
 
 	if needRewriteKeysFile {
-		if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+		if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 			log.Error("RewriteAllPublicKeys failed: %v", err)
 		}
 	}
diff --git a/services/user/user.go b/services/user/user.go
index 6604dba4d6..4fcb81581d 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	packages_model "code.gitea.io/gitea/models/packages"
@@ -24,6 +23,7 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/agit"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	org_service "code.gitea.io/gitea/services/org"
 	"code.gitea.io/gitea/services/packages"
 	container_service "code.gitea.io/gitea/services/packages/container"
@@ -252,10 +252,10 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 	}
 	committer.Close()
 
-	if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+	if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 		return err
 	}
-	if err = asymkey_model.RewriteAllPrincipalKeys(ctx); err != nil {
+	if err = asymkey_service.RewriteAllPrincipalKeys(ctx); err != nil {
 		return err
 	}
 

From 7ec4c65ea5d5a3765d24ee68ae421f2f911b6b5d Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 4 Mar 2024 18:57:30 +0800
Subject: [PATCH 262/679] Fix incorrect package link method calls in templates
 (#29580)

Fix #29562
Follow  #29531
---
 templates/admin/packages/list.tmpl                  | 2 +-
 templates/package/settings.tmpl                     | 2 +-
 templates/package/shared/cleanup_rules/preview.tmpl | 2 +-
 templates/package/shared/list.tmpl                  | 2 +-
 templates/package/shared/versionlist.tmpl           | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index cf860dab2a..1f86803d55 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -63,7 +63,7 @@
 							</td>
 							<td>{{.Package.Type.Name}}</td>
 							<td class="gt-ellipsis gt-max-width-12rem">{{.Package.Name}}</td>
-							<td class="gt-ellipsis gt-max-width-12rem"><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td>
+							<td class="gt-ellipsis gt-max-width-12rem"><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
 							<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
 							<td>
 							{{if .Repository}}
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl
index 10e26c7010..9424baf493 100644
--- a/templates/package/settings.tmpl
+++ b/templates/package/settings.tmpl
@@ -10,7 +10,7 @@
 			{{template "user/overview/header" .}}
 		{{end}}
 		{{template "base/alert" .}}
-		<p><a href="{{.PackageDescriptor.FullWebLink}}">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p>
+		<p><a href="{{.PackageDescriptor.VersionWebLink}}">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p>
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "packages.settings.link"}}
 		</h4>
diff --git a/templates/package/shared/cleanup_rules/preview.tmpl b/templates/package/shared/cleanup_rules/preview.tmpl
index 7a50d5ccca..cff8e8249f 100644
--- a/templates/package/shared/cleanup_rules/preview.tmpl
+++ b/templates/package/shared/cleanup_rules/preview.tmpl
@@ -19,7 +19,7 @@
 				<tr>
 					<td>{{.Package.Type.Name}}</td>
 					<td>{{.Package.Name}}</td>
-					<td><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td>
+					<td><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
 					<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
 					<td>{{FileSize .CalculateBlobSize}}</td>
 					<td>{{DateTime "short" .Version.CreatedUnix}}</td>
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index 51e080f495..09205b19a5 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -20,7 +20,7 @@
 		<div class="flex-item">
 			<div class="flex-item-main">
 				<div class="flex-item-title">
-					<a href="{{.FullWebLink}}">{{.Package.Name}}</a>
+					<a href="{{.VersionWebLink}}">{{.Package.Name}}</a>
 					<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
 				</div>
 				<div class="flex-item-body">
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl
index eee952c096..59d6d89b53 100644
--- a/templates/package/shared/versionlist.tmpl
+++ b/templates/package/shared/versionlist.tmpl
@@ -23,7 +23,7 @@
 	<div class="flex-list">
 		<div class="flex-item">
 			<div class="flex-item-main">
-				<a class="flex-item-title" href="{{.FullWebLink}}">{{.Version.LowerVersion}}</a>
+				<a class="flex-item-title" href="{{.VersionWebLink}}">{{.Version.LowerVersion}}</a>
 				<div class="flex-item-body">
 					{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink .Creator.GetDisplayName}}
 				</div>

From e91733468ef726fc9365aa4820cdd5f2ddfdaa23 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 4 Mar 2024 19:24:02 +0800
Subject: [PATCH 263/679] Add missing database transaction for new issue
 (#29490)

When creating an issue, inserting issue, assign users and set project
should be in the same transaction.
---
 routers/api/v1/repo/issue.go |  2 +-
 routers/web/repo/issue.go    | 22 +++++++++-------------
 services/issue/issue.go      | 23 ++++++++++++++++-------
 3 files changed, 26 insertions(+), 21 deletions(-)

diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index d43711e362..b63e7ab662 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -709,7 +709,7 @@ func CreateIssue(ctx *context.APIContext) {
 		form.Labels = make([]int64, 0)
 	}
 
-	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
+	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 		} else if errors.Is(err, user_model.ErrBlockedUser) {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 45fd01f4da..83a5b76bf1 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1224,6 +1224,14 @@ func NewIssuePost(ctx *context.Context) {
 		return
 	}
 
+	if projectID > 0 {
+		if !ctx.Repo.CanRead(unit.TypeProjects) {
+			// User must also be able to see the project.
+			ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
+			return
+		}
+	}
+
 	if setting.Attachment.Enabled {
 		attachments = form.Files
 	}
@@ -1256,7 +1264,7 @@ func NewIssuePost(ctx *context.Context) {
 		Ref:         form.Ref,
 	}
 
-	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
+	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 		} else if errors.Is(err, user_model.ErrBlockedUser) {
@@ -1267,18 +1275,6 @@ func NewIssuePost(ctx *context.Context) {
 		return
 	}
 
-	if projectID > 0 {
-		if !ctx.Repo.CanRead(unit.TypeProjects) {
-			// User must also be able to see the project.
-			ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
-			return
-		}
-		if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
-			ctx.ServerError("ChangeProjectAssign", err)
-			return
-		}
-	}
-
 	log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
 	if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
 		ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 27a106009c..0753813b64 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -22,7 +22,7 @@ import (
 )
 
 // NewIssue creates new issue with labels for repository.
-func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
+func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error {
 	if err := issue.LoadPoster(ctx); err != nil {
 		return err
 	}
@@ -31,14 +31,23 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
 		return user_model.ErrBlockedUser
 	}
 
-	if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
-		return err
-	}
-
-	for _, assigneeID := range assigneeIDs {
-		if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
+	if err := db.WithTx(ctx, func(ctx context.Context) error {
+		if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
 			return err
 		}
+		for _, assigneeID := range assigneeIDs {
+			if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
+				return err
+			}
+		}
+		if projectID > 0 {
+			if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil {
+				return err
+			}
+		}
+		return nil
+	}); err != nil {
+		return err
 	}
 
 	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)

From dae7f1ebdbe19620f40e110b285f7c0ecd0bb33b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 4 Mar 2024 20:02:45 +0800
Subject: [PATCH 264/679] Remove unnecessary SanitizeHTML from code (#29575)

* "mail/issue/default.tmpl": the body is rendered by backend
`markdown.RenderString() HTML`, it has been already sanitized
* "repo/settings/webhook/base_list.tmpl": "Description" is prepared by
backend `ctx.Tr`, it doesn't need to be sanitized
---
 docs/content/administration/mail-templates.en-us.md |  2 +-
 docs/content/administration/mail-templates.zh-cn.md |  2 +-
 modules/templates/helper.go                         | 10 ++--------
 modules/templates/helper_test.go                    |  1 -
 templates/mail/issue/default.tmpl                   |  2 +-
 templates/repo/settings/webhook/base_list.tmpl      |  2 +-
 templates/status/500.tmpl                           |  2 +-
 7 files changed, 7 insertions(+), 14 deletions(-)

diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index 0154fe55d0..4026b89975 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -224,7 +224,7 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages
         {{if not (eq .Body "")}}
             <h3>Message content</h3>
             <hr>
-            {{.Body | SanitizeHTML}}
+            {{.Body}}
         {{end}}
     </p>
     <hr>
diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md
index e8c2817336..3c7c2a9397 100644
--- a/docs/content/administration/mail-templates.zh-cn.md
+++ b/docs/content/administration/mail-templates.zh-cn.md
@@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
         {{if not (eq .Body "")}}
             <h3>消息内容:</h3>
             <hr>
-            {{.Body | SanitizeHTML}}
+            {{.Body}}
         {{end}}
     </p>
     <hr>
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 1487fce69d..0997239a55 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -208,14 +208,8 @@ func SafeHTML(s any) template.HTML {
 }
 
 // SanitizeHTML sanitizes the input by pre-defined markdown rules
-func SanitizeHTML(s any) template.HTML {
-	switch v := s.(type) {
-	case string:
-		return template.HTML(markup.Sanitize(v))
-	case template.HTML:
-		return template.HTML(markup.Sanitize(string(v)))
-	}
-	panic(fmt.Sprintf("unexpected type %T", s))
+func SanitizeHTML(s string) template.HTML {
+	return template.HTML(markup.Sanitize(s))
 }
 
 func HTMLEscape(s any) template.HTML {
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index 3365278ac2..64f29d033e 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -64,5 +64,4 @@ func TestHTMLFormat(t *testing.T) {
 
 func TestSanitizeHTML(t *testing.T) {
 	assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
-	assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(template.HTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`)))
 }
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index 021ca3989d..395b118d3e 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -58,7 +58,7 @@
 				{{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}}
 			{{end}}
 		{{else}}
-			{{.Body | SanitizeHTML}}
+			{{.Body}}
 		{{end -}}
 		{{- range .ReviewComments}}
 			<hr>
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index 00f9a48ba7..e56929b70f 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -10,7 +10,7 @@
 <div class="ui attached segment">
 	<div class="ui list">
 		<div class="item">
-			{{.Description | SanitizeHTML}}
+			{{.Description}}
 		</div>
 		{{range .Webhooks}}
 			<div class="item truncated-item-container">
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index 58795e4bc0..03d0183280 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -1,5 +1,5 @@
 {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics.
-* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName, SanitizeHTML
+* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName
 * ctx.Locale
 * .Flash
 * .ErrorMsg

From 62aa5e2cbd64c90546bbaf849070b42931a4e60b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 4 Mar 2024 20:56:34 +0800
Subject: [PATCH 265/679] Refactor star/watch button (#29576)

1. Use "star/unstart", but not `{{if}}un{{}}star{{}}` (the same to "watch/unwatch")
2. Use "not-mobile" for hiding the elements on mobile
---
 templates/repo/star_unstar.tmpl   | 13 ++++++-------
 templates/repo/watch_unwatch.tmpl | 13 ++++++-------
 web_src/css/repo/header.css       |  3 ---
 3 files changed, 12 insertions(+), 17 deletions(-)

diff --git a/templates/repo/star_unstar.tmpl b/templates/repo/star_unstar.tmpl
index 9c342f4065..1cdb98bf27 100644
--- a/templates/repo/star_unstar.tmpl
+++ b/templates/repo/star_unstar.tmpl
@@ -1,11 +1,10 @@
-<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star">
+<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}unstar{{else}}star{{end}}">
 	<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.star_guest_user"}}"{{end}}>
-		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}}>
-			{{if $.IsStaringRepo}}
-				{{svg "octicon-star-fill"}}<span class="text">{{ctx.Locale.Tr "repo.unstar"}}</span>
-			{{else}}
-				{{svg "octicon-star"}}<span class="text">{{ctx.Locale.Tr "repo.star"}}</span>
-			{{end}}
+		{{$buttonText := ctx.Locale.Tr "repo.star"}}
+		{{if $.IsStaringRepo}}{{$buttonText = ctx.Locale.Tr "repo.unstar"}}{{end}}
+		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}} aria-label="{{$buttonText}}">
+			{{if $.IsStaringRepo}}{{svg "octicon-star-fill"}}{{else}}{{svg "octicon-star"}}{{end}}
+			<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
 		</button>
 		<a hx-boost="false" class="ui basic label" href="{{$.RepoLink}}/stars">
 			{{CountFmt .Repository.NumStars}}
diff --git a/templates/repo/watch_unwatch.tmpl b/templates/repo/watch_unwatch.tmpl
index c42bc5a9e7..2bf2c7bd21 100644
--- a/templates/repo/watch_unwatch.tmpl
+++ b/templates/repo/watch_unwatch.tmpl
@@ -1,11 +1,10 @@
-<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch">
+<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}">
 	<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
-		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}}>
-			{{if $.IsWatchingRepo}}
-				{{svg "octicon-eye-closed" 16}}<span class="text">{{ctx.Locale.Tr "repo.unwatch"}}</span>
-			{{else}}
-				{{svg "octicon-eye"}}<span class="text">{{ctx.Locale.Tr "repo.watch"}}</span>
-			{{end}}
+		{{$buttonText := ctx.Locale.Tr "repo.watch"}}
+		{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}
+		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}} aria-label="{{$buttonText}}">
+			{{if $.IsWatchingRepo}}{{svg "octicon-eye-closed"}}{{else}}{{svg "octicon-eye"}}{{end}}
+			<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
 		</button>
 		<a hx-boost="false" class="ui basic label" href="{{.RepoLink}}/watchers">
 			{{CountFmt .Repository.NumWatches}}
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index d5c7d212e8..0eb03136ef 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -89,9 +89,6 @@
   .repo-header .flex-item {
     flex-grow: 1;
   }
-  .repo-buttons .ui.labeled.button .text {
-    display: none;
-  }
   .repo-header .flex-item-trailing .label {
     display: none;
   }

From fad232054542ade88268304fea9b09f778d74a29 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 4 Mar 2024 21:48:59 +0800
Subject: [PATCH 266/679] Make admin pages wider because of left sidebar added
 and some tables become too narrow (#29581)

Fix #25939

screenshots

<img width="1895" alt="image"
src="https://github.com/go-gitea/gitea/assets/81045/937eb28d-bb7d-4765-b580-bc991d61f467">
---
 templates/admin/layout_head.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl
index 0067f336e0..b326c82a6c 100644
--- a/templates/admin/layout_head.tmpl
+++ b/templates/admin/layout_head.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui container gt-mb-4">
 		{{template "base/alert" .ctxData}}
 	</div>
-	<div class="ui container flex-container">
+	<div class="ui container fluid padded flex-container">
 		{{template "admin/navbar" .ctxData}}
 		<div class="flex-container-main">
 			{{/* block: admin-setting-content */}}

From 76789bdbcd88c02b306842c91da18d8255dd582b Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 4 Mar 2024 21:51:12 +0800
Subject: [PATCH 267/679] Document that all unmerged feature PRs will be moved
 to next milestone when the feature freeze time comes (#29578)

Some contributors may be surprised when moving the feature PRs to the
next release when the feature freeze time comes. So this PR documents
the habits we have done for feature freeze so people expect the
maintainers' behaviors. We are sorry for disturbing you with the
milestones changes.

A feature freeze announcement should be published 2 or 3 weeks before
the feature freeze by maintainers.
---
 CONTRIBUTING.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dc90c6905b..5d20bc2589 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -464,7 +464,7 @@ We assume in good faith that the information you provide is legally binding.
 We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \
 The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \
 All the feature pull requests should be
-merged before feature freeze. And, during the frozen period, a corresponding
+merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding
 release branch is open for fixes backported from main branch. Release candidates
 are made during this period for user testing to
 obtain a final version that is maintained in this branch.

From 797ad68964121d5fd0f936567397b3456b2f8d8b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 4 Mar 2024 22:31:59 +0800
Subject: [PATCH 268/679] Add aria-label to the navbar menu button (#29587)

---
 options/locale/locale_en-US.ini | 3 ++-
 templates/base/head_navbar.tmpl | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 255fed28ad..3fc74959ca 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -320,6 +320,7 @@ env_config_keys = Environment Configuration
 env_config_keys_prompt = The following environment variables will also be applied to your configuration file:
 
 [home]
+nav_menu = Navigation Menu
 uname_holder = Username or Email Address
 password_holder = Password
 switch_dashboard_context = Switch Dashboard Context
@@ -654,7 +655,7 @@ block.note.title = Optional note:
 block.note.info = The note is not visible to the blocked user.
 block.note.edit = Edit note
 block.list = Blocked users
-block.list.none = You have not blocked any users. 
+block.list.none = You have not blocked any users.
 
 [settings]
 profile = Profile
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 3797de0a0f..51eeea405a 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -20,7 +20,7 @@
 				</div>
 			</a>
 			{{end}}
-			<button class="item tw-w-auto ui icon mini button gt-p-3 gt-m-0" id="navbar-expand-toggle">{{svg "octicon-three-bars"}}</button>
+			<button class="item tw-w-auto ui icon mini button gt-p-3 gt-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button>
 		</div>
 
 		<!-- navbar links non-mobile -->

From c660149a7079c2b06d4ee6dce2a45804d6d4d7f6 Mon Sep 17 00:00:00 2001
From: charles <30816317+charles7668@users.noreply.github.com>
Date: Mon, 4 Mar 2024 22:41:53 +0800
Subject: [PATCH 269/679] Do not exceed display for the PR page buttons on
 smaller screens (#29418)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fixes #29189.

This is the result after the fix at a width of 768 pixels.

![圖片](https://github.com/go-gitea/gitea/assets/30816317/626d06b3-fd5b-4392-84e1-1191c965aff5)
---
 web_src/css/repo.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 31cff0ca15..87ce829a78 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -942,7 +942,7 @@
   margin-bottom: -0.25rem;
 }
 
-@media (max-width: 767.98px) {
+@media (max-width: 991.98px) {
   .repository.view.issue .comment-list .comment .merge-section .item-section {
     align-items: flex-start;
     flex-direction: column;

From da3b7f5039158faae4b617ca878061f8a4f3e489 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 4 Mar 2024 21:24:12 +0100
Subject: [PATCH 270/679] Regenerate fomantic lockfile and build it with our
 browserslist (#29560)

1. Make fomantic build use [our
browserslist](https://github.com/go-gitea/gitea/blob/e3524c63d6d42865ea8288af89b372544d35474b/package.json#L99).
I found no other way than to sed-replace into it's js, the normal
browserlist config files do not work. The effect of this change is the
removal of some uneeded CSS vendor prefixes.
2. Regenerate `web_src/fomantic/package-lock.json`, this might shut up
some security scanners.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 Makefile                            |    1 +
 web_src/fomantic/build/semantic.css |  109 --
 web_src/fomantic/package-lock.json  | 1846 ++++++++++++++++++---------
 3 files changed, 1267 insertions(+), 689 deletions(-)

diff --git a/Makefile b/Makefile
index 0e9e792053..9bbc56451b 100644
--- a/Makefile
+++ b/Makefile
@@ -908,6 +908,7 @@ fomantic:
 	cd $(FOMANTIC_WORK_DIR) && npm install --no-save
 	cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
 	cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
+	$(SED_INPLACE) -e 's/  overrideBrowserslist\r/  overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js
 	cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
 	# fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event
 	$(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index ad3f13bb58..476f7ebf11 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -156,7 +156,6 @@
   width: 1.28571429em;
   height: 1.28571429em;
   border-radius: 500rem;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid currentColor;
   color: #FFFFFF;
@@ -530,7 +529,6 @@
   border-top-left-radius: inherit;
   border-bottom-left-radius: inherit;
   text-align: center;
-  -webkit-animation: none;
   animation: none;
   padding: 0.78571429em 0 0.78571429em 0;
   margin: 0;
@@ -594,7 +592,6 @@
 /* Loading Icon in Labeled Button */
 
 .ui.labeled.icon.button > .loading.icon:before {
-  -webkit-animation: loader 2s linear infinite;
   animation: loader 2s linear infinite;
 }
 
@@ -2350,7 +2347,6 @@
 .ui.checkbox {
   position: relative;
   display: inline-block;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   outline: none;
   vertical-align: baseline;
@@ -3227,9 +3223,7 @@
   background: rgba(0, 0, 0, 0.85);
   opacity: 0;
   line-height: 1;
-  -webkit-animation-fill-mode: both;
   animation-fill-mode: both;
-  -webkit-animation-duration: 0.5s;
   animation-duration: 0.5s;
   transition: background-color 0.5s linear;
   flex-direction: column;
@@ -3458,44 +3452,25 @@ body.dimmable > .dimmer {
 }
 
 .ui[class*="center dimmer"].transition[class*="fade up"].in {
-  -webkit-animation-name: fadeInUpCenter;
   animation-name: fadeInUpCenter;
 }
 
 .ui[class*="center dimmer"].transition[class*="fade down"].in {
-  -webkit-animation-name: fadeInDownCenter;
   animation-name: fadeInDownCenter;
 }
 
 .ui[class*="center dimmer"].transition[class*="fade up"].out {
-  -webkit-animation-name: fadeOutUpCenter;
   animation-name: fadeOutUpCenter;
 }
 
 .ui[class*="center dimmer"].transition[class*="fade down"].out {
-  -webkit-animation-name: fadeOutDownCenter;
   animation-name: fadeOutDownCenter;
 }
 
 .ui[class*="center dimmer"].bounce.transition {
-  -webkit-animation-name: bounceCenter;
   animation-name: bounceCenter;
 }
 
-@-webkit-keyframes fadeInUpCenter {
-  0% {
-    opacity: 0;
-    transform: translateY(-40%);
-    -webkit-transform: translateY(calc(-40% - 0.5px));
-  }
-
-  100% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-}
-
 @keyframes fadeInUpCenter {
   0% {
     opacity: 0;
@@ -3510,20 +3485,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes fadeInDownCenter {
-  0% {
-    opacity: 0;
-    transform: translateY(-60%);
-    -webkit-transform: translateY(calc(-60% - 0.5px));
-  }
-
-  100% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-}
-
 @keyframes fadeInDownCenter {
   0% {
     opacity: 0;
@@ -3538,20 +3499,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes fadeOutUpCenter {
-  0% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-
-  100% {
-    opacity: 0;
-    transform: translateY(-45%);
-    -webkit-transform: translateY(calc(-45% - 0.5px));
-  }
-}
-
 @keyframes fadeOutUpCenter {
   0% {
     opacity: 1;
@@ -3566,20 +3513,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes fadeOutDownCenter {
-  0% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-
-  100% {
-    opacity: 0;
-    transform: translateY(-55%);
-    -webkit-transform: translateY(calc(-55% - 0.5px));
-  }
-}
-
 @keyframes fadeOutDownCenter {
   0% {
     opacity: 1;
@@ -3594,21 +3527,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes bounceCenter {
-  0%, 20%, 50%, 80%, 100% {
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-
-  40% {
-    transform: translateY(calc(-50% - 30px));
-  }
-
-  60% {
-    transform: translateY(calc(-50% - 15px));
-  }
-}
-
 @keyframes bounceCenter {
   0%, 20%, 50%, 80%, 100% {
     transform: translateY(-50%);
@@ -3672,7 +3590,6 @@ body.dimmable > .dimmer {
   display: none;
   outline: none;
   top: 100%;
-  min-width: -webkit-max-content;
   min-width: -moz-max-content;
   min-width: max-content;
   margin: 0;
@@ -4068,7 +3985,6 @@ select.ui.dropdown {
 .ui.selection.dropdown .menu {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
   border-top-width: 0 !important;
@@ -4280,7 +4196,6 @@ select.ui.dropdown {
 @supports (-webkit-touch-callout: none) or (-webkit-overflow-scrolling: touch) or (-moz-appearance:none) {
 @media (-moz-touch-enabled), (pointer: coarse) {
     .ui.dropdown .scrollhint.menu:not(.hidden):before {
-      -webkit-animation: scrollhint 2s ease 2;
       animation: scrollhint 2s ease 2;
       content: '';
       z-index: 15;
@@ -4301,18 +4216,6 @@ select.ui.dropdown {
       border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%;
     }
 
-@-webkit-keyframes scrollhint {
-      0% {
-        opacity: 1;
-        top: 100%;
-      }
-
-      100% {
-        opacity: 0;
-        top: 0;
-      }
-}
-
 @keyframes scrollhint {
       0% {
         opacity: 1;
@@ -4414,7 +4317,6 @@ select.ui.dropdown {
 .ui.search.dropdown .menu {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
 }
@@ -4682,7 +4584,6 @@ select.ui.dropdown {
   margin: -0.64285714em 0 0 -0.64285714em;
   width: 1.28571429em;
   height: 1.28571429em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -5072,7 +4973,6 @@ select.ui.dropdown {
 .ui.scrolling.dropdown .menu {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
   min-width: 100% !important;
@@ -5564,7 +5464,6 @@ select.ui.dropdown {
   line-height: 1;
   height: 1em;
   width: 1.23em;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   font-weight: normal;
   font-style: normal;
@@ -7052,7 +6951,6 @@ select.ui.dropdown {
   margin: -1.5em 0 0 -1.5em;
   width: 3em;
   height: 3em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -10299,7 +10197,6 @@ a.ui.black.header:hover {
   margin: -0.64285714em 0 0 -0.64285714em;
   width: 1.28571429em;
   height: 1.28571429em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -13306,7 +13203,6 @@ ol.ui.suffixed.list li:before,
   left: 100%;
   /* IE needs 0, all others support max-content to show dropdown icon inline, so keep both settings! */
   min-width: 0;
-  min-width: -webkit-max-content;
   min-width: -moz-max-content;
   min-width: max-content;
   margin: 0 0 0 0;
@@ -16722,7 +16618,6 @@ Floated Menu / Item
   margin: -0.64285714em 0 0 -0.64285714em;
   width: 1.28571429em;
   height: 1.28571429em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -16904,7 +16799,6 @@ Floated Menu / Item
 .ui.search.short > .results {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
 }
@@ -17201,7 +17095,6 @@ Floated Menu / Item
   justify-content: center;
   align-items: stretch;
   max-width: initial;
-  -webkit-animation: none;
   animation: none;
   overflow: visible;
   padding: 1em 1em;
@@ -17494,7 +17387,6 @@ Floated Menu / Item
   margin: -1.5em 0 0 -1.5em;
   width: 3em;
   height: 3em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -17996,7 +17888,6 @@ input::selection {
   margin: -1.25em 0 0 -1.25em;
   width: 2.5em;
   height: 2.5em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
diff --git a/web_src/fomantic/package-lock.json b/web_src/fomantic/package-lock.json
index 8283122eb3..4000d10da7 100644
--- a/web_src/fomantic/package-lock.json
+++ b/web_src/fomantic/package-lock.json
@@ -19,6 +19,100 @@
         "findup": "bin/findup.js"
       }
     },
+    "node_modules/@choojs/findup/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/@octokit/auth-token": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
@@ -28,35 +122,97 @@
       }
     },
     "node_modules/@octokit/core": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
-      "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.0.1.tgz",
+      "integrity": "sha512-MIpPQXu8Y8GjHwXM81JLveiV+DHJZtLMcB5nKekBGOl3iAtk0HT3i12Xl8Biybu+bCS1+k4qbuKEq5d0RxNRnQ==",
       "peer": true,
       "dependencies": {
-        "@octokit/auth-token": "^2.4.4",
-        "@octokit/graphql": "^4.5.8",
-        "@octokit/request": "^5.6.3",
-        "@octokit/request-error": "^2.0.5",
-        "@octokit/types": "^6.0.3",
-        "before-after-hook": "^2.2.0",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/auth-token": "^5.0.0",
+        "@octokit/graphql": "^8.0.0",
+        "@octokit/request": "^9.0.0",
+        "@octokit/request-error": "^6.0.1",
+        "@octokit/types": "^12.0.0",
+        "before-after-hook": "^3.0.2",
+        "universal-user-agent": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/auth-token": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.0.1.tgz",
+      "integrity": "sha512-RTmWsLfig8SBoiSdgvCht4BXl1CHU89Co5xiQ5JF19my/sIRDFCQ1RPrmK0exgqUZuNm39C/bV8+/83+MJEjGg==",
+      "peer": true,
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/endpoint": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.0.0.tgz",
+      "integrity": "sha512-emBcNDxBdC1y3+knJonS5zhUB/CG6TihubxM2U1/pG/Z1y3a4oV0Gzz3lmkCvWWQI6h3tqBAX9MgCBFp+M68Jw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/openapi-types": {
+      "version": "20.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
+      "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
+      "peer": true
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/request": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.0.1.tgz",
+      "integrity": "sha512-kL+cAcbSl3dctYLuJmLfx6Iku2MXXy0jszhaEIjQNaCp4zjHXrhVAHeuaRdNvJjW9qjl3u1MJ72+OuBP0YW/pg==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/endpoint": "^10.0.0",
+        "@octokit/request-error": "^6.0.1",
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
       }
     },
     "node_modules/@octokit/core/node_modules/@octokit/request-error": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
-      "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.0.2.tgz",
+      "integrity": "sha512-WtRVpoHcNXs84+s9s/wqfHaxM68NGMg8Av7h59B50OVO0PwwMx+2GgQ/OliUd0iQBSNWgR6N8afi/KjSHbXHWw==",
       "peer": true,
       "dependencies": {
-        "@octokit/types": "^6.0.3",
-        "deprecation": "^2.0.0",
-        "once": "^1.4.0"
+        "@octokit/types": "^12.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
       }
     },
+    "node_modules/@octokit/core/node_modules/@octokit/types": {
+      "version": "12.6.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
+      "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/openapi-types": "^20.0.0"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/before-after-hook": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
+      "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
+      "peer": true
+    },
     "node_modules/@octokit/core/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+      "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
       "peer": true
     },
     "node_modules/@octokit/endpoint": {
@@ -70,31 +226,89 @@
       }
     },
     "node_modules/@octokit/endpoint/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+      "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
     },
     "node_modules/@octokit/graphql": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
-      "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.0.1.tgz",
+      "integrity": "sha512-lLDb6LhC1gBj2CxEDa5Xk10+H/boonhs+3Mi6jpRyetskDKNHe6crMeKmUE2efoLofMP8ruannLlCUgpTFmVzQ==",
       "peer": true,
       "dependencies": {
-        "@octokit/request": "^5.6.0",
-        "@octokit/types": "^6.0.3",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/request": "^9.0.0",
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.0.0.tgz",
+      "integrity": "sha512-emBcNDxBdC1y3+knJonS5zhUB/CG6TihubxM2U1/pG/Z1y3a4oV0Gzz3lmkCvWWQI6h3tqBAX9MgCBFp+M68Jw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
+      "version": "20.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
+      "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
+      "peer": true
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/request": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.0.1.tgz",
+      "integrity": "sha512-kL+cAcbSl3dctYLuJmLfx6Iku2MXXy0jszhaEIjQNaCp4zjHXrhVAHeuaRdNvJjW9qjl3u1MJ72+OuBP0YW/pg==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/endpoint": "^10.0.0",
+        "@octokit/request-error": "^6.0.1",
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/request-error": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.0.2.tgz",
+      "integrity": "sha512-WtRVpoHcNXs84+s9s/wqfHaxM68NGMg8Av7h59B50OVO0PwwMx+2GgQ/OliUd0iQBSNWgR6N8afi/KjSHbXHWw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/types": "^12.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/types": {
+      "version": "12.6.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
+      "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/openapi-types": "^20.0.0"
       }
     },
     "node_modules/@octokit/graphql/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+      "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
       "peer": true
     },
     "node_modules/@octokit/openapi-types": {
-      "version": "11.2.0",
-      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz",
-      "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA=="
+      "version": "12.11.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz",
+      "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="
     },
     "node_modules/@octokit/plugin-paginate-rest": {
       "version": "1.1.2",
@@ -179,9 +393,9 @@
       }
     },
     "node_modules/@octokit/request/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+      "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
     },
     "node_modules/@octokit/rest": {
       "version": "16.43.2",
@@ -207,11 +421,25 @@
       }
     },
     "node_modules/@octokit/types": {
-      "version": "6.34.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz",
-      "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==",
+      "version": "6.41.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
+      "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==",
       "dependencies": {
-        "@octokit/openapi-types": "^11.2.0"
+        "@octokit/openapi-types": "^12.11.0"
+      }
+    },
+    "node_modules/@one-ini/wasm": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+      "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
       }
     },
     "node_modules/@types/expect": {
@@ -220,23 +448,29 @@
       "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg=="
     },
     "node_modules/@types/node": {
-      "version": "14.18.21",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz",
-      "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q=="
+      "version": "20.11.24",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
+      "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
     },
     "node_modules/@types/vinyl": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz",
-      "integrity": "sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g==",
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.11.tgz",
+      "integrity": "sha512-vPXzCLmRp74e9LsP8oltnWKTH+jBwt86WgRUb4Pc9Lf3pkMVGyvIo2gm9bODeGfCay2DBB/hAWDuvf07JcK4rw==",
       "dependencies": {
         "@types/expect": "^1.20.4",
         "@types/node": "*"
       }
     },
     "node_modules/abbrev": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+      "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
     },
     "node_modules/accord": {
       "version": "0.29.0",
@@ -590,9 +824,15 @@
       }
     },
     "node_modules/async-each": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
-      "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ=="
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz",
+      "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ]
     },
     "node_modules/async-settle": {
       "version": "1.0.0",
@@ -703,9 +943,9 @@
       }
     },
     "node_modules/before-after-hook": {
-      "version": "2.2.2",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
-      "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+      "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
     },
     "node_modules/better-console": {
       "version": "1.0.1",
@@ -735,6 +975,15 @@
         "url": "https://bevry.me/fund"
       }
     },
+    "node_modules/bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "optional": true,
+      "dependencies": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -765,9 +1014,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.20.4",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz",
-      "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==",
+      "version": "4.23.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -776,14 +1025,17 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001349",
-        "electron-to-chromium": "^1.4.147",
-        "escalade": "^3.1.1",
-        "node-releases": "^2.0.5",
-        "picocolors": "^1.0.0"
+        "caniuse-lite": "^1.0.30001587",
+        "electron-to-chromium": "^1.4.668",
+        "node-releases": "^2.0.14",
+        "update-browserslist-db": "^1.0.13"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -792,22 +1044,20 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
-    "node_modules/browserslist/node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-    },
     "node_modules/btoa-lite": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
       "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA=="
     },
     "node_modules/buffer-equal": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz",
-      "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz",
+      "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==",
       "engines": {
-        "node": ">=0.4.0"
+        "node": ">=0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/buffer-from": {
@@ -835,12 +1085,18 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
-      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.0.2"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -855,9 +1111,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001352",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz",
-      "integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==",
+      "version": "1.0.30001591",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
+      "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -866,6 +1122,10 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ]
     },
@@ -948,61 +1208,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/class-utils/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/class-utils/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/class-utils/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/clean-css": {
@@ -1171,14 +1386,20 @@
       }
     },
     "node_modules/commander": {
-      "version": "2.20.3",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+      "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+      "engines": {
+        "node": ">=14"
+      }
     },
     "node_modules/component-emitter": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
-      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+      "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
     },
     "node_modules/concat-map": {
       "version": "0.0.1",
@@ -1217,12 +1438,9 @@
       }
     },
     "node_modules/convert-source-map": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
-      "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
-      "dependencies": {
-        "safe-buffer": "~5.1.1"
-      }
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
     },
     "node_modules/copy-anything": {
       "version": "2.0.6",
@@ -1284,12 +1502,15 @@
       }
     },
     "node_modules/d": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
-      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+      "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
       "dependencies": {
-        "es5-ext": "^0.10.50",
-        "type": "^1.0.1"
+        "es5-ext": "^0.10.64",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.12"
       }
     },
     "node_modules/dateformat": {
@@ -1317,9 +1538,9 @@
       }
     },
     "node_modules/decode-uri-component": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
-      "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+      "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
       "engines": {
         "node": ">=0.10"
       }
@@ -1336,9 +1557,9 @@
       }
     },
     "node_modules/deepmerge": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
-      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -1362,11 +1583,28 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/define-properties": {
+    "node_modules/define-data-property": {
       "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
-      "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "dependencies": {
+        "define-data-property": "^1.0.1",
         "has-property-descriptors": "^1.0.0",
         "object-keys": "^1.1.1"
       },
@@ -1494,24 +1732,73 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+    },
     "node_modules/editorconfig": {
-      "version": "0.15.3",
-      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
-      "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+      "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
       "dependencies": {
-        "commander": "^2.19.0",
-        "lru-cache": "^4.1.5",
-        "semver": "^5.6.0",
-        "sigmund": "^1.0.1"
+        "@one-ini/wasm": "0.1.1",
+        "commander": "^10.0.0",
+        "minimatch": "9.0.1",
+        "semver": "^7.5.3"
       },
       "bin": {
         "editorconfig": "bin/editorconfig"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/editorconfig/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/editorconfig/node_modules/minimatch": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+      "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/editorconfig/node_modules/semver": {
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.151",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.151.tgz",
-      "integrity": "sha512-XaG2LpZi9fdiWYOqJh0dJy4SlVywCvpgYXhzOlZTp4JqSKqxn5URqOjbm9OMYB3aInA2GuHQiem1QUOc1yT0Pw=="
+      "version": "1.4.690",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz",
+      "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA=="
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     "node_modules/end-of-stream": {
       "version": "1.4.4",
@@ -1521,6 +1808,18 @@
         "once": "^1.4.0"
       }
     },
+    "node_modules/errno": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+      "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+      "optional": true,
+      "dependencies": {
+        "prr": "~1.0.1"
+      },
+      "bin": {
+        "errno": "cli.js"
+      }
+    },
     "node_modules/error-ex": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -1529,14 +1828,34 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "node_modules/es-define-property": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+      "dependencies": {
+        "get-intrinsic": "^1.2.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es5-ext": {
-      "version": "0.10.61",
-      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.61.tgz",
-      "integrity": "sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==",
+      "version": "0.10.64",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+      "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
       "hasInstallScript": true,
       "dependencies": {
         "es6-iterator": "^2.0.3",
         "es6-symbol": "^3.1.3",
+        "esniff": "^2.0.1",
         "next-tick": "^1.1.0"
       },
       "engines": {
@@ -1554,12 +1873,15 @@
       }
     },
     "node_modules/es6-symbol": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
-      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+      "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
       "dependencies": {
-        "d": "^1.0.1",
-        "ext": "^1.1.2"
+        "d": "^1.0.2",
+        "ext": "^1.7.0"
+      },
+      "engines": {
+        "node": ">=0.12"
       }
     },
     "node_modules/es6-weak-map": {
@@ -1574,9 +1896,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
       "engines": {
         "node": ">=6"
       }
@@ -1589,6 +1911,29 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/esniff": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+      "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+      "dependencies": {
+        "d": "^1.0.1",
+        "es5-ext": "^0.10.62",
+        "event-emitter": "^0.3.5",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
     "node_modules/execa": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
@@ -1634,61 +1979,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/expand-brackets/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/expand-brackets/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/expand-brackets/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/expand-tilde": {
@@ -1703,18 +2003,13 @@
       }
     },
     "node_modules/ext": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
-      "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
       "dependencies": {
-        "type": "^2.5.0"
+        "type": "^2.7.2"
       }
     },
-    "node_modules/ext/node_modules/type": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz",
-      "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ=="
-    },
     "node_modules/extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -1803,6 +2098,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+      "optional": true
+    },
     "node_modules/fill-range": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -1968,6 +2269,86 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/foreground-child": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
+      "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/fork-stream": {
       "version": "0.0.4",
       "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz",
@@ -2010,10 +2391,31 @@
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
     },
+    "node_modules/fsevents": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+      "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+      "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "dependencies": {
+        "bindings": "^1.5.0",
+        "nan": "^2.12.1"
+      },
+      "engines": {
+        "node": ">= 4.0"
+      }
+    },
     "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/get-caller-file": {
       "version": "1.0.3",
@@ -2033,13 +2435,18 @@
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
-      "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.3"
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "hasown": "^2.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -2216,10 +2623,21 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "dependencies": {
+        "get-intrinsic": "^1.1.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/graceful-fs": {
-      "version": "4.2.10",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
-      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
     },
     "node_modules/growly": {
       "version": "1.3.0",
@@ -2889,21 +3307,32 @@
       }
     },
     "node_modules/gulp-json-editor": {
-      "version": "2.5.6",
-      "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.5.6.tgz",
-      "integrity": "sha512-66Xr6Q6m4mUNd0OOHflMB/RHgFNnLjlHgizOzUcx9CyMRymVZEM+/SpZcCDlvThBdXtQwXpdvtSepxVY/V6nQA==",
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.6.0.tgz",
+      "integrity": "sha512-Ni0ZUpNrhesHiTlHQth/Nv1rXCn0LUicEvzA5XuGy186C4PVeNoRjfuAIQrbmt3scKv8dgGbCs0hd77ScTw7hA==",
       "dependencies": {
-        "deepmerge": "^4.2.2",
-        "detect-indent": "^6.0.0",
-        "js-beautify": "^1.13.13",
-        "plugin-error": "^1.0.1",
+        "deepmerge": "^4.3.1",
+        "detect-indent": "^6.1.0",
+        "js-beautify": "^1.14.11",
+        "plugin-error": "^2.0.1",
         "through2": "^4.0.2"
       }
     },
+    "node_modules/gulp-json-editor/node_modules/plugin-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz",
+      "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==",
+      "dependencies": {
+        "ansi-colors": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/gulp-json-editor/node_modules/readable-stream": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
-      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
       "dependencies": {
         "inherits": "^2.0.3",
         "string_decoder": "^1.1.1",
@@ -3228,11 +3657,11 @@
       }
     },
     "node_modules/gulp-replace": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.3.tgz",
-      "integrity": "sha512-HcPHpWY4XdF8zxYkDODHnG2+7a3nD/Y8Mfu3aBgMiCFDW3X2GiOKXllsAmILcxe3KZT2BXoN18WrpEFm48KfLQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz",
+      "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==",
       "dependencies": {
-        "@types/node": "^14.14.41",
+        "@types/node": "*",
         "@types/vinyl": "^2.0.4",
         "istextorbinary": "^3.0.0",
         "replacestream": "^4.0.3",
@@ -3340,9 +3769,9 @@
       }
     },
     "node_modules/gulp-uglify/node_modules/uglify-js": {
-      "version": "3.16.0",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.0.tgz",
-      "integrity": "sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==",
+      "version": "3.17.4",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+      "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
       "bin": {
         "uglifyjs": "bin/uglifyjs"
       },
@@ -3445,7 +3874,7 @@
     "node_modules/gulp-util/node_modules/vinyl": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
-      "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=",
+      "integrity": "sha512-P5zdf3WB9uzr7IFoVQ2wZTmUwHL8cMZWJGzLBNCHNZ3NB6HTMsYABtt7z8tAGIINLXyAob9B9a1yzVGMFOYKEA==",
       "dependencies": {
         "clone": "^1.0.0",
         "clone-stats": "^0.0.1",
@@ -3466,17 +3895,6 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dependencies": {
-        "function-bind": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
     "node_modules/has-ansi": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
@@ -3508,11 +3926,22 @@
       }
     },
     "node_modules/has-property-descriptors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
-      "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dependencies": {
-        "get-intrinsic": "^1.1.1"
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3565,6 +3994,17 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/hasown": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
+      "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/homedir-polyfill": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -3592,6 +4032,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+      "optional": true,
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/import-regex": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/import-regex/-/import-regex-1.1.0.tgz",
@@ -3754,22 +4206,14 @@
       }
     },
     "node_modules/is-accessor-descriptor": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
-      "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
+      "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==",
       "dependencies": {
-        "kind-of": "^6.0.0"
+        "hasown": "^2.0.0"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
-      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.10"
       }
     },
     "node_modules/is-arrayish": {
@@ -3794,54 +4238,37 @@
       "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
     },
     "node_modules/is-core-module": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
-      "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==",
+      "version": "2.13.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
       "dependencies": {
-        "has": "^1.0.3"
+        "hasown": "^2.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/is-data-descriptor": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
-      "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz",
+      "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==",
       "dependencies": {
-        "kind-of": "^6.0.0"
+        "hasown": "^2.0.0"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
-      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/is-descriptor": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
-      "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz",
+      "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==",
       "dependencies": {
-        "is-accessor-descriptor": "^1.0.0",
-        "is-data-descriptor": "^1.0.0",
-        "kind-of": "^6.0.2"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-descriptor/node_modules/kind-of": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
-      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/is-extendable": {
@@ -4060,20 +4487,38 @@
         "url": "https://bevry.me/fund"
       }
     },
+    "node_modules/jackspeak": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
     "node_modules/jquery": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
-      "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
     },
     "node_modules/js-beautify": {
-      "version": "1.14.3",
-      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz",
-      "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==",
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
+      "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
       "dependencies": {
         "config-chain": "^1.1.13",
-        "editorconfig": "^0.15.3",
-        "glob": "^7.1.3",
-        "nopt": "^5.0.0"
+        "editorconfig": "^1.0.4",
+        "glob": "^10.3.3",
+        "js-cookie": "^3.0.5",
+        "nopt": "^7.2.0"
       },
       "bin": {
         "css-beautify": "js/bin/css-beautify.js",
@@ -4081,7 +4526,58 @@
         "js-beautify": "js/bin/js-beautify.js"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=14"
+      }
+    },
+    "node_modules/js-beautify/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/js-beautify/node_modules/glob": {
+      "version": "10.3.10",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^2.3.5",
+        "minimatch": "^9.0.1",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+        "path-scurry": "^1.10.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/js-beautify/node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+      "engines": {
+        "node": ">=14"
       }
     },
     "node_modules/json-stable-stringify-without-jsonify": {
@@ -4449,18 +4945,20 @@
       }
     },
     "node_modules/lru-cache": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
-      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
       "dependencies": {
-        "pseudomap": "^1.0.2",
-        "yallist": "^2.1.2"
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/macos-release": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz",
-      "integrity": "sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==",
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz",
+      "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==",
       "engines": {
         "node": ">=6"
       },
@@ -4468,6 +4966,28 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/make-dir": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+      "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+      "optional": true,
+      "dependencies": {
+        "pify": "^4.0.1",
+        "semver": "^5.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/make-dir/node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "optional": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -4633,6 +5153,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "optional": true,
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/mimic-fn": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
@@ -4653,9 +5185,20 @@
       }
     },
     "node_modules/minimist": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
-      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+      "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
     },
     "node_modules/mixin-deep": {
       "version": "1.3.2",
@@ -4728,6 +5271,12 @@
       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
       "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ=="
     },
+    "node_modules/nan": {
+      "version": "2.18.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
+      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
+      "optional": true
+    },
     "node_modules/nanomatch": {
       "version": "1.2.13",
       "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -4791,6 +5340,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/native-request": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.1.0.tgz",
+      "integrity": "sha512-uZ5rQaeRn15XmpgE0xoPL8YWqcX90VtCFglYwAgkvKM5e8fog+vePLAhHxuuv/gRkrQxIeh5U3q9sMNUrENqWw==",
+      "optional": true
+    },
     "node_modules/next-tick": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
@@ -4802,9 +5357,9 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
     },
     "node_modules/node-fetch": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
       "dependencies": {
         "whatwg-url": "^5.0.0"
       },
@@ -4833,34 +5388,34 @@
       }
     },
     "node_modules/node-releases": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz",
-      "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q=="
+      "version": "2.0.14",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
     },
     "node_modules/node.extend": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz",
-      "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.3.tgz",
+      "integrity": "sha512-xwADg/okH48PvBmRZyoX8i8GJaKuJ1CqlqotlZOhUio8egD1P5trJupHKBzcPjSF9ifK2gPcEICRBnkfPqQXZw==",
       "dependencies": {
-        "has": "^1.0.3",
-        "is": "^3.2.1"
+        "hasown": "^2.0.0",
+        "is": "^3.3.0"
       },
       "engines": {
         "node": ">=0.4.0"
       }
     },
     "node_modules/nopt": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
-      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
+      "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==",
       "dependencies": {
-        "abbrev": "1"
+        "abbrev": "^2.0.0"
       },
       "bin": {
         "nopt": "bin/nopt.js"
       },
       "engines": {
-        "node": ">=6"
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
     "node_modules/normalize-package-data": {
@@ -4957,47 +5512,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/object-copy/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/object-copy/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/object-copy/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
-      "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/object-copy/node_modules/kind-of": {
@@ -5031,13 +5555,13 @@
       }
     },
     "node_modules/object.assign": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
-      "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+      "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "define-properties": "^1.1.3",
-        "has-symbols": "^1.0.1",
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "has-symbols": "^1.0.3",
         "object-keys": "^1.1.1"
       },
       "engines": {
@@ -5303,6 +5827,29 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/path-scurry": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
+      "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+      "dependencies": {
+        "lru-cache": "^9.1.1 || ^10.0.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
+      "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
+      "engines": {
+        "node": "14 || >=16.14"
+      }
+    },
     "node_modules/path-type": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
@@ -5462,10 +6009,11 @@
       "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
       "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="
     },
-    "node_modules/pseudomap": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
-      "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
+    "node_modules/prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+      "optional": true
     },
     "node_modules/pump": {
       "version": "2.0.1",
@@ -5512,9 +6060,9 @@
       }
     },
     "node_modules/readable-stream": {
-      "version": "2.3.7",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
-      "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
       "dependencies": {
         "core-util-is": "~1.0.0",
         "inherits": "~2.0.3",
@@ -5525,6 +6073,11 @@
         "util-deprecate": "~1.0.1"
       }
     },
+    "node_modules/readable-stream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
     "node_modules/readdirp": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
@@ -5708,11 +6261,11 @@
       "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug=="
     },
     "node_modules/resolve": {
-      "version": "1.22.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
-      "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+      "version": "1.22.8",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
       "dependencies": {
-        "is-core-module": "^2.8.1",
+        "is-core-module": "^2.13.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
       },
@@ -5963,9 +6516,23 @@
       }
     },
     "node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
     },
     "node_modules/safe-regex": {
       "version": "1.1.0",
@@ -5981,9 +6548,9 @@
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "bin": {
         "semver": "bin/semver"
       }
@@ -6004,6 +6571,22 @@
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
     },
+    "node_modules/set-function-length": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
+      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+      "dependencies": {
+        "define-data-property": "^1.1.2",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.3",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/set-value": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@@ -6053,11 +6636,6 @@
       "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
       "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="
     },
-    "node_modules/sigmund": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
-      "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g=="
-    },
     "node_modules/signal-exit": {
       "version": "3.0.7",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -6138,61 +6716,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/snapdragon/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/snapdragon/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/snapdragon/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/snapdragon/node_modules/source-map": {
@@ -6239,18 +6772,18 @@
       }
     },
     "node_modules/spdx-correct": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
-      "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
       "dependencies": {
         "spdx-expression-parse": "^3.0.0",
         "spdx-license-ids": "^3.0.0"
       }
     },
     "node_modules/spdx-exceptions": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
-      "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A=="
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="
     },
     "node_modules/spdx-expression-parse": {
       "version": "3.0.1",
@@ -6262,9 +6795,9 @@
       }
     },
     "node_modules/spdx-license-ids": {
-      "version": "3.0.11",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
-      "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g=="
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
+      "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg=="
     },
     "node_modules/split-string": {
       "version": "3.1.0",
@@ -6352,61 +6885,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/static-extend/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/static-extend/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/static-extend/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/stream-exhaust": {
@@ -6415,9 +6903,9 @@
       "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw=="
     },
     "node_modules/stream-shift": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
-      "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+      "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
     },
     "node_modules/string_decoder": {
       "version": "1.1.1",
@@ -6427,6 +6915,11 @@
         "safe-buffer": "~5.1.0"
       }
     },
+    "node_modules/string_decoder/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
     "node_modules/string-width": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -6439,6 +6932,47 @@
         "node": ">=4"
       }
     },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/string-width/node_modules/ansi-regex": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
@@ -6482,6 +7016,26 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-bom": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
@@ -6667,7 +7221,7 @@
     "node_modules/to-absolute-glob": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
-      "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=",
+      "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==",
       "dependencies": {
         "is-absolute": "^1.0.0",
         "is-negated-glob": "^1.0.0"
@@ -6679,7 +7233,7 @@
     "node_modules/to-object-path": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
-      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==",
       "dependencies": {
         "kind-of": "^3.0.2"
       },
@@ -6715,7 +7269,7 @@
     "node_modules/to-regex-range": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
-      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==",
       "dependencies": {
         "is-number": "^3.0.0",
         "repeat-string": "^1.6.1"
@@ -6761,7 +7315,7 @@
     "node_modules/to-through": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz",
-      "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=",
+      "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==",
       "dependencies": {
         "through2": "^2.0.3"
       },
@@ -6781,7 +7335,7 @@
     "node_modules/tr46": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
     },
     "node_modules/tslib": {
       "version": "1.14.1",
@@ -6789,19 +7343,19 @@
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
     },
     "node_modules/type": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
-      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+      "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
     },
     "node_modules/typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
     },
     "node_modules/uglify-js": {
       "version": "2.8.29",
       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
-      "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+      "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==",
       "dependencies": {
         "source-map": "~0.5.1",
         "yargs": "~3.10.0"
@@ -6845,7 +7399,7 @@
     "node_modules/uglify-js/node_modules/yargs": {
       "version": "3.10.0",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
-      "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+      "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==",
       "dependencies": {
         "camelcase": "^1.0.2",
         "cliui": "^2.1.0",
@@ -6853,10 +7407,16 @@
         "window-size": "0.1.0"
       }
     },
+    "node_modules/uglify-to-browserify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+      "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==",
+      "optional": true
+    },
     "node_modules/unc-path-regex": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
-      "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
+      "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -6884,11 +7444,16 @@
     "node_modules/undertaker-registry": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz",
-      "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=",
+      "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==",
       "engines": {
         "node": ">= 0.10"
       }
     },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+    },
     "node_modules/union-value": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -6923,7 +7488,7 @@
     "node_modules/unset-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
-      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==",
       "dependencies": {
         "has-value": "^0.3.1",
         "isobject": "^3.0.0"
@@ -6973,16 +7538,50 @@
         "yarn": "*"
       }
     },
+    "node_modules/update-browserslist-db": {
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "escalade": "^3.1.1",
+        "picocolors": "^1.0.0"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/update-browserslist-db/node_modules/picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+    },
     "node_modules/urix": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
-      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==",
       "deprecated": "Please see https://github.com/lydell/urix#deprecated"
     },
     "node_modules/url-regex": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz",
-      "integrity": "sha1-260eDJ4p4QXdCx8J9oYvf9tIJyQ=",
+      "integrity": "sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==",
       "dependencies": {
         "ip-regex": "^1.0.1"
       },
@@ -7001,7 +7600,7 @@
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
     "node_modules/v8flags": {
       "version": "3.2.0",
@@ -7026,7 +7625,7 @@
     "node_modules/value-or-function": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz",
-      "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=",
+      "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==",
       "engines": {
         "node": ">= 0.10"
       }
@@ -7086,7 +7685,7 @@
     "node_modules/vinyl-sourcemap": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz",
-      "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=",
+      "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==",
       "dependencies": {
         "append-buffer": "^1.0.2",
         "convert-source-map": "^1.5.0",
@@ -7114,7 +7713,7 @@
     "node_modules/vinyl-sourcemaps-apply": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz",
-      "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=",
+      "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==",
       "dependencies": {
         "source-map": "^0.5.1"
       }
@@ -7130,12 +7729,12 @@
     "node_modules/webidl-conversions": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
     },
     "node_modules/whatwg-url": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
       "dependencies": {
         "tr46": "~0.0.3",
         "webidl-conversions": "^3.0.0"
@@ -7144,7 +7743,7 @@
     "node_modules/when": {
       "version": "3.7.8",
       "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz",
-      "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I="
+      "integrity": "sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw=="
     },
     "node_modules/which": {
       "version": "1.3.1",
@@ -7160,12 +7759,12 @@
     "node_modules/which-module": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
-      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8="
+      "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ=="
     },
     "node_modules/window-size": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
-      "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
+      "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==",
       "engines": {
         "node": ">= 0.8.0"
       }
@@ -7187,7 +7786,7 @@
     "node_modules/wordwrap": {
       "version": "0.0.2",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
-      "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+      "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==",
       "engines": {
         "node": ">=0.4.0"
       }
@@ -7195,7 +7794,7 @@
     "node_modules/wrap-ansi": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
-      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==",
       "dependencies": {
         "string-width": "^1.0.1",
         "strip-ansi": "^3.0.1"
@@ -7204,6 +7803,93 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
@@ -7231,12 +7917,12 @@
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
     },
     "node_modules/wrench-sui": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/wrench-sui/-/wrench-sui-0.0.3.tgz",
-      "integrity": "sha1-1hoSAwwf2NZxs90VqmyeD83E4sg=",
+      "integrity": "sha512-Y6qzMpcMG9akKnIdUsKzEF/Ht0KQJBP8ETkZj3FcGe93NC71e940WZUP1y+j+hc8Ecx9TyX0GvAWC4yymA88yA==",
       "engines": {
         "node": ">=0.1.97"
       }
@@ -7255,9 +7941,9 @@
       "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
     },
     "node_modules/yallist": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
-      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/yamljs": {
       "version": "0.3.0",
@@ -7293,9 +7979,9 @@
       }
     },
     "node_modules/yargs-parser": {
-      "version": "21.0.1",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
-      "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
       "engines": {
         "node": ">=12"
       }

From 1f0625a277e6ce19e5633a2624fcb7e044d04a29 Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Mon, 4 Mar 2024 21:49:21 +0100
Subject: [PATCH 271/679] Fix projects mode bugs (#29593)

Fix for regressions introduced by #28805

Enabled projects on repos created before the PR weren't detected. Also,
the way projects mode was detected in settings didn't match the way it
was detected on permission check, which leads to confusion.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 models/fixtures/repo_unit.yml        |  1 -
 models/repo/repo.go                  |  4 +++-
 models/repo/repo_unit.go             |  2 +-
 templates/repo/settings/options.tmpl | 12 ++++++------
 4 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index 6714294e2b..8a22db0445 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -520,7 +520,6 @@
   id: 75
   repo_id: 1
   type: 8
-  config: "{\"ProjectsMode\":\"all\"}"
   created_unix: 946684810
 
 -
diff --git a/models/repo/repo.go b/models/repo/repo.go
index ad2e21b66b..f6758f1591 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -412,9 +412,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
 			Config: new(ActionsConfig),
 		}
 	} else if tp == unit.TypeProjects {
+		cfg := new(ProjectsConfig)
+		cfg.ProjectsMode = ProjectsModeNone
 		return &RepoUnit{
 			Type:   tp,
-			Config: new(ProjectsConfig),
+			Config: cfg,
 		}
 	}
 
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 6b9dde7faf..5a841f4d31 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -236,7 +236,7 @@ func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
 		return cfg.ProjectsMode
 	}
 
-	return ProjectsModeNone
+	return ProjectsModeAll
 }
 
 func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 376cfe7607..0de42b34ea 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -460,19 +460,19 @@
 					</p>
 					<div class="ui dropdown selection">
 						<select name="projects_mode">
-							<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
-							<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
-							<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
+							<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
+							<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
+							<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
 						</select>
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						<div class="default text">
-							{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}
+							{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
 								{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
 							{{end}}
-							{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}
+							{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
 								{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
 							{{end}}
-							{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}
+							{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
 								{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
 							{{end}}
 						</div>

From 82875ae946b34e67beb3c89d0bd02a0fee9ad96e Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 5 Mar 2024 00:23:19 +0000
Subject: [PATCH 272/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_ja-JP.ini | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 4d1ecef61c..af06a78642 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -149,8 +149,8 @@ footer.software=ソフトウェアについて
 footer.links=リンク
 
 [heatmap]
-number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 個の貢献
-no_contributions=貢献なし
+number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 件の実績
+no_contributions=実績なし
 less=少
 more=多
 
@@ -1510,7 +1510,7 @@ issues.role.member_helper=このユーザーはこのリポジトリを所有し
 issues.role.collaborator=共同作業者
 issues.role.collaborator_helper=このユーザーはリポジトリ上で共同作業するように招待されています。
 issues.role.first_time_contributor=初めての貢献者
-issues.role.first_time_contributor_helper=これは、このユーザーのリポジトリへの最初の貢献です。
+issues.role.first_time_contributor_helper=これは、このユーザーによるリポジトリへの最初の貢献です。
 issues.role.contributor=貢献者
 issues.role.contributor_helper=このユーザーは以前にリポジトリにコミットしています。
 issues.re_request_review=レビューを再依頼
@@ -2011,7 +2011,8 @@ settings.mirror_settings.docs.more_information_if_disabled=プッシュミラー
 settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには?
 settings.mirror_settings.docs.doc_link_pull_section=ドキュメントの「リモートリポジトリからのプル」セクション。
 settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル
-settings.mirror_settings.mirrored_repository=同期するリポジトリ
+settings.mirror_settings.mirrored_repository=ミラー元のリポジトリ
+settings.mirror_settings.pushed_repository=プッシュ先のリポジトリ
 settings.mirror_settings.direction=方向
 settings.mirror_settings.direction.pull=プル
 settings.mirror_settings.direction.push=プッシュ
@@ -3546,6 +3547,8 @@ runs.actors_no_select=すべてのアクター
 runs.status_no_select=すべてのステータス
 runs.no_results=一致する結果はありません。
 runs.no_workflows=ワークフローはまだありません。
+runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? では<a target="_blank" rel="noopener noreferrer" href="%s">クイックスタートガイド</a>をご覧ください。
+runs.no_workflows.documentation=Gitea Actions の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。
 runs.no_runs=ワークフローはまだ実行されていません。
 runs.empty_commit_message=(空のコミットメッセージ)
 

From df1268ca08aaacae54c775a8eec34006dfe365e0 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 5 Mar 2024 10:12:03 +0800
Subject: [PATCH 273/679] Make "/user/login" page redirect if the current user
 has signed in (#29583)

Fix #29582 and maybe more.
Maybe fix #29116
---
 routers/web/auth/auth.go              | 30 ++++++++++++-------
 routers/web/auth/auth_test.go         | 43 +++++++++++++++++++++++++++
 routers/web/repo/wiki_test.go         |  2 +-
 services/contexttest/context_tests.go |  3 +-
 4 files changed, 66 insertions(+), 12 deletions(-)
 create mode 100644 routers/web/auth/auth_test.go

diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 04e410543d..da6bef207a 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -123,9 +123,21 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
 	return nil
 }
 
+func RedirectAfterLogin(ctx *context.Context) {
+	redirectTo := ctx.FormString("redirect_to")
+	if redirectTo == "" {
+		redirectTo = ctx.GetSiteCookie("redirect_to")
+	}
+	middleware.DeleteRedirectToCookie(ctx.Resp)
+	nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
+	if setting.LandingPageURL == setting.LandingPageLogin {
+		nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
+	}
+	ctx.RedirectToFirst(redirectTo, nextRedirectTo)
+}
+
 func CheckAutoLogin(ctx *context.Context) bool {
-	// Check auto-login
-	isSucceed, err := autoSignIn(ctx)
+	isSucceed, err := autoSignIn(ctx) // try to auto-login
 	if err != nil {
 		if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) {
 			ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true)
@@ -138,17 +150,10 @@ func CheckAutoLogin(ctx *context.Context) bool {
 	redirectTo := ctx.FormString("redirect_to")
 	if len(redirectTo) > 0 {
 		middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
-	} else {
-		redirectTo = ctx.GetSiteCookie("redirect_to")
 	}
 
 	if isSucceed {
-		middleware.DeleteRedirectToCookie(ctx.Resp)
-		nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
-		if setting.LandingPageURL == setting.LandingPageLogin {
-			nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
-		}
-		ctx.RedirectToFirst(redirectTo, nextRedirectTo)
+		RedirectAfterLogin(ctx)
 		return true
 	}
 
@@ -163,6 +168,11 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 
+	if ctx.IsSigned {
+		RedirectAfterLogin(ctx)
+		return
+	}
+
 	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go
new file mode 100644
index 0000000000..c6afbf877c
--- /dev/null
+++ b/routers/web/auth/auth_test.go
@@ -0,0 +1,43 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+	"net/http"
+	"net/url"
+	"testing"
+
+	"code.gitea.io/gitea/modules/test"
+	"code.gitea.io/gitea/services/contexttest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestUserLogin(t *testing.T) {
+	ctx, resp := contexttest.MockContext(t, "/user/login")
+	SignIn(ctx)
+	assert.Equal(t, http.StatusOK, resp.Code)
+
+	ctx, resp = contexttest.MockContext(t, "/user/login")
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, http.StatusSeeOther, resp.Code)
+	assert.Equal(t, "/", test.RedirectURL(resp))
+
+	ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other")
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, "/other", test.RedirectURL(resp))
+
+	ctx, resp = contexttest.MockContext(t, "/user/login")
+	ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"})
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, "/other-cookie", test.RedirectURL(resp))
+
+	ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com"))
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, "/", test.RedirectURL(resp))
+}
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 49c83cfef5..719cca3049 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -79,7 +79,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) {
 func TestWiki(t *testing.T) {
 	unittest.PrepareTestEnv(t)
 
-	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
+	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
 	ctx.SetParams("*", "Home")
 	contexttest.LoadRepo(t, ctx, 1)
 	Wiki(ctx)
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index 431017a30d..d3e6de7efe 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -7,6 +7,7 @@ package contexttest
 import (
 	gocontext "context"
 	"io"
+	"maps"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -36,7 +37,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
 	}
 	requestURL, err := url.Parse(path)
 	assert.NoError(t, err)
-	req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}}
+	req := &http.Request{Method: method, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}}
 	req = req.WithContext(middleware.WithContextData(req.Context()))
 	return req
 }

From ade62416917bc87810991585d7047851834ee316 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 5 Mar 2024 11:03:14 +0800
Subject: [PATCH 274/679] Use flex wrap to layout the PR update button (#29590)

Follow #29418

I think using "flex-wrap: wrap" here is better than hard-coding the screen width.

By using "flex-wrap: wrap", the UI layouts automatically for various
widths (even if in some languages, the sentence might be pretty long)
---
 web_src/css/repo.css | 11 ++---------
 1 file changed, 2 insertions(+), 9 deletions(-)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 87ce829a78..d60fb4db21 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -935,18 +935,11 @@
 
 .repository.view.issue .comment-list .comment .merge-section .item-section {
   display: flex;
+  flex-wrap: wrap;
   align-items: center;
   justify-content: space-between;
   padding: 0;
-  margin-top: -0.25rem;
-  margin-bottom: -0.25rem;
-}
-
-@media (max-width: 991.98px) {
-  .repository.view.issue .comment-list .comment .merge-section .item-section {
-    align-items: flex-start;
-    flex-direction: column;
-  }
+  gap: 0.5em;
 }
 
 .repository.view.issue .comment-list .comment .merge-section .divider {

From 72b213b00fe1aa18d082423fbc4cfffff483eb18 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 5 Mar 2024 05:31:29 +0100
Subject: [PATCH 275/679] Adjust tailwind content globs (#29596)

Tailwind content is not going to appear in `web_src/css`,
`web_src/fomantic` or `web_src/svg` or the JSON templates, so we don't
need to have tailwind scan these directories which will speed up the
build.

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 tailwind.config.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tailwind.config.js b/tailwind.config.js
index fb17980568..63a5387d19 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -29,8 +29,10 @@ export default {
   content: [
     isProduction && '!./templates/devtest/**/*',
     isProduction && '!./web_src/js/standalone/devtest.js',
+    '!./templates/swagger/v1_json.tmpl',
+    '!./templates/user/auth/oidc_wellknown.tmpl',
     './templates/**/*.tmpl',
-    './web_src/**/*.{js,vue}',
+    './web_src/js/**/*.{js,vue}',
   ].filter(Boolean),
   blocklist: [
     // classes that don't work without CSS variables from "@tailwind base" which we don't use

From 3e84bfdf410ec1fdf22711156d729e51d7355a8e Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 5 Mar 2024 12:59:16 +0800
Subject: [PATCH 276/679] Remove unnecessary ctxData for "attachments" template
 (#29600)

The "attachments" template never uses it
---
 templates/repo/diff/comments.tmpl                   | 2 +-
 templates/repo/issue/view_content.tmpl              | 2 +-
 templates/repo/issue/view_content/comments.tmpl     | 4 ++--
 templates/repo/issue/view_content/conversation.tmpl | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 99974ecf6a..f5fd7076fa 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -63,7 +63,7 @@
 			<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 			<div class="edit-content-zone gt-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
 			{{if .Attachments}}
-				{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
+				{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 			{{end}}
 		</div>
 		{{$reactions := .Reactions.GroupByType}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index edfa9c0bc5..89520ebe65 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -62,7 +62,7 @@
 						<div id="issue-{{.Issue.ID}}-raw" class="raw-content gt-hidden">{{.Issue.Content}}</div>
 						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
 						{{if .Issue.Attachments}}
-							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
+							{{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
 						{{end}}
 					</div>
 					{{$reactions := .Issue.Reactions.GroupByType}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index cb4950c18e..86cb716bb3 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -69,7 +69,7 @@
 						<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 						{{if .Attachments}}
-							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
+							{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 						{{end}}
 					</div>
 					{{$reactions := .Reactions.GroupByType}}
@@ -440,7 +440,7 @@
 							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 							{{if .Attachments}}
-								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
+								{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 							{{end}}
 						</div>
 						{{$reactions := .Reactions.GroupByType}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 5bb99d1db6..b6e075d0ce 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -95,7 +95,7 @@
 							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
 							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 							{{if .Attachments}}
-								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "RenderedContent" .RenderedContent}}
+								{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 							{{end}}
 						</div>
 						{{$reactions := .Reactions.GroupByType}}

From 7e8c1c5ba18e1ac8861f429b825163b8210fd178 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 5 Mar 2024 06:29:32 +0100
Subject: [PATCH 277/679] Replace more `gt-` with `tw-`, update frontend docs
 (#29595)

Tested a few things, all working fine. Not sure if the chinese machine
translation is good.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 .../contributing/guidelines-frontend.en-us.md    |  2 +-
 .../contributing/guidelines-frontend.zh-cn.md    |  2 +-
 templates/admin/emails/list.tmpl                 |  4 ++--
 templates/admin/packages/list.tmpl               |  4 ++--
 templates/admin/user/list.tmpl                   |  2 +-
 templates/base/head_navbar.tmpl                  |  6 +++---
 templates/base/paginate.tmpl                     |  2 +-
 templates/repo/commit_page.tmpl                  |  2 +-
 templates/repo/diff/box.tmpl                     |  2 +-
 templates/repo/diff/options_dropdown.tmpl        |  4 ++--
 templates/repo/diff/section_split.tmpl           |  8 ++++----
 templates/repo/diff/section_unified.tmpl         |  2 +-
 templates/repo/diff/whitespace_dropdown.tmpl     | 16 ++++++++--------
 templates/repo/issue/card.tmpl                   |  2 +-
 .../repo/issue/labels/labels_selector_field.tmpl |  4 ++--
 templates/repo/issue/new_form.tmpl               |  2 +-
 templates/repo/issue/view_content/sidebar.tmpl   |  6 +++---
 templates/repo/settings/branches.tmpl            |  2 +-
 templates/status/500.tmpl                        |  2 +-
 web_src/css/helpers.css                          |  9 ---------
 web_src/js/features/repo-diff.js                 |  4 ++--
 web_src/js/features/repo-issue.js                |  4 ++--
 web_src/js/features/repo-legacy.js               |  6 +++---
 web_src/js/features/user-auth.js                 |  4 ++--
 web_src/js/markup/mermaid.js                     |  4 ++--
 25 files changed, 48 insertions(+), 57 deletions(-)

diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index a33a38a6f9..2c0aaaed4a 100644
--- a/docs/content/contributing/guidelines-frontend.en-us.md
+++ b/docs/content/contributing/guidelines-frontend.en-us.md
@@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
 11. Custom event names are recommended to use `ce-` prefix.
-12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
+12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
 
 ### Accessibility / ARIA
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index 43f72b4808..ace0d97f49 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
 11. 推荐使用自定义事件名称前缀`ce-`。
-12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。
+12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
 13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
 
 ### 可访问性 / ARIA
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index bcd80368e6..29fbb5f039 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -47,8 +47,8 @@
 					{{range .Emails}}
 						<tr>
 							<td><a href="{{AppSubUrl}}/{{.Name | PathEscape}}">{{.Name}}</a></td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.FullName}}</td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td>
+							<td class="gt-ellipsis tw-max-w-48">{{.FullName}}</td>
+							<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
 							<td>{{if .IsPrimary}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>
 								{{if .CanChange}}
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 1f86803d55..aef4815424 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -62,8 +62,8 @@
 								{{end}}
 							</td>
 							<td>{{.Package.Type.Name}}</td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.Package.Name}}</td>
-							<td class="gt-ellipsis gt-max-width-12rem"><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
+							<td class="gt-ellipsis tw-max-w-48">{{.Package.Name}}</td>
+							<td class="gt-ellipsis tw-max-w-48"><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
 							<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
 							<td>
 							{{if .Repository}}
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 8fdc80fc70..e9ce17ac90 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -96,7 +96,7 @@
 									<span class="ui mini label">{{ctx.Locale.Tr "admin.users.remote"}}</span>
 								{{end}}
 							</td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td>
+							<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
 							<td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 51eeea405a..4f48dc82c3 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -14,7 +14,7 @@
 		<div class="ui secondary menu item navbar-mobile-right">
 			{{if .IsSigned}}
 			<a id="mobile-notifications-icon" class="item tw-w-auto gt-p-3" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
-				<div class="gt-relative">
+				<div class="tw-relative">
 					{{svg "octicon-bell"}}
 					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
@@ -76,7 +76,7 @@
 		{{else if .IsSigned}}
 			{{if EnableTimetracking}}
 			<a class="active-stopwatch-trigger item gt-mx-0{{if not .ActiveStopwatch}} gt-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
-				<div class="gt-relative">
+				<div class="tw-relative">
 					{{svg "octicon-stopwatch"}}
 					<span class="header-stopwatch-dot"></span>
 				</div>
@@ -112,7 +112,7 @@
 			{{end}}
 
 			<a class="item not-mobile gt-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
-				<div class="gt-relative">
+				<div class="tw-relative">
 					{{svg "octicon-bell"}}
 					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl
index ef7d0b341b..8c2adc1f94 100644
--- a/templates/base/paginate.tmpl
+++ b/templates/base/paginate.tmpl
@@ -17,7 +17,7 @@
 					{{if eq .Num -1}}
 						<a class="disabled item">...</a>
 					{{else}}
-						<a class="{{if .IsCurrent}}active {{end}}item gt-content-center" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a>
+						<a class="{{if .IsCurrent}}active {{end}}item tw-content-center" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a>
 					{{end}}
 				{{end}}
 				<a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$paginationLink}}?page={{.Next}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 1d900cdefb..80af73ce48 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -17,7 +17,7 @@
 				{{$class = (print $class " isWarning")}}
 			{{end}}
 		{{end}}
-		<div class="ui top attached header clearing segment gt-relative commit-header {{$class}}">
+		<div class="ui top attached header clearing segment tw-relative commit-header {{$class}}">
 			<div class="gt-df gt-mb-4 gt-fw">
 				<h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
 				{{if not $.PageIsWiki}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 1cb3aaaa21..39df6faea5 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -112,7 +112,7 @@
 					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
 						<h4 class="diff-file-header sticky-2nd-row ui top attached normal header gt-df gt-ac gt-sb gt-fw">
 							<div class="diff-file-name gt-df gt-ac gt-gap-2 gt-fw">
-								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} gt-invisible{{end}}">
+								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
 										{{svg "octicon-chevron-right" 18}}
 									{{else}}
diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl
index b7c46dd846..09b7b80e41 100644
--- a/templates/repo/diff/options_dropdown.tmpl
+++ b/templates/repo/diff/options_dropdown.tmpl
@@ -17,13 +17,13 @@
 		{{if .Issue.Index}}
 			{{if .ShowOutdatedComments}}
 				<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=false">
-					<label class="gt-pointer-events-none">
+					<label class="tw-pointer-events-none">
 						{{ctx.Locale.Tr "repo.issues.review.option.hide_outdated_comments"}}
 					</label>
 				</a>
 			{{else}}
 				<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=true">
-					<label class="gt-pointer-events-none">
+					<label class="tw-pointer-events-none">
 						{{ctx.Locale.Tr "repo.issues.review.option.show_outdated_comments"}}
 					</label>
 				</a>
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index 672193565b..0999d36f20 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -47,7 +47,7 @@
 					<td class="lines-type-marker lines-type-marker-old del-code"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
 					<td class="lines-code lines-code-old del-code">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
@@ -62,7 +62,7 @@
 					<td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="gt-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-new add-code">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} gt-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
@@ -79,7 +79,7 @@
 					<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-old">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
@@ -94,7 +94,7 @@
 					<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-new">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index 2c271d0866..2fc116a991 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -52,7 +52,7 @@
 			{{else}}
 				<td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}">{{/*
 					*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
-						*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{/*
+						*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{/*
 							*/}}{{svg "octicon-plus"}}{{/*
 						*/}}</button>{{/*
 					*/}}{{end}}{{/*
diff --git a/templates/repo/diff/whitespace_dropdown.tmpl b/templates/repo/diff/whitespace_dropdown.tmpl
index 7bf2ac9aec..cfabf836d6 100644
--- a/templates/repo/diff/whitespace_dropdown.tmpl
+++ b/templates/repo/diff/whitespace_dropdown.tmpl
@@ -2,26 +2,26 @@
 	{{svg "gitea-whitespace"}}
 	<div class="menu">
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=show-all&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_show_everything"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-all&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_all_whitespace"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-change&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_amount_changes"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-eol&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_at_eol"}}
 			</label>
 		</a>
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 5e524079c8..ff635c736a 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -7,7 +7,7 @@
 		</div>
 	{{end}}
 	<div class="content gt-p-0 tw-w-full">
-		<div class="gt-df gt-items-start">
+		<div class="gt-df tw-items-start">
 			<div class="issue-card-icon">
 				{{template "shared/issueicon" .}}
 			</div>
diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl
index d24dac46eb..e42a1de895 100644
--- a/templates/repo/issue/labels/labels_selector_field.tmpl
+++ b/templates/repo/issue/labels/labels_selector_field.tmpl
@@ -21,7 +21,7 @@
 					<div class="divider"></div>
 				{{end}}
 				{{$previousExclusiveScope = $exclusiveScope}}
-				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}gt-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
+				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 					<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
 				</a>
@@ -34,7 +34,7 @@
 					<div class="divider"></div>
 				{{end}}
 				{{$previousExclusiveScope = $exclusiveScope}}
-				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}gt-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
+				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 					<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
 				</a>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index e67314bfd5..b2b9e308f5 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -156,7 +156,7 @@
 					<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
 					{{range .Assignees}}
 						<a class="item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
-							<span class="octicon-check gt-invisible">{{svg "octicon-check"}}</span>
+							<span class="octicon-check tw-invisible">{{svg "octicon-check"}}</span>
 							<span class="text">
 								{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3"}}{{template "repo/search_name" .}}
 							</span>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index f5b6751d6d..329a39dd69 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -20,7 +20,7 @@
 					{{range .Reviewers}}
 						{{if .User}}
 							<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_{{.ItemID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
-								<span class="octicon-check {{if not .Checked}}gt-invisible{{end}}">{{svg "octicon-check"}}</span>
+								<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
 								<span class="text">
 									{{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}}
 								</span>
@@ -35,7 +35,7 @@
 					{{range .TeamReviewers}}
 						{{if .Team}}
 							<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
-								<span class="octicon-check {{if not .Checked}}gt-invisible{{end}}">{{svg "octicon-check" 16}}</span>
+								<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span>
 								<span class="text">
 									{{svg "octicon-people" 16 "gt-ml-4 gt-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
 								</span>
@@ -231,7 +231,7 @@
 							{{$checked = true}}
 						{{end}}
 					{{end}}
-					<span class="octicon-check {{if not $checked}}gt-invisible{{end}}">{{svg "octicon-check"}}</span>
+					<span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
 					<span class="text">
 						{{ctx.AvatarUtils.Avatar . 20 "gt-mr-3"}}{{template "repo/search_name" .}}
 					</span>
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index f8896b504e..78421ec009 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -16,7 +16,7 @@
 					{{.CsrfTokenHtml}}
 					<input type="hidden" name="action" value="default_branch">
 					{{if not .Repository.IsEmpty}}
-						<div class="ui dropdown selection gt-f1 gt-mr-3 gt-max-width-24rem">
+						<div class="ui dropdown selection gt-f1 gt-mr-3 tw-max-w-96">
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 							<input type="hidden" name="branch" value="{{.Repository.DefaultBranch}}">
 							<div class="default text">{{.Repository.DefaultBranch}}</div>
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index 03d0183280..a821fe55da 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -38,7 +38,7 @@
 			<div class="ui container gt-my-5">
 				{{if .ErrorMsg}}
 					<p>{{ctx.Locale.Tr "error.occurred"}}:</p>
-					<pre class="tw-whitespace-pre-wrap gt-break-all">{{.ErrorMsg}}</pre>
+					<pre class="tw-whitespace-pre-wrap tw-break-all">{{.ErrorMsg}}</pre>
 				{{end}}
 				<div class="center gt-mt-5">
 					{{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 71f3a619b9..dad0f9b127 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -46,16 +46,7 @@ Gitea's private styles use `g-` prefix.
   text-overflow: ellipsis;
 }
 
-.gt-max-width-12rem { max-width: 12rem !important; }
-.gt-max-width-24rem { max-width: 24rem !important; }
-
 /* below class names match Tailwind CSS */
-.gt-break-all { word-break: break-all !important; }
-.gt-content-center { align-content: center !important; }
-.gt-invisible { visibility: hidden !important; }
-.gt-items-start { align-items: flex-start !important; }
-.gt-pointer-events-none { pointer-events: none !important; }
-.gt-relative { position: relative !important; }
 .gt-object-contain { object-fit: contain !important; }
 .gt-no-underline { text-decoration-line: none !important; }
 .gt-normal-case { text-transform: none !important; }
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 5c73bf4bbc..77691e15e6 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -69,9 +69,9 @@ function initRepoDiffConversationForm() {
 
       $form.closest('.conversation-holder').replaceWith($newConversationHolder);
       if ($form.closest('tr').data('line-type') === 'same') {
-        $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).addClass('gt-invisible');
+        $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).addClass('tw-invisible');
       } else {
-        $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).addClass('gt-invisible');
+        $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).addClass('tw-invisible');
       }
       $newConversationHolder.find('.dropdown').dropdown();
       initCompReactionSelector($newConversationHolder);
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 10faeb135d..6fb13b0dda 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -180,9 +180,9 @@ export function initRepoIssueCommentDelete() {
           const idx = $conversationHolder.data('idx');
           const lineType = $conversationHolder.closest('tr').data('line-type');
           if (lineType === 'same') {
-            $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).removeClass('gt-invisible');
+            $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).removeClass('tw-invisible');
           } else {
-            $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).removeClass('gt-invisible');
+            $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).removeClass('tw-invisible');
           }
           $conversationHolder.remove();
         }
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 10ad836797..8fcc78c177 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -150,7 +150,7 @@ export function initRepoCommentForm() {
 
         if ($(this).hasClass('checked')) {
           $(this).removeClass('checked');
-          $(this).find('.octicon-check').addClass('gt-invisible');
+          $(this).find('.octicon-check').addClass('tw-invisible');
           if (hasUpdateAction) {
             if (!($(this).data('id') in items)) {
               items[$(this).data('id')] = {
@@ -164,7 +164,7 @@ export function initRepoCommentForm() {
           }
         } else {
           $(this).addClass('checked');
-          $(this).find('.octicon-check').removeClass('gt-invisible');
+          $(this).find('.octicon-check').removeClass('tw-invisible');
           if (hasUpdateAction) {
             if (!($(this).data('id') in items)) {
               items[$(this).data('id')] = {
@@ -218,7 +218,7 @@ export function initRepoCommentForm() {
 
       $(this).parent().find('.item').each(function () {
         $(this).removeClass('checked');
-        $(this).find('.octicon-check').addClass('gt-invisible');
+        $(this).find('.octicon-check').addClass('tw-invisible');
       });
 
       if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js
index 60d186e699..a871ac471c 100644
--- a/web_src/js/features/user-auth.js
+++ b/web_src/js/features/user-auth.js
@@ -9,13 +9,13 @@ export function initUserAuthOauth2() {
 
   for (const link of outer.querySelectorAll('.oauth-login-link')) {
     link.addEventListener('click', () => {
-      inner.classList.add('gt-invisible');
+      inner.classList.add('tw-invisible');
       outer.classList.add('is-loading');
       setTimeout(() => {
         // recover previous content to let user try again
         // usually redirection will be performed before this action
         outer.classList.remove('is-loading');
-        inner.classList.remove('gt-invisible');
+        inner.classList.remove('tw-invisible');
       }, 5000);
     });
   }
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
index 84d88a94c3..82e9909fec 100644
--- a/web_src/js/markup/mermaid.js
+++ b/web_src/js/markup/mermaid.js
@@ -45,7 +45,7 @@ export async function renderMermaid() {
       const {svg} = await mermaid.render('mermaid', source);
 
       const iframe = document.createElement('iframe');
-      iframe.classList.add('markup-render', 'gt-invisible');
+      iframe.classList.add('markup-render', 'tw-invisible');
       iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
 
       const mermaidBlock = document.createElement('div');
@@ -62,7 +62,7 @@ export async function renderMermaid() {
         iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
         setTimeout(() => { // avoid flash of iframe background
           mermaidBlock.classList.remove('is-loading');
-          iframe.classList.remove('gt-invisible');
+          iframe.classList.remove('tw-invisible');
         }, 0);
       });
 

From 4fd9c56ed09b31e2f6164a5f534a31c6624d0478 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Tue, 5 Mar 2024 13:55:47 +0800
Subject: [PATCH 278/679] Skip email domain check when admin users adds user
 manually (#29522)

Fix #27457

Administrators should be able to manually create any user even if the
user's email address is not in `EMAIL_DOMAIN_ALLOWLIST`.
---
 models/user/email_address.go        | 75 ++++++++++++++++++-----------
 models/user/user.go                 | 20 +++++++-
 routers/api/v1/admin/user.go        |  2 +-
 routers/web/admin/users.go          |  2 +-
 tests/integration/api_admin_test.go | 26 ++++++++++
 5 files changed, 93 insertions(+), 32 deletions(-)

diff --git a/models/user/email_address.go b/models/user/email_address.go
index 5d67304691..3cb2e8268c 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -154,37 +154,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
 
 var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
 
-// ValidateEmail check if email is a allowed address
+// ValidateEmail check if email is a valid & allowed address
 func ValidateEmail(email string) error {
-	if len(email) == 0 {
-		return ErrEmailInvalid{email}
+	if err := validateEmailBasic(email); err != nil {
+		return err
 	}
+	return validateEmailDomain(email)
+}
 
-	if !emailRegexp.MatchString(email) {
-		return ErrEmailCharIsNotSupported{email}
-	}
-
-	if email[0] == '-' {
-		return ErrEmailInvalid{email}
-	}
-
-	if _, err := mail.ParseAddress(email); err != nil {
-		return ErrEmailInvalid{email}
-	}
-
-	// if there is no allow list, then check email against block list
-	if len(setting.Service.EmailDomainAllowList) == 0 &&
-		validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
-		return ErrEmailInvalid{email}
-	}
-
-	// if there is an allow list, then check email against allow list
-	if len(setting.Service.EmailDomainAllowList) > 0 &&
-		!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
-		return ErrEmailInvalid{email}
-	}
-
-	return nil
+// ValidateEmailForAdmin check if email is a valid address when admins manually add users
+func ValidateEmailForAdmin(email string) error {
+	return validateEmailBasic(email)
+	// In this case we do not need to check the email domain
 }
 
 func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
@@ -534,3 +515,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
 
 	return committer.Commit()
 }
+
+// validateEmailBasic checks whether the email complies with the rules
+func validateEmailBasic(email string) error {
+	if len(email) == 0 {
+		return ErrEmailInvalid{email}
+	}
+
+	if !emailRegexp.MatchString(email) {
+		return ErrEmailCharIsNotSupported{email}
+	}
+
+	if email[0] == '-' {
+		return ErrEmailInvalid{email}
+	}
+
+	if _, err := mail.ParseAddress(email); err != nil {
+		return ErrEmailInvalid{email}
+	}
+
+	return nil
+}
+
+// validateEmailDomain checks whether the email domain is allowed or blocked
+func validateEmailDomain(email string) error {
+	// if there is no allow list, then check email against block list
+	if len(setting.Service.EmailDomainAllowList) == 0 &&
+		validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
+		return ErrEmailInvalid{email}
+	}
+
+	// if there is an allow list, then check email against allow list
+	if len(setting.Service.EmailDomainAllowList) > 0 &&
+		!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
+		return ErrEmailInvalid{email}
+	}
+
+	return nil
+}
diff --git a/models/user/user.go b/models/user/user.go
index 2e1d6af176..0bdda8655f 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -586,6 +586,16 @@ type CreateUserOverwriteOptions struct {
 
 // CreateUser creates record of a new user.
 func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
+	return createUser(ctx, u, false, overwriteDefault...)
+}
+
+// AdminCreateUser is used by admins to manually create users
+func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
+	return createUser(ctx, u, true, overwriteDefault...)
+}
+
+// createUser creates record of a new user.
+func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
 	if err = IsUsableUsername(u.Name); err != nil {
 		return err
 	}
@@ -639,8 +649,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 		return err
 	}
 
-	if err := ValidateEmail(u.Email); err != nil {
-		return err
+	if createdByAdmin {
+		if err := ValidateEmailForAdmin(u.Email); err != nil {
+			return err
+		}
+	} else {
+		if err := ValidateEmail(u.Email); err != nil {
+			return err
+		}
 	}
 
 	ctx, committer, err := db.TxContext(ctx)
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 64315108b0..7f4200f684 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -133,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
 		u.UpdatedUnix = u.CreatedUnix
 	}
 
-	if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
+	if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
 		if user_model.IsErrUserAlreadyExist(err) ||
 			user_model.IsErrEmailAlreadyUsed(err) ||
 			db.IsErrNameReserved(err) ||
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index a34e0d0f0d..dfb103c8ab 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) {
 		u.MustChangePassword = form.MustChangePassword
 	}
 
-	if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
+	if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
 		switch {
 		case user_model.IsErrUserAlreadyExist(err):
 			ctx.Data["Err_UserName"] = true
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
index 0748a75ba4..53bdd11afd 100644
--- a/tests/integration/api_admin_test.go
+++ b/tests/integration/api_admin_test.go
@@ -14,9 +14,11 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/gobwas/glob"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -333,3 +335,27 @@ func TestAPICron(t *testing.T) {
 		}
 	})
 }
+
+func TestAPICreateUser_NotAllowedEmailDomain(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
+	defer func() {
+		setting.Service.EmailDomainAllowList = []glob.Glob{}
+	}()
+
+	adminUsername := "user1"
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
+
+	req := NewRequestWithValues(t, "POST", "/api/v1/admin/users", map[string]string{
+		"email":                "allowedUser1@example1.org",
+		"login_name":           "allowedUser1",
+		"username":             "allowedUser1",
+		"password":             "allowedUser1_pass",
+		"must_change_password": "true",
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusCreated)
+
+	req = NewRequest(t, "DELETE", "/api/v1/admin/users/allowedUser1").AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusNoContent)
+}

From f14779592494d41b3ab04caaab53487f2f4ede5a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 5 Mar 2024 15:21:52 +0100
Subject: [PATCH 279/679] Fix contributor graphs mobile layout and
 responsiveness (#29597)

Also removed a unneeded and actually conflicting class name
`stats-table`.

Fixes: https://github.com/go-gitea/gitea/issues/29192

<img width="445" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/787804ed-6ba4-437f-b314-f23cbe2edf7a">
---
 web_src/js/components/RepoContributors.vue | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 84fdcae1f6..22c247ae32 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -303,7 +303,7 @@ export default {
 </script>
 <template>
   <div>
-    <h2 class="ui header gt-df gt-ac gt-sb">
+    <div class="ui header gt-df gt-ac gt-sb">
       <div>
         <relative-time
           v-if="xAxisMin > 0"
@@ -334,7 +334,7 @@ export default {
         <div class="ui dropdown jump" id="repo-contributors">
           <div class="ui basic compact button">
             <span class="text">
-              {{ locale.filterLabel }} <strong>{{ locale.contributionType[type] }}</strong>
+              <span class="not-mobile">{{ locale.filterLabel }}&nbsp;</span><strong>{{ locale.contributionType[type] }}</strong>
               <svg-icon name="octicon-triangle-down" :size="14"/>
             </span>
           </div>
@@ -351,7 +351,7 @@ export default {
           </div>
         </div>
       </div>
-    </h2>
+    </div>
     <div class="gt-df ui segment main-graph">
       <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
         <div v-if="isLoading">
@@ -370,7 +370,8 @@ export default {
     </div>
     <div class="contributor-grid">
       <div
-        v-for="(contributor, index) in sortedContributors" :key="index" class="stats-table"
+        v-for="(contributor, index) in sortedContributors"
+        :key="index"
         v-memo="[sortedContributors, type]"
       >
         <div class="ui top attached header gt-df gt-f1">
@@ -406,13 +407,25 @@ export default {
 <style scoped>
 .main-graph {
   height: 260px;
+  padding-top: 2px;
 }
+
 .contributor-grid {
   display: grid;
   grid-template-columns: repeat(2, 1fr);
   gap: 1rem;
 }
 
+.contributor-grid > * {
+  min-width: 0;
+}
+
+@media (max-width: 991.98px) {
+  .contributor-grid {
+    grid-template-columns: repeat(1, 1fr);
+  }
+}
+
 .contributor-name {
   margin-bottom: 0;
 }

From 3f3335ae51d89520acce6573e870590423248c5c Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 5 Mar 2024 23:47:07 +0900
Subject: [PATCH 280/679] Add empty repo check in `DetectAndHandleSchedules`
 (#29606)

![image](https://github.com/go-gitea/gitea/assets/18380374/e6081301-bd3e-4cf6-ba4e-e574348dffb4)
---
 services/actions/notifier_helper.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index b248af1d01..b0d848b5ad 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -486,6 +486,10 @@ func handleSchedules(
 
 // DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
 func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
+	if repo.IsEmpty {
+		return nil
+	}
+
 	gitRepo, err := gitrepo.OpenRepository(context.Background(), repo)
 	if err != nil {
 		return fmt.Errorf("git.OpenRepository: %w", err)

From ebff37ae09e99126668cbee5804d45f069a42081 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 5 Mar 2024 23:13:35 +0800
Subject: [PATCH 281/679] Improve natural sort (#29611)

Hugely simplify the code, and add more tests (only new approach could
pass)
---
 modules/base/natural_sort.go      | 81 ++-----------------------------
 modules/base/natural_sort_test.go |  9 +++-
 2 files changed, 12 insertions(+), 78 deletions(-)

diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go
index e920177f89..0f90ec70ce 100644
--- a/modules/base/natural_sort.go
+++ b/modules/base/natural_sort.go
@@ -4,85 +4,12 @@
 package base
 
 import (
-	"math/big"
-	"unicode/utf8"
+	"golang.org/x/text/collate"
+	"golang.org/x/text/language"
 )
 
 // NaturalSortLess compares two strings so that they could be sorted in natural order
 func NaturalSortLess(s1, s2 string) bool {
-	var i1, i2 int
-	for {
-		rune1, j1, end1 := getNextRune(s1, i1)
-		rune2, j2, end2 := getNextRune(s2, i2)
-		if end1 || end2 {
-			return end1 != end2 && end1
-		}
-		dec1 := isDecimal(rune1)
-		dec2 := isDecimal(rune2)
-		var less, equal bool
-		if dec1 && dec2 {
-			i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
-		} else if !dec1 && !dec2 {
-			equal = rune1 == rune2
-			less = rune1 < rune2
-			i1 = j1
-			i2 = j2
-		} else {
-			return rune1 < rune2
-		}
-		if !equal {
-			return less
-		}
-	}
-}
-
-func getNextRune(str string, pos int) (rune, int, bool) {
-	if pos < len(str) {
-		r, w := utf8.DecodeRuneInString(str[pos:])
-		// Fallback to ascii
-		if r == utf8.RuneError {
-			r = rune(str[pos])
-			w = 1
-		}
-		return r, pos + w, false
-	}
-	return 0, pos, true
-}
-
-func isDecimal(r rune) bool {
-	return '0' <= r && r <= '9'
-}
-
-func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
-	d1, d2 := true, true
-	var dec1, dec2 string
-	for d1 || d2 {
-		if d1 {
-			r, j, end := getNextRune(str1, pos1)
-			if !end && isDecimal(r) {
-				dec1 += string(r)
-				pos1 = j
-			} else {
-				d1 = false
-			}
-		}
-		if d2 {
-			r, j, end := getNextRune(str2, pos2)
-			if !end && isDecimal(r) {
-				dec2 += string(r)
-				pos2 = j
-			} else {
-				d2 = false
-			}
-		}
-	}
-	less, equal = compareBigNumbers(dec1, dec2)
-	return pos1, pos2, less, equal
-}
-
-func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
-	d1, _ := big.NewInt(0).SetString(dec1, 10)
-	d2, _ := big.NewInt(0).SetString(dec2, 10)
-	cmp := d1.Cmp(d2)
-	return cmp < 0, cmp == 0
+	c := collate.New(language.English, collate.Numeric)
+	return c.CompareString(s1, s2) < 0
 }
diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go
index 91e864ad2a..f27a4eb53a 100644
--- a/modules/base/natural_sort_test.go
+++ b/modules/base/natural_sort_test.go
@@ -11,7 +11,7 @@ import (
 
 func TestNaturalSortLess(t *testing.T) {
 	test := func(s1, s2 string, less bool) {
-		assert.Equal(t, less, NaturalSortLess(s1, s2))
+		assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
 	}
 	test("v1.20.0", "v1.2.0", false)
 	test("v1.20.0", "v1.29.0", true)
@@ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) {
 	test("a-1-a", "a-1-b", true)
 	test("2", "12", true)
 	test("a", "ab", true)
+
+	test("A", "b", true)
+	test("a", "B", true)
+
+	test("cafe", "café", true)
+	test("café", "cafe", false)
+	test("caff", "café", false)
 }

From 136dd99e86eea9c8bfe61b972a12b395655171e8 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Wed, 6 Mar 2024 00:51:56 +0800
Subject: [PATCH 282/679] Skip email domain check when admins edit user emails
 (#29609)

Follow #29522

Administrators should be able to set a user's email address even if the
email address is not in `EMAIL_DOMAIN_ALLOWLIST`
---
 models/user/email_address.go        |  2 +-
 routers/api/v1/admin/user.go        |  2 +-
 routers/web/admin/users.go          |  2 +-
 services/user/email.go              |  5 +++--
 services/user/email_test.go         | 22 ++++++++++++++++++----
 tests/integration/api_admin_test.go | 29 +++++++++++++++++++++++++++++
 6 files changed, 53 insertions(+), 9 deletions(-)

diff --git a/models/user/email_address.go b/models/user/email_address.go
index 3cb2e8268c..11700a0129 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -162,7 +162,7 @@ func ValidateEmail(email string) error {
 	return validateEmailDomain(email)
 }
 
-// ValidateEmailForAdmin check if email is a valid address when admins manually add users
+// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users
 func ValidateEmailForAdmin(email string) error {
 	return validateEmailBasic(email)
 	// In this case we do not need to check the email domain
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 7f4200f684..986305d423 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -209,7 +209,7 @@ func EditUser(ctx *context.APIContext) {
 	}
 
 	if form.Email != nil {
-		if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
+		if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
 			switch {
 			case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
 				ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index dfb103c8ab..671a0d8885 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -412,7 +412,7 @@ func EditUserPost(ctx *context.Context) {
 	}
 
 	if form.Email != "" {
-		if err := user_service.AddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
+		if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
 			switch {
 			case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
 				ctx.Data["Err_Email"] = true
diff --git a/services/user/email.go b/services/user/email.go
index 07e19bc688..5c0de708e9 100644
--- a/services/user/email.go
+++ b/services/user/email.go
@@ -14,12 +14,13 @@ import (
 	"code.gitea.io/gitea/modules/util"
 )
 
-func AddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
+// AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address
+func AdminAddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
 	if strings.EqualFold(u.Email, emailStr) {
 		return nil
 	}
 
-	if err := user_model.ValidateEmail(emailStr); err != nil {
+	if err := user_model.ValidateEmailForAdmin(emailStr); err != nil {
 		return err
 	}
 
diff --git a/services/user/email_test.go b/services/user/email_test.go
index 8f419b69f9..b40f86b6a6 100644
--- a/services/user/email_test.go
+++ b/services/user/email_test.go
@@ -10,11 +10,13 @@ import (
 	organization_model "code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
 
+	"github.com/gobwas/glob"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
+func TestAdminAddOrSetPrimaryEmailAddress(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 27})
@@ -28,7 +30,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
 	assert.NotEqual(t, "new-primary@example.com", primary.Email)
 	assert.Equal(t, user.Email, primary.Email)
 
-	assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com"))
+	assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com"))
 
 	primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
 	assert.NoError(t, err)
@@ -39,7 +41,19 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, emails, 2)
 
-	assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com"))
+	setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
+	defer func() {
+		setting.Service.EmailDomainAllowList = []glob.Glob{}
+	}()
+
+	assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary2@example2.com"))
+
+	primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+	assert.NoError(t, err)
+	assert.Equal(t, "new-primary2@example2.com", primary.Email)
+	assert.Equal(t, user.Email, primary.Email)
+
+	assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com"))
 
 	primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
 	assert.NoError(t, err)
@@ -48,7 +62,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
 
 	emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID)
 	assert.NoError(t, err)
-	assert.Len(t, emails, 2)
+	assert.Len(t, emails, 3)
 }
 
 func TestReplacePrimaryEmailAddress(t *testing.T) {
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
index 53bdd11afd..8a330a68e2 100644
--- a/tests/integration/api_admin_test.go
+++ b/tests/integration/api_admin_test.go
@@ -359,3 +359,32 @@ func TestAPICreateUser_NotAllowedEmailDomain(t *testing.T) {
 	req = NewRequest(t, "DELETE", "/api/v1/admin/users/allowedUser1").AddTokenAuth(token)
 	MakeRequest(t, req, http.StatusNoContent)
 }
+
+func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
+	defer func() {
+		setting.Service.EmailDomainAllowList = []glob.Glob{}
+	}()
+
+	adminUsername := "user1"
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
+	urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2")
+
+	newEmail := "user2@example1.com"
+	req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
+		LoginName: "user2",
+		SourceID:  0,
+		Email:     &newEmail,
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusOK)
+
+	originalEmail := "user2@example.com"
+	req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
+		LoginName: "user2",
+		SourceID:  0,
+		Email:     &originalEmail,
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusOK)
+}

From 368743baf3d904f86b553a88718583906f571c87 Mon Sep 17 00:00:00 2001
From: ChristopherHX <christopher.homberger@web.de>
Date: Tue, 5 Mar 2024 18:34:42 +0100
Subject: [PATCH 283/679] Add ac claim for old docker/build-push-action@v3 /
 current buildx gha cache (#29584)

Also resolves a warning for current releases

```
| ##[group]GitHub Actions runtime token ACs
| ##[warning]Cannot parse GitHub Actions Runtime Token ACs: "undefined" is not valid JSON
| ##[endgroup]
====>
| ##[group]GitHub Actions runtime token ACs
| ##[endgroup]
```
\* this is an error in v3

References in the docker org:
-
https://github.com/docker/build-push-action/blob/831ca179d3cf91cf0c90ca465a408fa61e2129a2/src/main.ts#L24
-
https://github.com/docker/actions-toolkit/blob/7d8b4dc6694df35a06fae786427672ce27a8c18d/src/github.ts#L61

No known official action of GitHub makes use of this claim.

Current releases throw an error when configure to use actions cache
```
| ERROR: failed to solve: failed to configure gha cache exporter: invalid token without access controls
| ##[error]buildx failed with: ERROR: failed to solve: failed to configure gha cache exporter: invalid token without access controls
```
---
 services/actions/auth.go      | 25 +++++++++++++++++++++++++
 services/actions/auth_test.go |  9 +++++++++
 2 files changed, 34 insertions(+)

diff --git a/services/actions/auth.go b/services/actions/auth.go
index e0f9a9015d..8e934d89a8 100644
--- a/services/actions/auth.go
+++ b/services/actions/auth.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 
@@ -21,17 +22,41 @@ type actionsClaims struct {
 	TaskID int64
 	RunID  int64
 	JobID  int64
+	Ac     string `json:"ac"`
 }
 
+type actionsCacheScope struct {
+	Scope      string
+	Permission actionsCachePermission
+}
+
+type actionsCachePermission int
+
+const (
+	actionsCachePermissionRead = 1 << iota
+	actionsCachePermissionWrite
+)
+
 func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
 	now := time.Now()
 
+	ac, err := json.Marshal(&[]actionsCacheScope{
+		{
+			Scope:      "",
+			Permission: actionsCachePermissionWrite,
+		},
+	})
+	if err != nil {
+		return "", err
+	}
+
 	claims := actionsClaims{
 		RegisteredClaims: jwt.RegisteredClaims{
 			ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
 			NotBefore: jwt.NewNumericDate(now),
 		},
 		Scp:    fmt.Sprintf("Actions.Results:%d:%d", runID, jobID),
+		Ac:     string(ac),
 		TaskID: taskID,
 		RunID:  runID,
 		JobID:  jobID,
diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go
index 1f62f17f52..f73ae8ae4c 100644
--- a/services/actions/auth_test.go
+++ b/services/actions/auth_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"testing"
 
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/golang-jwt/jwt/v5"
@@ -29,6 +30,14 @@ func TestCreateAuthorizationToken(t *testing.T) {
 	taskIDClaim, ok := claims["TaskID"]
 	assert.True(t, ok, "Has TaskID claim in jwt token")
 	assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one")
+	acClaim, ok := claims["ac"]
+	assert.True(t, ok, "Has ac claim in jwt token")
+	ac, ok := acClaim.(string)
+	assert.True(t, ok, "ac claim is a string for buildx gha cache")
+	scopes := []actionsCacheScope{}
+	err = json.Unmarshal([]byte(ac), &scopes)
+	assert.NoError(t, err, "ac claim is a json list for buildx gha cache")
+	assert.GreaterOrEqual(t, len(scopes), 1, "Expected at least one action cache scope for buildx gha cache")
 }
 
 func TestParseAuthorizationToken(t *testing.T) {

From 06039bf0b7ec4dffe74ae323b8bbbbedec69d0c8 Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Tue, 5 Mar 2024 20:35:29 -0500
Subject: [PATCH 284/679] bump protobuf module (#29617)

---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 03f6ad1215..d58890de28 100644
--- a/go.mod
+++ b/go.mod
@@ -113,7 +113,7 @@ require (
 	golang.org/x/text v0.14.0
 	golang.org/x/tools v0.17.0
 	google.golang.org/grpc v1.60.1
-	google.golang.org/protobuf v1.32.0
+	google.golang.org/protobuf v1.33.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 	gopkg.in/ini.v1 v1.67.0
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index b3b8ad8ce4..87072571e5 100644
--- a/go.sum
+++ b/go.sum
@@ -1308,8 +1308,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

From c481dba52c00c331903689291cf946330be9fb3e Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 6 Mar 2024 02:48:14 +0100
Subject: [PATCH 285/679] Run editorconfig-checker on `locale_en-US.ini`
 (#29608)

Will prevent trailing whitespace etc being introduced in this file.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 .github/workflows/files-changed.yml | 1 +
 Makefile                            | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index c909f78597..f9b6b1ec49 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -48,6 +48,7 @@ jobs:
               - "Makefile"
               - ".golangci.yml"
               - ".editorconfig"
+              - "options/locale/locale_en-US.ini"
 
             frontend:
               - "**/*.js"
diff --git a/Makefile b/Makefile
index 9bbc56451b..52357ba00d 100644
--- a/Makefile
+++ b/Makefile
@@ -147,6 +147,7 @@ GO_DIRS := build cmd models modules routers services tests
 WEB_DIRS := web_src/js web_src/css
 
 SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
+EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
 
 GO_SOURCES := $(wildcard *.go)
 GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
@@ -426,7 +427,7 @@ lint-go-vet:
 
 .PHONY: lint-editorconfig
 lint-editorconfig:
-	$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .github/workflows
+	@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
 
 .PHONY: lint-actions
 lint-actions:

From 61619c84d0fc511f2532f8c82d98fe971da69447 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 6 Mar 2024 13:09:38 +0800
Subject: [PATCH 286/679] Fix 500 error when adding PR comment (#29622)

---
 routers/web/repo/pull_review.go | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 64212291e1..bce807aacd 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -10,6 +10,7 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	pull_model "code.gitea.io/gitea/models/pull"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
@@ -19,6 +20,7 @@ import (
 	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	pull_service "code.gitea.io/gitea/services/pull"
+	user_service "code.gitea.io/gitea/services/user"
 )
 
 const (
@@ -203,6 +205,10 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
 		return
 	}
 	ctx.Data["AfterCommitID"] = pullHeadCommitID
+	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+	}
+
 	if origin == "diff" {
 		ctx.HTML(http.StatusOK, tplDiffConversation)
 	} else if origin == "timeline" {

From 8d32f3cb745124a99a8948ae771bcbb68fa3f297 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 6 Mar 2024 06:21:39 +0100
Subject: [PATCH 287/679] Update Twitter Logo (#29621)

<img width="430" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/9cf7b0a3-406b-4dd6-ab3d-d31a96b9335a">
---
 public/assets/img/svg/gitea-twitter.svg | 2 +-
 web_src/svg/gitea-twitter.svg           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/assets/img/svg/gitea-twitter.svg b/public/assets/img/svg/gitea-twitter.svg
index 5d11c6eaec..5ed1e264ca 100644
--- a/public/assets/img/svg/gitea-twitter.svg
+++ b/public/assets/img/svg/gitea-twitter.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" aria-hidden="true" class="gitea-twitter__svg gitea-twitter__gitea-twitter svg gitea-twitter" clip-rule="evenodd" viewBox="-89.009 -46.884 643.937 446.884" width="16" height="16"><path fill="#1da1f2" fill-rule="nonzero" d="M154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052A205.3 205.3 0 0 0 492 47.346c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.4 202.4 0 0 1-64.098 24.511C395.903 12.276 369.679 0 340.641 0c-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504A100.74 100.74 0 0 0 20.57 69.24c0 35.034 17.82 65.961 44.92 84.055a100.2 100.2 0 0 1-45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101 101 0 0 1-26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224A205 205 0 0 1 0 354.634c44.674 28.645 97.72 45.359 154.734 45.359"/></svg>
\ No newline at end of file
+<svg viewBox="0 0 24 24" class="svg gitea-twitter" xmlns="http://www.w3.org/2000/svg" width="16" height="16" aria-hidden="true"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982z"/></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-twitter.svg b/web_src/svg/gitea-twitter.svg
index 096b9add2b..f972d23f90 100644
--- a/web_src/svg/gitea-twitter.svg
+++ b/web_src/svg/gitea-twitter.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="-89.009 -46.884 643.937 446.884" class="svg gitea-twitter" width="16" height="16" aria-hidden="true"><path fill="#1da1f2" fill-rule="nonzero" d="M154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052A205.304 205.304 0 0 0 492 47.346c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.42 202.42 0 0 1-64.098 24.511C395.903 12.276 369.679 0 340.641 0c-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504A100.739 100.739 0 0 0 20.57 69.24c0 35.034 17.82 65.961 44.92 84.055a100.172 100.172 0 0 1-45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101.036 101.036 0 0 1-26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224A204.9 204.9 0 0 1 0 354.634c44.674 28.645 97.72 45.359 154.734 45.359"/></svg>
\ No newline at end of file
+<svg viewBox="0 0 24 24"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23l-8.905-12.684zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982l-5.926-8.3z"/></svg>
\ No newline at end of file

From da15d6127c8d430dfc069f9815ce783dd9ca35f7 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 6 Mar 2024 13:52:48 +0800
Subject: [PATCH 288/679] A small refactor for agit implementation (#29614)

This PR made the code simpler, reduced unnecessary database queries and
fixed some warnning for the errors.New .

---------

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 services/agit/agit.go | 73 +++++++++++++++++++------------------------
 1 file changed, 33 insertions(+), 40 deletions(-)

diff --git a/services/agit/agit.go b/services/agit/agit.go
index 2233fe8547..eb3bafa906 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
 	"strings"
 
 	issues_model "code.gitea.io/gitea/models/issues"
@@ -21,26 +22,17 @@ import (
 
 // ProcReceive handle proc receive work
 func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
-	// TODO: Add more options?
-	var (
-		topicBranch string
-		title       string
-		description string
-		forcePush   bool
-	)
-
 	results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
-
-	ownerName := repo.OwnerName
-	repoName := repo.Name
-
-	topicBranch = opts.GitPushOptions["topic"]
-	_, forcePush = opts.GitPushOptions["force-push"]
+	topicBranch := opts.GitPushOptions["topic"]
+	forcePush, _ := strconv.ParseBool(opts.GitPushOptions["force-push"])
+	title := strings.TrimSpace(opts.GitPushOptions["title"])
+	description := strings.TrimSpace(opts.GitPushOptions["description"]) // TODO: Add more options?
 	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+	userName := strings.ToLower(opts.UserName)
 
 	pusher, err := user_model.GetUserByID(ctx, opts.UserID)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to get user. Error: %w", err)
+		return nil, fmt.Errorf("failed to get user. Error: %w", err)
 	}
 
 	for i := range opts.OldCommitIDs {
@@ -85,9 +77,6 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 			continue
 		}
 
-		var headBranch string
-		userName := strings.ToLower(opts.UserName)
-
 		if len(curentTopicBranch) == 0 {
 			curentTopicBranch = topicBranch
 		}
@@ -95,6 +84,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		// because different user maybe want to use same topic,
 		// So it's better to make sure the topic branch name
 		// has user name prefix
+		var headBranch string
 		if !strings.HasPrefix(curentTopicBranch, userName+"/") {
 			headBranch = userName + "/" + curentTopicBranch
 		} else {
@@ -104,21 +94,26 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
 		if err != nil {
 			if !issues_model.IsErrPullRequestNotExist(err) {
-				return nil, fmt.Errorf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %w", ownerName, repoName, err)
+				return nil, fmt.Errorf("failed to get unmerged agit flow pull request in repository: %s Error: %w", repo.FullName(), err)
+			}
+
+			var commit *git.Commit
+			if title == "" || description == "" {
+				commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i])
+				if err != nil {
+					return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err)
+				}
 			}
 
 			// create a new pull request
-			if len(title) == 0 {
-				var has bool
-				title, has = opts.GitPushOptions["title"]
-				if !has || len(title) == 0 {
-					commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i])
-					if err != nil {
-						return nil, fmt.Errorf("Failed to get commit %s in repository: %s/%s Error: %w", opts.NewCommitIDs[i], ownerName, repoName, err)
-					}
-					title = strings.Split(commit.CommitMessage, "\n")[0]
-				}
-				description = opts.GitPushOptions["description"]
+			if title == "" {
+				title = strings.Split(commit.CommitMessage, "\n")[0]
+			}
+			if description == "" {
+				_, description, _ = strings.Cut(commit.CommitMessage, "\n\n")
+			}
+			if description == "" {
+				description = title
 			}
 
 			prIssue := &issues_model.Issue{
@@ -160,12 +155,12 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 
 		// update exist pull request
 		if err := pr.LoadBaseRepo(ctx); err != nil {
-			return nil, fmt.Errorf("Unable to load base repository for PR[%d] Error: %w", pr.ID, err)
+			return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err)
 		}
 
 		oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
 		if err != nil {
-			return nil, fmt.Errorf("Unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
+			return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
 		}
 
 		if oldCommitID == opts.NewCommitIDs[i] {
@@ -179,9 +174,11 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		}
 
 		if !forcePush {
-			output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
+			output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").
+				AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).
+				RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
 			if err != nil {
-				return nil, fmt.Errorf("Fail to detect force push: %w", err)
+				return nil, fmt.Errorf("failed to detect force push: %w", err)
 			} else if len(output) > 0 {
 				results = append(results, private.HookProcReceiveRefResult{
 					OriginalRef: opts.RefFullNames[i],
@@ -195,17 +192,13 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 
 		pr.HeadCommitID = opts.NewCommitIDs[i]
 		if err = pull_service.UpdateRef(ctx, pr); err != nil {
-			return nil, fmt.Errorf("Failed to update pull ref. Error: %w", err)
+			return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
 		}
 
 		pull_service.AddToTaskQueue(ctx, pr)
-		pusher, err := user_model.GetUserByID(ctx, opts.UserID)
-		if err != nil {
-			return nil, fmt.Errorf("Failed to get user. Error: %w", err)
-		}
 		err = pr.LoadIssue(ctx)
 		if err != nil {
-			return nil, fmt.Errorf("Failed to load pull issue. Error: %w", err)
+			return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)
 		}
 		comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
 		if err == nil && comment != nil {

From 5cddab4f74bbb307ddf13e458c7ac22f93b9283a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 6 Mar 2024 14:26:32 +0800
Subject: [PATCH 289/679] Make wiki default branch name changable (#29603)

Fix #29000
Fix #28685
Fix #18568

Related: #27497

And by the way fix #24036, add a Cancel button there (one line)
---
 models/migrations/migrations.go      |  2 +
 models/migrations/v1_22/v289.go      | 18 ++++++
 models/repo/repo.go                  |  4 ++
 modules/git/repo_base_gogit.go       |  4 +-
 modules/git/repo_base_nogogit.go     |  4 +-
 options/locale/locale_en-US.ini      |  2 +
 routers/web/repo/setting/setting.go  |  7 +++
 routers/web/repo/wiki.go             | 60 +++++++++++--------
 routers/web/repo/wiki_test.go        | 30 ++++++++++
 services/forms/repo_form.go          |  1 +
 services/repository/create.go        |  2 +
 services/repository/migrate.go       | 88 ++++++++++++++++++----------
 services/wiki/wiki.go                | 79 ++++++++++++++++++-------
 services/wiki/wiki_test.go           | 10 ++--
 templates/repo/settings/options.tmpl |  4 ++
 templates/repo/wiki/new.tmpl         |  5 +-
 templates/repo/wiki/pages.tmpl       |  1 +
 17 files changed, 232 insertions(+), 89 deletions(-)
 create mode 100644 models/migrations/v1_22/v289.go

diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 9d288ec2bd..d40866f3e9 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -562,6 +562,8 @@ var migrations = []Migration{
 	NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
 	// v288 -> v289
 	NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
+	// v289 -> v290
+	NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go
new file mode 100644
index 0000000000..e2dfc48715
--- /dev/null
+++ b/models/migrations/v1_22/v289.go
@@ -0,0 +1,18 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import "xorm.io/xorm"
+
+func AddDefaultWikiBranch(x *xorm.Engine) error {
+	type Repository struct {
+		ID                int64
+		DefaultWikiBranch string
+	}
+	if err := x.Sync(&Repository{}); err != nil {
+		return err
+	}
+	_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")
+	return err
+}
diff --git a/models/repo/repo.go b/models/repo/repo.go
index f6758f1591..1d17e565ae 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -136,6 +136,7 @@ type Repository struct {
 	OriginalServiceType api.GitServiceType `xorm:"index"`
 	OriginalURL         string             `xorm:"VARCHAR(2048)"`
 	DefaultBranch       string
+	DefaultWikiBranch   string
 
 	NumWatches          int
 	NumStars            int
@@ -285,6 +286,9 @@ func (repo *Repository) AfterLoad() {
 	repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
 	repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
 	repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
+	if repo.DefaultWikiBranch == "" {
+		repo.DefaultWikiBranch = setting.Repository.DefaultBranch
+	}
 }
 
 // LoadAttributes loads attributes of the repository.
diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go
index 3ca5eb36c6..0cd07dcdc8 100644
--- a/modules/git/repo_base_gogit.go
+++ b/modules/git/repo_base_gogit.go
@@ -8,11 +8,11 @@ package git
 
 import (
 	"context"
-	"errors"
 	"path/filepath"
 
 	gitealog "code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/osfs"
@@ -52,7 +52,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 	if err != nil {
 		return nil, err
 	} else if !isDir(repoPath) {
-		return nil, errors.New("no such file or directory")
+		return nil, util.NewNotExistErrorf("no such file or directory")
 	}
 
 	fs := osfs.New(repoPath)
diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
index 86b6a93567..7f6512200b 100644
--- a/modules/git/repo_base_nogogit.go
+++ b/modules/git/repo_base_nogogit.go
@@ -9,10 +9,10 @@ package git
 import (
 	"bufio"
 	"context"
-	"errors"
 	"path/filepath"
 
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 )
 
 func init() {
@@ -54,7 +54,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 	if err != nil {
 		return nil, err
 	} else if !isDir(repoPath) {
-		return nil, errors.New("no such file or directory")
+		return nil, util.NewNotExistErrorf("no such file or directory")
 	}
 
 	// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 3fc74959ca..59aae68cae 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2092,6 +2092,8 @@ settings.branches.add_new_rule = Add New Rule
 settings.advanced_settings = Advanced Settings
 settings.wiki_desc = Enable Repository Wiki
 settings.use_internal_wiki = Use Built-In Wiki
+settings.default_wiki_branch_name = Default Wiki Branch Name
+settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch.
 settings.use_external_wiki = Use External Wiki
 settings.external_wiki_url = External Wiki URL
 settings.external_wiki_url_error = The external wiki URL is not a valid URL.
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 992a980d9e..e045e3b8dc 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -488,6 +488,13 @@ func SettingsPost(ctx *context.Context) {
 			}
 		}
 
+		if form.DefaultWikiBranch != "" {
+			if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
+				log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
+				ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
+			}
+		}
+
 		if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
 			if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
 				ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 91cf727e2c..88b63da88d 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -93,17 +93,32 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
 }
 
 func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
-	wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
-	if err != nil {
-		ctx.ServerError("OpenRepository", err)
-		return nil, nil, err
+	wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+	if errGitRepo != nil {
+		ctx.ServerError("OpenRepository", errGitRepo)
+		return nil, nil, errGitRepo
 	}
 
-	commit, err := wikiRepo.GetBranchCommit(wiki_service.DefaultBranch)
-	if err != nil {
-		return wikiRepo, nil, err
+	commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
+	if git.IsErrNotExist(errCommit) {
+		// if the default branch recorded in database is out of sync, then re-sync it
+		gitRepoDefaultBranch, errBranch := wikiGitRepo.GetDefaultBranch()
+		if errBranch != nil {
+			return wikiGitRepo, nil, errBranch
+		}
+		// update the default branch in the database
+		errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
+		if errDb != nil {
+			return wikiGitRepo, nil, errDb
+		}
+		ctx.Repo.Repository.DefaultWikiBranch = gitRepoDefaultBranch
+		// retry to get the commit from the correct default branch
+		commit, errCommit = wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
 	}
-	return wikiRepo, commit, nil
+	if errCommit != nil {
+		return wikiGitRepo, nil, errCommit
+	}
+	return wikiGitRepo, commit, nil
 }
 
 // wikiContentsByEntry returns the contents of the wiki page referenced by the
@@ -316,7 +331,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 	}
 
 	// get commit count - wiki revisions
-	commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename)
+	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 	ctx.Data["CommitCount"] = commitsCount
 
 	return wikiRepo, entry
@@ -368,7 +383,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
 	ctx.Data["footerContent"] = ""
 
 	// get commit count - wiki revisions
-	commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename)
+	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 	ctx.Data["CommitCount"] = commitsCount
 
 	// get page
@@ -380,7 +395,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
 	// get Commit Count
 	commitsHistory, err := wikiRepo.CommitsByFileAndRange(
 		git.CommitsByFileAndRangeOptions{
-			Revision: wiki_service.DefaultBranch,
+			Revision: ctx.Repo.Repository.DefaultWikiBranch,
 			File:     pageFilename,
 			Page:     page,
 		})
@@ -402,20 +417,17 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
 
 func renderEditPage(ctx *context.Context) {
 	wikiRepo, commit, err := findWikiRepoCommit(ctx)
-	if err != nil {
+	defer func() {
 		if wikiRepo != nil {
-			wikiRepo.Close()
+			_ = wikiRepo.Close()
 		}
+	}()
+	if err != nil {
 		if !git.IsErrNotExist(err) {
 			ctx.ServerError("GetBranchCommit", err)
 		}
 		return
 	}
-	defer func() {
-		if wikiRepo != nil {
-			wikiRepo.Close()
-		}
-	}()
 
 	// get requested pagename
 	pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
@@ -584,17 +596,15 @@ func WikiPages(ctx *context.Context) {
 	ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
 
 	wikiRepo, commit, err := findWikiRepoCommit(ctx)
-	if err != nil {
-		if wikiRepo != nil {
-			wikiRepo.Close()
-		}
-		return
-	}
 	defer func() {
 		if wikiRepo != nil {
-			wikiRepo.Close()
+			_ = wikiRepo.Close()
 		}
 	}()
+	if err != nil {
+		ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
+		return
+	}
 
 	entries, err := commit.ListEntries()
 	if err != nil {
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 719cca3049..52e216e6a0 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -9,6 +9,7 @@ import (
 	"net/url"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/git"
@@ -221,3 +222,32 @@ func TestWikiRaw(t *testing.T) {
 		}
 	}
 }
+
+func TestDefaultWikiBranch(t *testing.T) {
+	unittest.PrepareTestEnv(t)
+
+	assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
+
+	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
+	ctx.SetParams("*", "Home")
+	contexttest.LoadRepo(t, ctx, 1)
+	assert.Equal(t, "wrong-branch", ctx.Repo.Repository.DefaultWikiBranch)
+	Wiki(ctx) // after the visiting, the out-of-sync database record will update the branch name to "master"
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "master", ctx.Repo.Repository.DefaultWikiBranch)
+
+	// invalid branch name should fail
+	assert.Error(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "the bad name"))
+	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "master", repo.DefaultWikiBranch)
+
+	// the same branch name, should succeed (actually a no-op)
+	assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "master"))
+	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "master", repo.DefaultWikiBranch)
+
+	// change to another name
+	assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "main"))
+	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "main", repo.DefaultWikiBranch)
+}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 8c3e458d2f..e45a2a1695 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -133,6 +133,7 @@ type RepoSettingForm struct {
 	EnableCode                            bool
 	EnableWiki                            bool
 	EnableExternalWiki                    bool
+	DefaultWikiBranch                     string
 	ExternalWikiURL                       string
 	EnableIssues                          bool
 	EnableExternalTracker                 bool
diff --git a/services/repository/create.go b/services/repository/create.go
index c47ce9c413..8d8c39197d 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -173,6 +173,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re
 	}
 
 	repo.DefaultBranch = setting.Repository.DefaultBranch
+	repo.DefaultWikiBranch = setting.Repository.DefaultBranch
 
 	if len(opts.DefaultBranch) > 0 {
 		repo.DefaultBranch = opts.DefaultBranch
@@ -240,6 +241,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
 		TrustModel:                      opts.TrustModel,
 		IsMirror:                        opts.IsMirror,
 		DefaultBranch:                   opts.DefaultBranch,
+		DefaultWikiBranch:               setting.Repository.DefaultBranch,
 		ObjectFormatName:                opts.ObjectFormatName,
 	}
 
diff --git a/services/repository/migrate.go b/services/repository/migrate.go
index 51fdd90f54..aae2ddc120 100644
--- a/services/repository/migrate.go
+++ b/services/repository/migrate.go
@@ -25,6 +25,54 @@ import (
 	"code.gitea.io/gitea/modules/util"
 )
 
+func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) {
+	wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
+	wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
+	if wikiRemotePath == "" {
+		return "", nil
+	}
+
+	if err := util.RemoveAll(wikiPath); err != nil {
+		return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", wikiPath, err)
+	}
+
+	cleanIncompleteWikiPath := func() {
+		if err := util.RemoveAll(wikiPath); err != nil {
+			log.Error("Failed to remove incomplete wiki dir %q, err: %v", wikiPath, err)
+		}
+	}
+	if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
+		Mirror:        true,
+		Quiet:         true,
+		Timeout:       migrateTimeout,
+		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
+	}); err != nil {
+		log.Error("Clone wiki failed, err: %v", err)
+		cleanIncompleteWikiPath()
+		return "", err
+	}
+
+	if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
+		cleanIncompleteWikiPath()
+		return "", err
+	}
+
+	wikiRepo, err := git.OpenRepository(ctx, wikiPath)
+	if err != nil {
+		cleanIncompleteWikiPath()
+		return "", fmt.Errorf("failed to open wiki repo %q, err: %w", wikiPath, err)
+	}
+	defer wikiRepo.Close()
+
+	defaultBranch, err := wikiRepo.GetDefaultBranch()
+	if err != nil {
+		cleanIncompleteWikiPath()
+		return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err)
+	}
+
+	return defaultBranch, nil
+}
+
 // MigrateRepositoryGitData starts migrating git related data after created migrating repository
 func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
 	repo *repo_model.Repository, opts migration.MigrateOptions,
@@ -44,21 +92,20 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
 
 	migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
 
-	var err error
-	if err = util.RemoveAll(repoPath); err != nil {
-		return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err)
+	if err := util.RemoveAll(repoPath); err != nil {
+		return repo, fmt.Errorf("failed to remove existing repo dir %q, err: %w", repoPath, err)
 	}
 
-	if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
+	if err := git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
 		Mirror:        true,
 		Quiet:         true,
 		Timeout:       migrateTimeout,
 		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
 	}); err != nil {
 		if errors.Is(err, context.DeadlineExceeded) {
-			return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err)
+			return repo, fmt.Errorf("clone timed out, consider increasing [git.timeout] MIGRATE in app.ini, underlying err: %w", err)
 		}
-		return repo, fmt.Errorf("Clone: %w", err)
+		return repo, fmt.Errorf("clone error: %w", err)
 	}
 
 	if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
@@ -66,37 +113,18 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
 	}
 
 	if opts.Wiki {
-		wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
-		wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
-		if len(wikiRemotePath) > 0 {
-			if err := util.RemoveAll(wikiPath); err != nil {
-				return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
-			}
-
-			if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
-				Mirror:        true,
-				Quiet:         true,
-				Timeout:       migrateTimeout,
-				Branch:        "master",
-				SkipTLSVerify: setting.Migrations.SkipTLSVerify,
-			}); err != nil {
-				log.Warn("Clone wiki: %v", err)
-				if err := util.RemoveAll(wikiPath); err != nil {
-					return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
-				}
-			} else {
-				if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
-					return repo, err
-				}
-			}
+		defaultWikiBranch, err := cloneWiki(ctx, u, opts, migrateTimeout)
+		if err != nil {
+			return repo, fmt.Errorf("clone wiki error: %w", err)
 		}
+		repo.DefaultWikiBranch = defaultWikiBranch
 	}
 
 	if repo.OwnerID == u.ID {
 		repo.Owner = u
 	}
 
-	if err = repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
+	if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
 		return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
 	}
 
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 50d52d3140..6f1ca120b0 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -6,18 +6,22 @@ package wiki
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"strings"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/sync"
+	"code.gitea.io/gitea/modules/util"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
@@ -25,10 +29,7 @@ import (
 // TODO: use clustered lock (unique queue? or *abuse* cache)
 var wikiWorkingPool = sync.NewExclusivePool()
 
-const (
-	DefaultRemote = "origin"
-	DefaultBranch = "master"
-)
+const DefaultRemote = "origin"
 
 // InitWiki initializes a wiki for repository,
 // it does nothing when repository already has wiki.
@@ -41,25 +42,25 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
 		return fmt.Errorf("InitRepository: %w", err)
 	} else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
 		return fmt.Errorf("createDelegateHooks: %w", err)
-	} else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
-		return fmt.Errorf("unable to set default wiki branch to master: %w", err)
+	} else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + repo.DefaultWikiBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
+		return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err)
 	}
 	return nil
 }
 
 // prepareGitPath try to find a suitable file path with file name by the given raw wiki name.
 // return: existence, prepared file path with name, error
-func prepareGitPath(gitRepo *git.Repository, wikiPath WebPath) (bool, string, error) {
+func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
 	unescaped := string(wikiPath) + ".md"
 	gitPath := WebPathToGitPath(wikiPath)
 
 	// Look for both files
-	filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, gitPath)
+	filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath)
 	if err != nil {
-		if strings.Contains(err.Error(), "Not a valid object name master") {
-			return false, gitPath, nil
+		if strings.Contains(err.Error(), "Not a valid object name") {
+			return false, gitPath, nil // branch doesn't exist
 		}
-		log.Error("%v", err)
+		log.Error("Wiki LsTree failed, err: %v", err)
 		return false, gitPath, err
 	}
 
@@ -95,7 +96,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		return fmt.Errorf("InitWiki: %w", err)
 	}
 
-	hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), DefaultBranch)
+	hasDefaultBranch := git.IsBranchExist(ctx, repo.WikiPath(), repo.DefaultWikiBranch)
 
 	basePath, err := repo_module.CreateTemporaryPath("update-wiki")
 	if err != nil {
@@ -112,8 +113,8 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		Shared: true,
 	}
 
-	if hasMasterBranch {
-		cloneOpts.Branch = DefaultBranch
+	if hasDefaultBranch {
+		cloneOpts.Branch = repo.DefaultWikiBranch
 	}
 
 	if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil {
@@ -128,14 +129,14 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 	}
 	defer gitRepo.Close()
 
-	if hasMasterBranch {
+	if hasDefaultBranch {
 		if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
 			log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
 			return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err)
 		}
 	}
 
-	isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, newWikiName)
+	isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, newWikiName)
 	if err != nil {
 		return err
 	}
@@ -151,7 +152,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		isOldWikiExist := true
 		oldWikiPath := newWikiPath
 		if oldWikiName != newWikiName {
-			isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, oldWikiName)
+			isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.DefaultWikiBranch, oldWikiName)
 			if err != nil {
 				return err
 			}
@@ -200,7 +201,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 	} else {
 		commitTreeOpts.NoGPGSign = true
 	}
-	if hasMasterBranch {
+	if hasDefaultBranch {
 		commitTreeOpts.Parents = []string{"HEAD"}
 	}
 
@@ -212,7 +213,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 
 	if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
 		Remote: DefaultRemote,
-		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
+		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
 		Env: repo_module.FullPushingEnvironment(
 			doer,
 			doer,
@@ -269,7 +270,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 	if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{
 		Bare:   true,
 		Shared: true,
-		Branch: DefaultBranch,
+		Branch: repo.DefaultWikiBranch,
 	}); err != nil {
 		log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
 		return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
@@ -287,7 +288,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err)
 	}
 
-	found, wikiPath, err := prepareGitPath(gitRepo, wikiName)
+	found, wikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, wikiName)
 	if err != nil {
 		return err
 	}
@@ -331,7 +332,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 
 	if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
 		Remote: DefaultRemote,
-		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
+		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
 		Env: repo_module.FullPushingEnvironment(
 			doer,
 			doer,
@@ -358,3 +359,37 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
 	system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath())
 	return nil
 }
+
+func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, newBranch string) error {
+	if !git.IsValidRefPattern(newBranch) {
+		return fmt.Errorf("invalid branch name: %s", newBranch)
+	}
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		repo.DefaultWikiBranch = newBranch
+		if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_wiki_branch"); err != nil {
+			return fmt.Errorf("unable to update database: %w", err)
+		}
+
+		gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo)
+		if errors.Is(err, util.ErrNotExist) {
+			return nil // no git repo on storage, no need to do anything else
+		} else if err != nil {
+			return fmt.Errorf("unable to open repository: %w", err)
+		}
+		defer gitRepo.Close()
+
+		oldDefBranch, err := gitRepo.GetDefaultBranch()
+		if err != nil {
+			return fmt.Errorf("unable to get default branch: %w", err)
+		}
+		if oldDefBranch == newBranch {
+			return nil
+		}
+
+		err = gitRepo.RenameBranch(oldDefBranch, newBranch)
+		if err != nil {
+			return fmt.Errorf("unable to rename default branch: %w", err)
+		}
+		return nil
+	})
+}
diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go
index 59c77060f2..0a18cffa25 100644
--- a/services/wiki/wiki_test.go
+++ b/services/wiki/wiki_test.go
@@ -170,7 +170,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
 				return
 			}
 			defer gitRepo.Close()
-			masterTree, err := gitRepo.GetTree(DefaultBranch)
+			masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
 			assert.NoError(t, err)
 			gitPath := WebPathToGitPath(webPath)
 			entry, err := masterTree.GetTreeEntryByPath(gitPath)
@@ -215,7 +215,7 @@ func TestRepository_EditWikiPage(t *testing.T) {
 		// Now need to show that the page has been added:
 		gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo)
 		assert.NoError(t, err)
-		masterTree, err := gitRepo.GetTree(DefaultBranch)
+		masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
 		assert.NoError(t, err)
 		gitPath := WebPathToGitPath(webPath)
 		entry, err := masterTree.GetTreeEntryByPath(gitPath)
@@ -242,7 +242,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) {
 		return
 	}
 	defer gitRepo.Close()
-	masterTree, err := gitRepo.GetTree(DefaultBranch)
+	masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
 	assert.NoError(t, err)
 	gitPath := WebPathToGitPath("Home")
 	_, err = masterTree.GetTreeEntryByPath(gitPath)
@@ -280,7 +280,7 @@ func TestPrepareWikiFileName(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			webPath := UserTitleToWebPath("", tt.arg)
-			existence, newWikiPath, err := prepareGitPath(gitRepo, webPath)
+			existence, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, webPath)
 			if (err != nil) != tt.wantErr {
 				assert.NoError(t, err)
 				return
@@ -312,7 +312,7 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
 	}
 	defer gitRepo.Close()
 
-	existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
+	existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home")
 	assert.False(t, existence)
 	assert.NoError(t, err)
 	assert.EqualValues(t, "Home.md", newWikiPath)
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 0de42b34ea..a699396a84 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -335,6 +335,10 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
 						</div>
 					</div>
+					<div class="inline field gt-pl-4">
+						<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
+						<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
+					</div>
 					<div class="field">
 						<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
 							<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.UnitEnabled $.Context $.UnitTypeExternalWiki}}checked{{end}}>
diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl
index ff31df0c32..640f8ca9cd 100644
--- a/templates/repo/wiki/new.tmpl
+++ b/templates/repo/wiki/new.tmpl
@@ -36,9 +36,8 @@
 			</div>
 			<div class="divider"></div>
 			<div class="text right">
-				<button class="ui primary button">
-					{{ctx.Locale.Tr "repo.wiki.save_page"}}
-				</button>
+				<a class="ui basic cancel button" href="{{.Link}}">{{ctx.Locale.Tr "cancel"}}</a>
+				<button class="ui primary button">{{ctx.Locale.Tr "repo.wiki.save_page"}}</button>
 			</div>
 		</form>
 	</div>
diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl
index 22eb2619f9..cace0d48e0 100644
--- a/templates/repo/wiki/pages.tmpl
+++ b/templates/repo/wiki/pages.tmpl
@@ -10,6 +10,7 @@
 				{{end}}
 			</span>
 		</h2>
+		{{if .IsRepositoryAdmin}}<div>{{ctx.Locale.Tr "repo.default_branch"}}: {{.Repository.DefaultWikiBranch}}</div>{{end}}
 		<table class="ui table wiki-pages-list">
 			<tbody>
 				{{range .Pages}}

From a2b0fb1a64b0794b808a013089758a49f56d8915 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Wed, 6 Mar 2024 16:24:43 +0900
Subject: [PATCH 290/679] Fix wrong line number in code search result (#29260)

Fix #29136

Before: The result is a table and all line numbers are all in one row.

After: Use a separate table column for the line numbers.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/indexer/code/search.go    | 50 +++++++++++++++++++------------
 templates/code/searchresults.tmpl | 15 +---------
 templates/repo/search.tmpl        | 15 +---------
 templates/shared/searchfile.tmpl  | 14 +++++++++
 4 files changed, 47 insertions(+), 47 deletions(-)
 create mode 100644 templates/shared/searchfile.tmpl

diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index e19e22eea0..2ddc2397fa 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -16,14 +16,18 @@ import (
 
 // Result a search result to display
 type Result struct {
-	RepoID         int64
-	Filename       string
-	CommitID       string
-	UpdatedUnix    timeutil.TimeStamp
-	Language       string
-	Color          string
-	LineNumbers    []int
-	FormattedLines template.HTML
+	RepoID      int64
+	Filename    string
+	CommitID    string
+	UpdatedUnix timeutil.TimeStamp
+	Language    string
+	Color       string
+	Lines       []ResultLine
+}
+
+type ResultLine struct {
+	Num              int
+	FormattedContent template.HTML
 }
 
 type SearchResultLanguages = internal.SearchResultLanguages
@@ -70,7 +74,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 	var formattedLinesBuffer bytes.Buffer
 
 	contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n")
-	lineNumbers := make([]int, len(contentLines))
+	lines := make([]ResultLine, 0, len(contentLines))
 	index := startIndex
 	for i, line := range contentLines {
 		var err error
@@ -93,21 +97,29 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 			return nil, err
 		}
 
-		lineNumbers[i] = startLineNum + i
+		lines = append(lines, ResultLine{Num: startLineNum + i})
 		index += len(line)
 	}
 
-	highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
+	// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
+	hl, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
+	highlightedLines := strings.Split(string(hl), "\n")
+
+	// The lines outputted by highlight.Code might not match the original lines, because "highlight" removes the last `\n`
+	lines = lines[:min(len(highlightedLines), len(lines))]
+	highlightedLines = highlightedLines[:len(lines)]
+	for i := 0; i < len(lines); i++ {
+		lines[i].FormattedContent = template.HTML(highlightedLines[i])
+	}
 
 	return &Result{
-		RepoID:         result.RepoID,
-		Filename:       result.Filename,
-		CommitID:       result.CommitID,
-		UpdatedUnix:    result.UpdatedUnix,
-		Language:       result.Language,
-		Color:          result.Color,
-		LineNumbers:    lineNumbers,
-		FormattedLines: highlighted,
+		RepoID:      result.RepoID,
+		Filename:    result.Filename,
+		CommitID:    result.CommitID,
+		UpdatedUnix: result.UpdatedUnix,
+		Language:    result.Language,
+		Color:       result.Color,
+		Lines:       lines,
 	}, nil
 }
 
diff --git a/templates/code/searchresults.tmpl b/templates/code/searchresults.tmpl
index bb21a5e0dc..08bb12951d 100644
--- a/templates/code/searchresults.tmpl
+++ b/templates/code/searchresults.tmpl
@@ -22,20 +22,7 @@
 				<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
 			</h4>
 			<div class="ui attached table segment">
-				<div class="file-body file-code code-view">
-					<table>
-						<tbody>
-							<tr>
-								<td class="lines-num">
-									{{range .LineNumbers}}
-										<a href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{$result.Filename | PathEscapeSegments}}#L{{.}}"><span>{{.}}</span></a>
-									{{end}}
-								</td>
-								<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
-							</tr>
-						</tbody>
-					</table>
-				</div>
+				{{template "shared/searchfile" dict "RepoLink" $repo.Link "SearchResult" .}}
 			</div>
 			{{template "shared/searchbottom" dict "root" $ "result" .}}
 		</div>
diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl
index 7b3ad7282e..7513d444cc 100644
--- a/templates/repo/search.tmpl
+++ b/templates/repo/search.tmpl
@@ -44,20 +44,7 @@
 								<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments .Filename}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
 							</h4>
 							<div class="ui attached table segment">
-								<div class="file-body file-code code-view">
-									<table>
-										<tbody>
-											<tr>
-												<td class="lines-num">
-													{{range .LineNumbers}}
-														<a href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
-													{{end}}
-												</td>
-												<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
-											</tr>
-										</tbody>
-									</table>
-								</div>
+								{{template "shared/searchfile" dict "RepoLink" $.SourcePath "SearchResult" .}}
 							</div>
 							{{template "shared/searchbottom" dict "root" $ "result" .}}
 						</div>
diff --git a/templates/shared/searchfile.tmpl b/templates/shared/searchfile.tmpl
new file mode 100644
index 0000000000..280584e4d1
--- /dev/null
+++ b/templates/shared/searchfile.tmpl
@@ -0,0 +1,14 @@
+<div class="file-body file-code code-view">
+	<table>
+		<tbody>
+			{{range .SearchResult.Lines}}
+				<tr>
+					<td class="lines-num">
+						<a href="{{$.RepoLink}}/src/commit/{{PathEscape $.SearchResult.CommitID}}/{{PathEscapeSegments $.SearchResult.Filename}}#L{{.Num}}"><span>{{.Num}}</span></a>
+					</td>
+					<td class="lines-code chroma"><code class="code-inner">{{.FormattedContent}}</code></td>
+				</tr>
+			{{end}}
+		</tbody>
+	</table>
+</div>

From 5bdf805e054ec4131d8f1451752f251e8807cc73 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Wed, 6 Mar 2024 16:47:52 +0800
Subject: [PATCH 291/679] Sync branches to DB immediately when handle git hook
 calling (#29493)

Unlike other async processing in the queue, we should sync branches to
the DB immediately when handling git hook calling. If it fails, users
can see the error message in the output of the git command.

It can avoid potential inconsistency issues, and help #29494.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/git/branch.go                 |   5 +
 routers/private/hook_post_receive.go |  66 +++++++++++++-
 services/repository/branch.go        | 115 ++++++++++++++++-------
 services/repository/push.go          |   9 --
 tests/integration/git_push_test.go   | 131 +++++++++++++++++++++++++++
 5 files changed, 282 insertions(+), 44 deletions(-)
 create mode 100644 tests/integration/git_push_test.go

diff --git a/models/git/branch.go b/models/git/branch.go
index db02fc9b28..fa0781fed1 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -158,6 +158,11 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
 	return &branch, nil
 }
 
+func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) {
+	branches := make([]*Branch, 0, len(branchNames))
+	return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches)
+}
+
 func AddBranches(ctx context.Context, branches []*Branch) error {
 	for _, branch := range branches {
 		if _, err := db.GetEngine(ctx).Insert(branch); err != nil {
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 4eafe3923d..c5504126f8 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -8,9 +8,11 @@ import (
 	"net/http"
 	"strconv"
 
+	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	repo_module "code.gitea.io/gitea/modules/repository"
@@ -27,6 +29,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 
 	// We don't rely on RepoAssignment here because:
 	// a) we don't need the git repo in this function
+	//    OUT OF DATE: we do need the git repo to sync the branch to the db now.
 	// b) our update function will likely change the repository in the db so we will need to refresh it
 	// c) we don't always need the repo
 
@@ -34,7 +37,11 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 	repoName := ctx.Params(":repo")
 
 	// defer getting the repository at this point - as we should only retrieve it if we're going to call update
-	var repo *repo_model.Repository
+	var (
+		repo    *repo_model.Repository
+		gitRepo *git.Repository
+	)
+	defer gitRepo.Close() // it's safe to call Close on a nil pointer
 
 	updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
 	wasEmpty := false
@@ -87,6 +94,63 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 			})
 			return
 		}
+
+		branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
+		for _, update := range updates {
+			if !update.RefFullName.IsBranch() {
+				continue
+			}
+			if repo == nil {
+				repo = loadRepository(ctx, ownerName, repoName)
+				if ctx.Written() {
+					return
+				}
+				wasEmpty = repo.IsEmpty
+			}
+
+			if update.IsDelRef() {
+				if err := git_model.AddDeletedBranch(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
+					log.Error("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err)
+					ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+						Err: fmt.Sprintf("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err),
+					})
+					return
+				}
+			} else {
+				branchesToSync = append(branchesToSync, update)
+			}
+		}
+		if len(branchesToSync) > 0 {
+			if gitRepo == nil {
+				var err error
+				gitRepo, err = gitrepo.OpenRepository(ctx, repo)
+				if err != nil {
+					log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
+					ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+						Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
+					})
+					return
+				}
+			}
+
+			var (
+				branchNames = make([]string, 0, len(branchesToSync))
+				commitIDs   = make([]string, 0, len(branchesToSync))
+			)
+			for _, update := range branchesToSync {
+				branchNames = append(branchNames, update.RefFullName.BranchName())
+				commitIDs = append(commitIDs, update.NewCommitID)
+			}
+
+			if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, func(commitID string) (*git.Commit, error) {
+				return gitRepo.GetCommit(commitID)
+			}); err != nil {
+				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+					Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
+				})
+				return
+			}
+		}
 	}
 
 	// Handle Push Options
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 55cedf5d84..402814fb9a 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -221,44 +221,91 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri
 	return err
 }
 
-// syncBranchToDB sync the branch information in the database. It will try to update the branch first,
-// if updated success with affect records > 0, then all are done. Because that means the branch has been in the database.
-// If no record is affected, that means the branch does not exist in database. So there are two possibilities.
-// One is this is a new branch, then we just need to insert the record. Another is the branches haven't been synced,
-// then we need to sync all the branches into database.
-func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
-	cnt, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit)
-	if err != nil {
-		return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
-	}
-	if cnt > 0 { // This means branch does exist, so it's a normal update. It also means the branch has been synced.
-		return nil
+// SyncBranchesToDB sync the branch information in the database.
+// It will check whether the branches of the repository have never been synced before.
+// If so, it will sync all branches of the repository.
+// Otherwise, it will sync the branches that need to be updated.
+func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, commitIDs []string, getCommit func(commitID string) (*git.Commit, error)) error {
+	// Some designs that make the code look strange but are made for performance optimization purposes:
+	// 1. Sync branches in a batch to reduce the number of DB queries.
+	// 2. Lazy load commit information since it may be not necessary.
+	// 3. Exit early if synced all branches of git repo when there's no branch in DB.
+	// 4. Check the branches in DB if they are already synced.
+	//
+	// If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
+	// See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
+	// For the first batch, it will hit optimization 3.
+	// For other batches, it will hit optimization 4.
+
+	if len(branchNames) != len(commitIDs) {
+		return fmt.Errorf("branchNames and commitIDs length not match")
 	}
 
-	// if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
-	// we cannot simply insert the branch but need to check we have branches or not
-	hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
-		RepoID:          repoID,
-		IsDeletedBranch: optional.Some(false),
-	}.ToConds())
-	if err != nil {
-		return err
-	}
-	if !hasBranch {
-		if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
-			return fmt.Errorf("repo_module.SyncRepoBranches %d:%s failed: %v", repoID, branchName, err)
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		branches, err := git_model.GetBranches(ctx, repoID, branchNames)
+		if err != nil {
+			return fmt.Errorf("git_model.GetBranches: %v", err)
+		}
+
+		if len(branches) == 0 {
+			// if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
+			// we cannot simply insert the branch but need to check we have branches or not
+			hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
+				RepoID:          repoID,
+				IsDeletedBranch: optional.Some(false),
+			}.ToConds())
+			if err != nil {
+				return err
+			}
+			if !hasBranch {
+				if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
+					return fmt.Errorf("repo_module.SyncRepoBranches %d failed: %v", repoID, err)
+				}
+				return nil
+			}
+		}
+
+		branchMap := make(map[string]*git_model.Branch, len(branches))
+		for _, branch := range branches {
+			branchMap[branch.Name] = branch
+		}
+
+		newBranches := make([]*git_model.Branch, 0, len(branchNames))
+
+		for i, branchName := range branchNames {
+			commitID := commitIDs[i]
+			branch, exist := branchMap[branchName]
+			if exist && branch.CommitID == commitID {
+				continue
+			}
+
+			commit, err := getCommit(branchName)
+			if err != nil {
+				return fmt.Errorf("get commit of %s failed: %v", branchName, err)
+			}
+
+			if exist {
+				if _, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit); err != nil {
+					return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
+				}
+				return nil
+			}
+
+			// if database have branches but not this branch, it means this is a new branch
+			newBranches = append(newBranches, &git_model.Branch{
+				RepoID:        repoID,
+				Name:          branchName,
+				CommitID:      commit.ID.String(),
+				CommitMessage: commit.Summary(),
+				PusherID:      pusherID,
+				CommitTime:    timeutil.TimeStamp(commit.Committer.When.Unix()),
+			})
+		}
+
+		if len(newBranches) > 0 {
+			return db.Insert(ctx, newBranches)
 		}
 		return nil
-	}
-
-	// if database have branches but not this branch, it means this is a new branch
-	return db.Insert(ctx, &git_model.Branch{
-		RepoID:        repoID,
-		Name:          branchName,
-		CommitID:      commit.ID.String(),
-		CommitMessage: commit.Summary(),
-		PusherID:      pusherID,
-		CommitTime:    timeutil.TimeStamp(commit.Committer.When.Unix()),
 	})
 }
 
diff --git a/services/repository/push.go b/services/repository/push.go
index 9aaf0e1c9b..89a3127902 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/db"
-	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/cache"
@@ -259,10 +258,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 					commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
 				}
 
-				if err = syncBranchToDB(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil {
-					return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err)
-				}
-
 				notify_service.PushCommits(ctx, pusher, repo, opts, commits)
 
 				// Cache for big repository
@@ -275,10 +270,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 					// close all related pulls
 					log.Error("close related pull request failed: %v", err)
 				}
-
-				if err := git_model.AddDeletedBranch(ctx, repo.ID, branch, pusher.ID); err != nil {
-					return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err)
-				}
 			}
 
 			// Even if user delete a branch on a repository which he didn't watch, he will be watch that.
diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go
new file mode 100644
index 0000000000..cb2910b175
--- /dev/null
+++ b/tests/integration/git_push_test.go
@@ -0,0 +1,131 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/url"
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	repo_service "code.gitea.io/gitea/services/repository"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGitPush(t *testing.T) {
+	onGiteaRun(t, testGitPush)
+}
+
+func testGitPush(t *testing.T, u *url.URL) {
+	t.Run("Push branches at once", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			for i := 0; i < 100; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				pushed = append(pushed, branchName)
+				doGitCreateBranch(gitPath, branchName)(t)
+			}
+			pushed = append(pushed, "master")
+			doGitPushTestRepository(gitPath, "origin", "--all")(t)
+			return pushed, deleted
+		})
+	})
+
+	t.Run("Push branches one by one", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			for i := 0; i < 100; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				doGitCreateBranch(gitPath, branchName)(t)
+				doGitPushTestRepository(gitPath, "origin", branchName)(t)
+				pushed = append(pushed, branchName)
+			}
+			return pushed, deleted
+		})
+	})
+
+	t.Run("Delete branches", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete
+			pushed = append(pushed, "master")
+
+			for i := 0; i < 100; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				pushed = append(pushed, branchName)
+				doGitCreateBranch(gitPath, branchName)(t)
+			}
+			doGitPushTestRepository(gitPath, "origin", "--all")(t)
+
+			for i := 0; i < 10; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				doGitPushTestRepository(gitPath, "origin", "--delete", branchName)(t)
+				deleted = append(deleted, branchName)
+			}
+			return pushed, deleted
+		})
+	})
+}
+
+func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gitPath string) (pushed, deleted []string)) {
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+		Name:          "repo-to-push",
+		Description:   "test git push",
+		AutoInit:      false,
+		DefaultBranch: "main",
+		IsPrivate:     false,
+	})
+	require.NoError(t, err)
+	require.NotEmpty(t, repo)
+
+	gitPath := t.TempDir()
+
+	doGitInitTestRepository(gitPath)(t)
+
+	oldPath := u.Path
+	oldUser := u.User
+	defer func() {
+		u.Path = oldPath
+		u.User = oldUser
+	}()
+	u.Path = repo.FullName() + ".git"
+	u.User = url.UserPassword(user.LowerName, userPassword)
+
+	doGitAddRemote(gitPath, "origin", u)(t)
+
+	gitRepo, err := git.OpenRepository(git.DefaultContext, gitPath)
+	require.NoError(t, err)
+	defer gitRepo.Close()
+
+	pushedBranches, deletedBranches := gitOperation(t, gitPath)
+
+	dbBranches := make([]*git_model.Branch, 0)
+	require.NoError(t, db.GetEngine(db.DefaultContext).Where("repo_id=?", repo.ID).Find(&dbBranches))
+	assert.Equalf(t, len(pushedBranches), len(dbBranches), "mismatched number of branches in db")
+	dbBranchesMap := make(map[string]*git_model.Branch, len(dbBranches))
+	for _, branch := range dbBranches {
+		dbBranchesMap[branch.Name] = branch
+	}
+
+	deletedBranchesMap := make(map[string]bool, len(deletedBranches))
+	for _, branchName := range deletedBranches {
+		deletedBranchesMap[branchName] = true
+	}
+
+	for _, branchName := range pushedBranches {
+		branch, ok := dbBranchesMap[branchName]
+		deleted := deletedBranchesMap[branchName]
+		assert.True(t, ok, "branch %s not found in database", branchName)
+		assert.Equal(t, deleted, branch.IsDeleted, "IsDeleted of %s is %v, but it's expected to be %v", branchName, branch.IsDeleted, deleted)
+		commitID, err := gitRepo.GetBranchCommitID(branchName)
+		require.NoError(t, err)
+		assert.Equal(t, commitID, branch.CommitID)
+	}
+
+	require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID))
+}

From a4bcfb8ef1d5b2b522f78c9560d53ddbdbb02218 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Wed, 6 Mar 2024 17:56:04 +0800
Subject: [PATCH 292/679] Detect broken git hooks (#29494)

Detect broken git hooks by checking if the commit id of branches in DB
is the same with the git repo.

It can help #29338 #28277 and maybe more issues.

Users could complain about actions, webhooks, and activities not
working, but they were not aware that it is caused by broken git hooks
unless they could see a warning.

<img width="1348" alt="image"
src="https://github.com/go-gitea/gitea/assets/9418365/2b92a46d-7f1d-4115-bef4-9f970bd695da">


It should be merged after #29493. Otherwise, users could see a ephemeral
warning after committing and opening the repo home page immediately.

And it also waits for #29495, since the doc link (the anchor part) will
be updated.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 options/locale/locale_en-US.ini |  1 +
 routers/web/repo/view.go        | 27 +++++++++++++++++++++++++++
 2 files changed, 28 insertions(+)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 59aae68cae..d30c8e521d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2643,6 +2643,7 @@ find_file.no_matching = No matching file found
 error.csv.too_large = Can't render this file because it is too large.
 error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d.
 error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
+error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the <a target="_blank" rel="noreferrer" href="%s">documentation</a> to fix them, then push some commits to refresh the status.
 
 [graphs]
 component_loading = Loading %s...
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 4df10fbea1..d47c926fa1 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -998,6 +998,8 @@ func renderHomeCode(ctx *context.Context) {
 		return
 	}
 
+	checkOutdatedBranch(ctx)
+
 	checkCitationFile(ctx, entry)
 	if ctx.Written() {
 		return
@@ -1064,6 +1066,31 @@ func renderHomeCode(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplRepoHome)
 }
 
+func checkOutdatedBranch(ctx *context.Context) {
+	if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) {
+		return
+	}
+
+	// get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
+	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
+	if err != nil {
+		log.Error("GetBranchCommitID: %v", err)
+		// Don't return an error page, as it can be rechecked the next time the user opens the page.
+		return
+	}
+
+	dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName)
+	if err != nil {
+		log.Error("GetBranch: %v", err)
+		// Don't return an error page, as it can be rechecked the next time the user opens the page.
+		return
+	}
+
+	if dbBranch.CommitID != commit.ID.String() {
+		ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
+	}
+}
+
 // RenderUserCards render a page show users according the input template
 func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
 	page := ctx.FormInt("page")

From c6cc392b55b71ae1787e86e75b7121c3769adcbe Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Wed, 6 Mar 2024 19:23:27 +0900
Subject: [PATCH 293/679] Fix wrong header of org project view page (#29626)

Follow #29248

The project view page still using `user/overview/header.tmpl`

Before:

![image](https://github.com/go-gitea/gitea/assets/18380374/9cb638a3-7cc6-4efa-979a-e2592007fd12)

After:

![image](https://github.com/go-gitea/gitea/assets/18380374/62b0b2ea-8cb0-459f-b27a-bad3908eb1c0)
---
 templates/user/overview/header.tmpl | 28 +++++++++++++++-------------
 1 file changed, 15 insertions(+), 13 deletions(-)

diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl
index 4fdaa70d87..cf5e21fa62 100644
--- a/templates/user/overview/header.tmpl
+++ b/templates/user/overview/header.tmpl
@@ -29,19 +29,21 @@
 		</a>
 	{{end}}
 
-	<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
-		{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
-	</a>
-	{{if not .DisableStars}}
-		<a class="{{if eq .TabName "stars"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=stars">
-			{{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}}
-			{{if .ContextUser.NumStars}}
-				<div class="ui small label">{{.ContextUser.NumStars}}</div>
-			{{end}}
-		</a>
-	{{else}}
-		<a class="{{if eq .TabName "watching"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=watching">
-			{{svg "octicon-eye"}} {{ctx.Locale.Tr "user.watched"}}
+	{{if .ContextUser.IsIndividual}}
+		<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
+			{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
 		</a>
+		{{if not .DisableStars}}
+			<a class="{{if eq .TabName "stars"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=stars">
+				{{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}}
+				{{if .ContextUser.NumStars}}
+					<div class="ui small label">{{.ContextUser.NumStars}}</div>
+				{{end}}
+			</a>
+		{{else}}
+			<a class="{{if eq .TabName "watching"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=watching">
+				{{svg "octicon-eye"}} {{ctx.Locale.Tr "user.watched"}}
+			</a>
+		{{end}}
 	{{end}}
 </div>

From c381343a60b9c9eedb9dbc0931d1b87cb3c1f366 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 6 Mar 2024 19:25:00 +0800
Subject: [PATCH 294/679] Add a link for the recently pushed branch
 notification (#29627)

---
 templates/repo/code/recently_pushed_new_branches.tmpl | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index fedba06fad..5e603eae81 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -2,7 +2,8 @@
 	<div class="ui positive message gt-df gt-ac">
 		<div class="gt-f1">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
-			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" .Name $timeSince}}
+			{{$branchLink := HTMLFormat `<a href="%s/src/branch/%s">%s</a>` $.RepoLink (PathEscapeSegments .Name) .Name}}
+			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
 		</div>
 		<a role="button" class="ui compact positive button gt-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
 			{{ctx.Locale.Tr "repo.pulls.compare_changes"}}

From 90a3f2d4b7ed3890d9655c0334444f86d89b7b30 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 6 Mar 2024 19:50:39 +0800
Subject: [PATCH 295/679] Avoid unexpected panic in graceful manager (#29629)

There is a fundamental design problem of the "manager" and the "wait
group".
If nothing has started, the "Wait" just panics: sync: WaitGroup is
reused before previous Wait has returned
There is no clear solution besides a complete rewriting of the "manager"

If there are some mistakes in the app.ini, end users would just see the
"panic", but not the real error messages. A real case: #27643

This PR is just a quick fix for the annoying panic problem.
---
 modules/graceful/manager_unix.go    | 10 +++++++++-
 modules/graceful/manager_windows.go | 10 +++++++++-
 2 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go
index f4af4993d9..edf5fc248f 100644
--- a/modules/graceful/manager_unix.go
+++ b/modules/graceful/manager_unix.go
@@ -59,7 +59,15 @@ func (g *Manager) start() {
 	go func() {
 		defer close(startupDone)
 		// Wait till we're done getting all the listeners and then close the unused ones
-		g.createServerWaitGroup.Wait()
+		func() {
+			// FIXME: there is a fundamental design problem of the "manager" and the "wait group".
+			// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
+			// There is no clear solution besides a complete rewriting of the "manager"
+			defer func() {
+				_ = recover()
+			}()
+			g.createServerWaitGroup.Wait()
+		}()
 		// Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
 		_ = CloseProvidedListeners()
 		g.notify(readyMsg)
diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go
index 0248dcb24d..ecf30af3f3 100644
--- a/modules/graceful/manager_windows.go
+++ b/modules/graceful/manager_windows.go
@@ -150,7 +150,15 @@ func (g *Manager) awaitServer(limit time.Duration) bool {
 	c := make(chan struct{})
 	go func() {
 		defer close(c)
-		g.createServerWaitGroup.Wait()
+		func() {
+			// FIXME: there is a fundamental design problem of the "manager" and the "wait group".
+			// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
+			// There is no clear solution besides a complete rewriting of the "manager"
+			defer func() {
+				_ = recover()
+			}()
+			g.createServerWaitGroup.Wait()
+		}()
 	}()
 	if limit > 0 {
 		select {

From e308d25f1b2fe24b4735432b05e5e221879a2705 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 6 Mar 2024 20:17:19 +0800
Subject: [PATCH 296/679] Cache repository default branch commit status to
 reduce query on commit status table (#29444)

After repository commit status has been introduced on dashaboard, the
most top SQL comes from `GetLatestCommitStatusForPairs`.

This PR adds a cache for the repository's default branch's latest
combined commit status. When a new commit status updated, the cache will
be marked as invalid.

<img width="998" alt="image"
src="https://github.com/go-gitea/gitea/assets/81045/76759de7-3a83-4d54-8571-278f5422aed3">
---
 routers/api/v1/repo/status.go                 |   4 +-
 routers/web/repo/repo.go                      |  28 ++--
 .../repository/commitstatus/commitstatus.go   | 135 ++++++++++++++++++
 services/repository/files/commit.go           |  48 -------
 4 files changed, 145 insertions(+), 70 deletions(-)
 create mode 100644 services/repository/commitstatus/commitstatus.go

diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go
index 53711bedeb..9e36ea0aed 100644
--- a/routers/api/v1/repo/status.go
+++ b/routers/api/v1/repo/status.go
@@ -14,7 +14,7 @@ import (
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
-	files_service "code.gitea.io/gitea/services/repository/files"
+	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
 )
 
 // NewCommitStatus creates a new CommitStatus
@@ -64,7 +64,7 @@ func NewCommitStatus(ctx *context.APIContext) {
 		Description: form.Description,
 		Context:     form.Context,
 	}
-	if err := files_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
+	if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
 		ctx.Error(http.StatusInternalServerError, "CreateCommitStatus", err)
 		return
 	}
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index f0caf199a2..b54d29c580 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -35,6 +35,7 @@ import (
 	"code.gitea.io/gitea/services/forms"
 	repo_service "code.gitea.io/gitea/services/repository"
 	archiver_service "code.gitea.io/gitea/services/repository/archiver"
+	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
 )
 
 const (
@@ -634,30 +635,14 @@ func SearchRepo(ctx *context.Context) {
 		return
 	}
 
-	// collect the latest commit of each repo
-	// at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
-	repoBranchNames := make(map[int64]string, len(repos))
-	for _, repo := range repos {
-		repoBranchNames[repo.ID] = repo.DefaultBranch
-	}
-
-	repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
+	latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
 	if err != nil {
-		log.Error("FindBranchesByRepoAndBranchName: %v", err)
-		return
-	}
-
-	// call the database O(1) times to get the commit statuses for all repos
-	repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll)
-	if err != nil {
-		log.Error("GetLatestCommitStatusForPairs: %v", err)
+		log.Error("FindReposLastestCommitStatuses: %v", err)
 		return
 	}
 
 	results := make([]*repo_service.WebSearchRepository, len(repos))
 	for i, repo := range repos {
-		latestCommitStatus := git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
-
 		results[i] = &repo_service.WebSearchRepository{
 			Repository: &api.Repository{
 				ID:       repo.ID,
@@ -671,8 +656,11 @@ func SearchRepo(ctx *context.Context) {
 				Link:     repo.Link(),
 				Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
 			},
-			LatestCommitStatus:       latestCommitStatus,
-			LocaleLatestCommitStatus: latestCommitStatus.LocaleString(ctx.Locale),
+		}
+
+		if latestCommitStatuses[i] != nil {
+			results[i].LatestCommitStatus = latestCommitStatuses[i]
+			results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale)
 		}
 	}
 
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
new file mode 100644
index 0000000000..145fc7d53c
--- /dev/null
+++ b/services/repository/commitstatus/commitstatus.go
@@ -0,0 +1,135 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package commitstatus
+
+import (
+	"context"
+	"crypto/sha256"
+	"fmt"
+
+	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/cache"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/log"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/automerge"
+)
+
+func getCacheKey(repoID int64, brancheName string) string {
+	hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", repoID, brancheName)))
+	return fmt.Sprintf("commit_status:%x", hashBytes)
+}
+
+func updateCommitStatusCache(ctx context.Context, repoID int64, branchName string, status api.CommitStatusState) error {
+	c := cache.GetCache()
+	return c.Put(getCacheKey(repoID, branchName), string(status), 3*24*60)
+}
+
+func deleteCommitStatusCache(ctx context.Context, repoID int64, branchName string) error {
+	c := cache.GetCache()
+	return c.Delete(getCacheKey(repoID, branchName))
+}
+
+// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
+// NOTE: All text-values will be trimmed from whitespaces.
+// Requires: Repo, Creator, SHA
+func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
+	repoPath := repo.RepoPath()
+
+	// confirm that commit is exist
+	gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+	if err != nil {
+		return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
+	}
+	defer closer.Close()
+
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
+	commit, err := gitRepo.GetCommit(sha)
+	if err != nil {
+		return fmt.Errorf("GetCommit[%s]: %w", sha, err)
+	}
+	if len(sha) != objectFormat.FullLength() {
+		// use complete commit sha
+		sha = commit.ID.String()
+	}
+
+	if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
+		Repo:         repo,
+		Creator:      creator,
+		SHA:          commit.ID,
+		CommitStatus: status,
+	}); err != nil {
+		return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+	}
+
+	defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
+	if err != nil {
+		return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err)
+	}
+
+	if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
+		if err := deleteCommitStatusCache(ctx, repo.ID, repo.DefaultBranch); err != nil {
+			log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+		}
+	}
+
+	if status.State.IsSuccess() {
+		if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
+			return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+		}
+	}
+
+	return nil
+}
+
+// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
+func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
+	results := make([]*git_model.CommitStatus, len(repos))
+	c := cache.GetCache()
+
+	for i, repo := range repos {
+		status, ok := c.Get(getCacheKey(repo.ID, repo.DefaultBranch)).(string)
+		if ok && status != "" {
+			results[i] = &git_model.CommitStatus{State: api.CommitStatusState(status)}
+		}
+	}
+
+	// collect the latest commit of each repo
+	// at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
+	repoBranchNames := make(map[int64]string, len(repos))
+	for i, repo := range repos {
+		if results[i] == nil {
+			repoBranchNames[repo.ID] = repo.DefaultBranch
+		}
+	}
+
+	repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
+	if err != nil {
+		return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err)
+	}
+
+	// call the database O(1) times to get the commit statuses for all repos
+	repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err)
+	}
+
+	for i, repo := range repos {
+		if results[i] == nil {
+			results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
+			if results[i].State != "" {
+				if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil {
+					log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+				}
+			}
+		}
+	}
+
+	return results, nil
+}
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index 512aec7c81..e0dad29273 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -5,61 +5,13 @@ package files
 
 import (
 	"context"
-	"fmt"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
-	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/services/automerge"
 )
 
-// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
-// NOTE: All text-values will be trimmed from whitespaces.
-// Requires: Repo, Creator, SHA
-func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
-	repoPath := repo.RepoPath()
-
-	// confirm that commit is exist
-	gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
-	if err != nil {
-		return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
-	}
-	defer closer.Close()
-
-	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
-
-	commit, err := gitRepo.GetCommit(sha)
-	if err != nil {
-		gitRepo.Close()
-		return fmt.Errorf("GetCommit[%s]: %w", sha, err)
-	} else if len(sha) != objectFormat.FullLength() {
-		// use complete commit sha
-		sha = commit.ID.String()
-	}
-	gitRepo.Close()
-
-	if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
-		Repo:         repo,
-		Creator:      creator,
-		SHA:          commit.ID,
-		CommitStatus: status,
-	}); err != nil {
-		return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
-	}
-
-	if status.State.IsSuccess() {
-		if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
-			return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
-		}
-	}
-
-	return nil
-}
-
 // CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch
 func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, branch string) (*git.DivergeObject, error) {
 	divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch)

From 1d2548949adf6046f330d27084efce6e63330e04 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 6 Mar 2024 21:12:44 +0800
Subject: [PATCH 297/679] Avoid issue info panic (#29625)

Fix #29624
---
 models/activities/action.go | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/models/activities/action.go b/models/activities/action.go
index fcc97e3872..36205eedd1 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -393,10 +393,14 @@ func (a *Action) GetCreate() time.Time {
 	return a.CreatedUnix.AsTime()
 }
 
-// GetIssueInfos returns a list of issues associated with
-// the action.
+// GetIssueInfos returns a list of associated information with the action.
 func (a *Action) GetIssueInfos() []string {
-	return strings.SplitN(a.Content, "|", 3)
+	// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
+	ret := strings.SplitN(a.Content, "|", 3)
+	for len(ret) < 3 {
+		ret = append(ret, "")
+	}
+	return ret
 }
 
 // GetIssueTitle returns the title of first issue associated with the action.

From c996e359585597d8c86e57edf91ada1564e5106c Mon Sep 17 00:00:00 2001
From: Rafael Heard <rafael.heard@gmail.com>
Date: Wed, 6 Mar 2024 09:20:26 -0500
Subject: [PATCH 298/679] Move all login and account creation page labels to be
 above inputs (#29432)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

There are a few inconsistencies within Gitea and this PR addresses one
of them. This PR updates the sign-in page layout, including the register
and openID tabs, to match the layout of the settings pages
(/user/settings) for more consistency.

This PR updates the following routes:
`/user/login`
`/user/sign_up`
`/user/login/openid`
`/user/forgot_password`
`/user/link_account`
`/user/recover_account`

**Before**
<img width="968" alt="Screenshot 2024-02-05 at 8 27 24 AM"
src="https://github.com/go-gitea/gitea/assets/6152817/fb0cb517-57c0-4eed-be1d-56f36bd1960d">


**After**
<img width="968" alt="Screenshot 2024-02-05 at 8 26 39 AM"
src="https://github.com/go-gitea/gitea/assets/6152817/428d691d-0a42-4a67-a646-05527f2a7b41">


This PR addresses a revert of the original PR due to this
[comment](https://github.com/go-gitea/gitea/pull/28753#issuecomment-1956596817).

---------

Co-authored-by: rafh <rafaelheard@gmail.com>
---
 templates/user/auth/activate.tmpl               |  7 +++----
 templates/user/auth/captcha.tmpl                |  3 +--
 templates/user/auth/change_passwd_inner.tmpl    |  7 +++----
 templates/user/auth/forgot_passwd.tmpl          |  3 +--
 templates/user/auth/prohibit_login.tmpl         |  2 +-
 templates/user/auth/reset_passwd.tmpl           |  6 ++----
 templates/user/auth/signin_inner.tmpl           | 15 ++++++---------
 templates/user/auth/signin_openid.tmpl          |  6 ++----
 templates/user/auth/signup_inner.tmpl           | 14 ++++++--------
 templates/user/auth/signup_openid_register.tmpl |  9 ++++-----
 templates/user/auth/twofa.tmpl                  |  5 ++---
 templates/user/auth/twofa_scratch.tmpl          |  5 ++---
 web_src/css/form.css                            | 10 +---------
 13 files changed, 34 insertions(+), 58 deletions(-)

diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl
index e32a5d8707..7f8ff0eb5a 100644
--- a/templates/user/auth/activate.tmpl
+++ b/templates/user/auth/activate.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user activate">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/activate" method="post">
+			<form class="ui form ignore-dirty tw-max-w-2xl tw-m-auto" action="{{AppSubUrl}}/user/activate" method="post">
 				{{.CsrfTokenHtml}}
 				<h2 class="ui top attached header">
 					{{ctx.Locale.Tr "auth.active_your_account"}}
@@ -10,12 +10,11 @@
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
 					{{if .NeedVerifyLocalPassword}}
-						<div class="required inline field">
+						<div class="required field">
 							<label for="verify-password">{{ctx.Locale.Tr "password"}}</label>
 							<input id="verify-password" name="password" type="password" autocomplete="off" required>
 						</div>
 						<div class="inline field">
-							<label></label>
 							<button class="ui primary button">{{ctx.Locale.Tr "install.confirm_password"}}</button>
 						</div>
 						<input name="code" type="hidden" value="{{.ActivationCode}}">
@@ -29,7 +28,7 @@
 							</div>
 						</details>
 						<div class="divider"></div>
-						<div class="text right">
+						<div class="text">
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
 						</div>
 					{{end}}
diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index d4d1a82418..0e9c2b9d22 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -1,9 +1,8 @@
 {{if .EnableCaptcha}}{{if eq .CaptchaType "image"}}
 	<div class="inline field">
-		<label>{{/* This is CAPTCHA field */}}</label>
 		{{.Captcha.CreateHTML}}
 	</div>
-	<div class="required inline field {{if .Err_Captcha}}error{{end}}">
+	<div class="required field {{if .Err_Captcha}}error{{end}}">
 		<label for="captcha">{{ctx.Locale.Tr "captcha"}}</label>
 		<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off">
 	</div>
diff --git a/templates/user/auth/change_passwd_inner.tmpl b/templates/user/auth/change_passwd_inner.tmpl
index cffc798a64..01bbf500e5 100644
--- a/templates/user/auth/change_passwd_inner.tmpl
+++ b/templates/user/auth/change_passwd_inner.tmpl
@@ -5,18 +5,17 @@
 			{{ctx.Locale.Tr "settings.change_password"}}
 		</h4>
 		<div class="ui attached segment">
-			<form class="ui form" action="{{.ChangePasscodeLink}}" method="post">
+			<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.ChangePasscodeLink}}" method="post">
 			{{.CsrfTokenHtml}}
-			<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+			<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 				<label for="password">{{ctx.Locale.Tr "password"}}</label>
 				<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
 			</div>
-			<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+			<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 				<label for="retype">{{ctx.Locale.Tr "re_type"}}</label>
 				<input id="retype" name="retype" type="password" autocomplete="new-password" required>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<button class="ui primary button">{{ctx.Locale.Tr "settings.change_password"}}</button>
 			</div>
 			</form>
diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl
index 21a630ec5e..55bcf63018 100644
--- a/templates/user/auth/forgot_passwd.tmpl
+++ b/templates/user/auth/forgot_passwd.tmpl
@@ -12,13 +12,12 @@
 					{{if .IsResetSent}}
 						<p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" .Email .ResetPwdCodeLives}}</p>
 					{{else if .IsResetRequest}}
-						<div class="required inline field {{if .Err_Email}}error{{end}}">
+						<div class="required field {{if .Err_Email}}error{{end}}">
 							<label for="email">{{ctx.Locale.Tr "email"}}</label>
 							<input id="email" name="email" type="email"  value="{{.Email}}" autofocus required>
 						</div>
 						<div class="divider"></div>
 						<div class="inline field">
-							<label></label>
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.send_reset_mail"}}</button>
 						</div>
 					{{else if .IsResetDisable}}
diff --git a/templates/user/auth/prohibit_login.tmpl b/templates/user/auth/prohibit_login.tmpl
index 668aa20e71..962ddfa98c 100644
--- a/templates/user/auth/prohibit_login.tmpl
+++ b/templates/user/auth/prohibit_login.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user activate">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form">
+			<form class="ui form tw-max-w-2xl tw-m-auto">
 				<h2 class="ui top attached header">
 					{{ctx.Locale.Tr "auth.prohibit_login"}}
 				</h2>
diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl
index 9fee30f554..4d569e206c 100644
--- a/templates/user/auth/reset_passwd.tmpl
+++ b/templates/user/auth/reset_passwd.tmpl
@@ -17,13 +17,12 @@
 						</div>
 					{{end}}
 					{{if .IsResetForm}}
-						<div class="required inline field {{if .Err_Password}}error{{end}}">
+						<div class="required field {{if .Err_Password}}error{{end}}">
 							<label for="password">{{ctx.Locale.Tr "settings.new_password"}}</label>
 							<input id="password" name="password" type="password"  value="{{.password}}" autocomplete="new-password" autofocus required>
 						</div>
 						{{if not .user_signed_in}}
 						<div class="inline field">
-							<label></label>
 							<div class="ui checkbox">
 								<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 								<input name="remember" type="checkbox">
@@ -42,7 +41,7 @@
 						</div>
 						<input type="hidden" name="scratch_code" value="true">
 						{{else}}
-						<div class="required inline field {{if .Err_Passcode}}error{{end}}">
+						<div class="required field {{if .Err_Passcode}}error{{end}}">
 							<label for="passcode">{{ctx.Locale.Tr "passcode"}}</label>
 							<input id="passcode" name="passcode" type="number" autocomplete="off" autofocus required>
 						</div>
@@ -50,7 +49,6 @@
 						{{end}}
 						<div class="divider"></div>
 						<div class="inline field">
-							<label></label>
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.reset_password_helper"}}</button>
 							{{if and .has_two_factor (not .scratch_code)}}
 								<a href="{{.Link}}?code={{.Code}}&amp;scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 0d0064b02a..d7d3649a4d 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -9,21 +9,20 @@
 	{{end}}
 </h4>
 <div class="ui attached segment">
-	<form class="ui form" action="{{.SignInLink}}" method="post">
+	<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.SignInLink}}" method="post">
 	{{.CsrfTokenHtml}}
-	<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+	<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
 		<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 	</div>
 	{{if or (not .DisablePassword) .LinkAccountMode}}
-	<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+	<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="password">{{ctx.Locale.Tr "password"}}</label>
 		<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required>
 	</div>
 	{{end}}
 	{{if not .LinkAccountMode}}
 	<div class="inline field">
-		<label></label>
 		<div class="ui checkbox">
 			<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 			<input name="remember" type="checkbox">
@@ -33,8 +32,7 @@
 
 	{{template "user/auth/captcha" .}}
 
-	<div class="inline field">
-		<label></label>
+	<div class="field">
 		<button class="ui primary button">
 			{{if .LinkAccountMode}}
 				{{ctx.Locale.Tr "auth.oauth_signin_submit"}}
@@ -46,8 +44,7 @@
 	</div>
 
 	{{if .ShowRegistrationButton}}
-		<div class="inline field">
-			<label></label>
+		<div class="field">
 			<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
 		</div>
 	{{end}}
@@ -60,7 +57,7 @@
 		<div class="gt-df gt-fc gt-jc">
 			<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
 				{{range $provider := .OAuth2Providers}}
-					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
 						{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 					</a>
diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl
index 0428026aa8..c1f392dc13 100644
--- a/templates/user/auth/signin_openid.tmpl
+++ b/templates/user/auth/signin_openid.tmpl
@@ -8,12 +8,12 @@
 			OpenID
 		</h4>
 		<div class="ui attached segment">
-			<form class="ui form" action="{{.Link}}" method="post">
+			<form class="ui form tw-m-auto" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="inline field">
 				{{ctx.Locale.Tr "auth.openid_signin_desc"}}
 			</div>
-			<div class="required inline field {{if .Err_OpenID}}error{{end}}">
+			<div class="required field {{if .Err_OpenID}}error{{end}}">
 				<label for="openid">
 				{{svg "fontawesome-openid"}}
 				OpenID URI
@@ -21,14 +21,12 @@
 				<input id="openid" name="openid" value="{{.openid}}" autofocus required>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<div class="ui checkbox">
 					<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 					<input name="remember" type="checkbox">
 				</div>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<button class="ui primary button">{{ctx.Locale.Tr "sign_in"}}</button>
 			</div>
 			</form>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index e930bd3d15..cfd826a0ce 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -7,7 +7,7 @@
 		{{end}}
 	</h4>
 	<div class="ui attached segment">
-		<form class="ui form" action="{{.SignUpLink}}" method="post">
+		<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.SignUpLink}}" method="post">
 			{{.CsrfTokenHtml}}
 			{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
 			{{template "base/alert" .}}
@@ -15,21 +15,21 @@
 			{{if .DisableRegistration}}
 				<p>{{ctx.Locale.Tr "auth.disable_register_prompt"}}</p>
 			{{else}}
-				<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+				<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 					<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
 					<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 				</div>
-				<div class="required inline field {{if .Err_Email}}error{{end}}">
+				<div class="required field {{if .Err_Email}}error{{end}}">
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
 					<input id="email" name="email" type="email" value="{{.email}}" required>
 				</div>
 
 				{{if not .DisablePassword}}
-					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+					<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="password">{{ctx.Locale.Tr "password"}}</label>
 						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
 					</div>
-					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+					<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="retype">{{ctx.Locale.Tr "re_type"}}</label>
 						<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required>
 					</div>
@@ -38,7 +38,6 @@
 				{{template "user/auth/captcha" .}}
 
 				<div class="inline field">
-					<label></label>
 					<button class="ui primary button">
 						{{if .LinkAccountMode}}
 							{{ctx.Locale.Tr "auth.oauth_signup_submit"}}
@@ -50,7 +49,6 @@
 
 				{{if not .LinkAccountMode}}
 				<div class="inline field">
-					<label></label>
 					<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.register_helper_msg"}}</a>
 				</div>
 				{{end}}
@@ -64,7 +62,7 @@
 				<div class="gt-df gt-fc gt-jc">
 					<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
 						{{range $provider := .OAuth2Providers}}
-							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
 								{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 							</a>
diff --git a/templates/user/auth/signup_openid_register.tmpl b/templates/user/auth/signup_openid_register.tmpl
index 81c36957d1..c017a0e65b 100644
--- a/templates/user/auth/signup_openid_register.tmpl
+++ b/templates/user/auth/signup_openid_register.tmpl
@@ -7,28 +7,27 @@
 					{{ctx.Locale.Tr "auth.openid_register_title"}}
 				</h4>
 				<div class="ui attached segment">
-					<p>
+					<p class="tw-max-w-2xl tw-mx-auto">
 						{{ctx.Locale.Tr "auth.openid_register_desc"}}
 					</p>
 					<form class="ui form" action="{{.Link}}" method="post">
 					{{.CsrfTokenHtml}}
-					<div class="required inline field {{if .Err_UserName}}error{{end}}">
+					<div class="required field {{if .Err_UserName}}error{{end}}">
 						<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
 						<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 					</div>
-					<div class="required inline field {{if .Err_Email}}error{{end}}">
+					<div class="required field {{if .Err_Email}}error{{end}}">
 						<label for="email">{{ctx.Locale.Tr "email"}}</label>
 						<input id="email" name="email" type="email" value="{{.email}}" required>
 					</div>
 
 					{{template "user/auth/captcha" .}}
 
-					<div class="inline field">
+					<div class="field">
 						<label for="openid">OpenID URI</label>
 						<input id="openid" value="{{.OpenID}}" readonly>
 					</div>
 					<div class="inline field">
-						<label></label>
 						<button class="ui primary button">{{ctx.Locale.Tr "auth.create_new_account"}}</button>
 					</div>
 					</form>
diff --git a/templates/user/auth/twofa.tmpl b/templates/user/auth/twofa.tmpl
index 5260178d13..d245239171 100644
--- a/templates/user/auth/twofa.tmpl
+++ b/templates/user/auth/twofa.tmpl
@@ -2,20 +2,19 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user signin">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form" action="{{.Link}}" method="post">
+			<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<h3 class="ui top attached header">
 					{{ctx.Locale.Tr "twofa"}}
 				</h3>
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
-					<div class="required inline field">
+					<div class="required field">
 						<label for="passcode">{{ctx.Locale.Tr "passcode"}}</label>
 						<input id="passcode" name="passcode" type="text" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" autofocus required>
 					</div>
 
 					<div class="inline field">
-						<label></label>
 						<button class="ui primary button">{{ctx.Locale.Tr "auth.verify"}}</button>
 						<a href="{{AppSubUrl}}/user/two_factor/scratch">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
 					</div>
diff --git a/templates/user/auth/twofa_scratch.tmpl b/templates/user/auth/twofa_scratch.tmpl
index 1aa044b4a5..23ad77f2a9 100644
--- a/templates/user/auth/twofa_scratch.tmpl
+++ b/templates/user/auth/twofa_scratch.tmpl
@@ -2,20 +2,19 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user signin">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form" action="{{.Link}}" method="post">
+			<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<h3 class="ui top attached header">
 					{{ctx.Locale.Tr "twofa_scratch"}}
 				</h3>
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
-					<div class="required inline field">
+					<div class="required field">
 						<label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label>
 						<input id="token" name="token" type="text" autocomplete="off" autofocus required>
 					</div>
 
 					<div class="inline field">
-						<label></label>
 						<button class="ui primary button">{{ctx.Locale.Tr "auth.verify"}}</button>
 					</div>
 				</div>
diff --git a/web_src/css/form.css b/web_src/css/form.css
index e4efa34948..1580a0b4cc 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -239,11 +239,8 @@ textarea:focus,
   }
 }
 
-.user.activate form,
 .user.forgot.password form,
 .user.reset.password form,
-.user.link-account form,
-.user.signin form,
 .user.signup form {
   margin: auto;
   width: 700px !important;
@@ -275,12 +272,7 @@ textarea:focus,
   .user.signup form .header {
     padding-left: 280px !important;
   }
-  .user.activate form .inline.field > label,
-  .user.forgot.password form .inline.field > label,
-  .user.reset.password form .inline.field > label,
-  .user.link-account form .inline.field > label,
-  .user.signin form .inline.field > label,
-  .user.signup form .inline.field > label {
+  .user.activate form .inline.field > label {
     text-align: right;
     width: 250px !important;
     word-wrap: break-word;

From f6d01ac2d89b326edd1169f25a5e946f025b0eb8 Mon Sep 17 00:00:00 2001
From: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Date: Thu, 7 Mar 2024 00:10:06 +0800
Subject: [PATCH 299/679] Add download URL for executable files (#28260)

Consider executable files as a valid case when returning a DownloadURL for them.
They are just regular files with the difference being the executable permission bit being set.

Co-authored-by: Gusted <postmaster@gusted.xyz>
---
 services/repository/files/content.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index 9500b8f46d..95e7c7087c 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -220,7 +220,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
 		}
 	}
 	// Handle links
-	if entry.IsRegular() || entry.IsLink() {
+	if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
 		downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
 		if err != nil {
 			return nil, err

From 16f13265143ff08cb6c33e976998b262e94fe569 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 6 Mar 2024 22:44:24 +0100
Subject: [PATCH 300/679] Tweak actions color and borders (#29640)

- Increase contrast overall
- Unalias the ansi color in dark theme and copy them to light
- Add outer border
- Add border radius

<img width="1337" alt="Screenshot 2024-03-06 at 22 30 03"
src="https://github.com/go-gitea/gitea/assets/115237/11407c0f-0bb2-435e-a034-22b1f106d9b0">
<img width="1335" alt="Screenshot 2024-03-06 at 22 36 59"
src="https://github.com/go-gitea/gitea/assets/115237/267db442-0979-4acc-a79e-8579b4cb0262">
---
 web_src/css/themes/theme-gitea-dark.css  | 36 ++++++++++----------
 web_src/css/themes/theme-gitea-light.css | 42 ++++++++++++------------
 web_src/js/components/RepoActionView.vue |  8 +++--
 3 files changed, 44 insertions(+), 42 deletions(-)

diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index ac77b7bbd9..c187888a38 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -61,10 +61,10 @@
   --color-secondary-hover: var(--color-secondary-dark-3);
   --color-secondary-active: var(--color-secondary-dark-2);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #d9dde2;
-  --color-console-fg-subtle: #95989c;
-  --color-console-bg: #1c2023;
-  --color-console-border: #272b2e;
+  --color-console-fg: #f8f8f9;
+  --color-console-fg-subtle: #bec4c8;
+  --color-console-bg: #181b1d;
+  --color-console-border: #313538;
   --color-console-hover-bg: #ffffff16;
   --color-console-active-bg: #313538;
   --color-console-menu-bg: #272b2e;
@@ -122,21 +122,21 @@
   --color-brown-dark-2: #835b42;
   --color-black-dark-2: #292a2e;
   /* ansi colors used for actions console and console files */
-  --color-ansi-black: var(--color-black);
-  --color-ansi-red: var(--color-red);
-  --color-ansi-green: var(--color-green);
-  --color-ansi-yellow: var(--color-yellow);
-  --color-ansi-blue: var(--color-blue);
-  --color-ansi-magenta: var(--color-pink);
-  --color-ansi-cyan: var(--color-teal);
+  --color-ansi-black: #1f2326;
+  --color-ansi-red: #cc4848;
+  --color-ansi-green: #87ab63;
+  --color-ansi-yellow: #cc9903;
+  --color-ansi-blue: #3a8ac6;
+  --color-ansi-magenta: #d22e8b;
+  --color-ansi-cyan: #00918a;
   --color-ansi-white: var(--color-console-fg-subtle);
-  --color-ansi-bright-black: var(--color-black-light);
-  --color-ansi-bright-red: var(--color-red-light);
-  --color-ansi-bright-green: var(--color-green-light);
-  --color-ansi-bright-yellow: var(--color-yellow-light);
-  --color-ansi-bright-blue: var(--color-blue-light);
-  --color-ansi-bright-magenta: var(--color-pink-light);
-  --color-ansi-bright-cyan: var(--color-teal-light);
+  --color-ansi-bright-black: #46494d;
+  --color-ansi-bright-red: #d15a5a;
+  --color-ansi-bright-green: #93b373;
+  --color-ansi-bright-yellow: #eaaf03;
+  --color-ansi-bright-blue: #4e96cc;
+  --color-ansi-bright-magenta: #d74397;
+  --color-ansi-bright-cyan: #00b6ad;
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
   --color-grey: #3c4043;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 5c375712d8..5137e0774c 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -61,14 +61,14 @@
   --color-secondary-hover: var(--color-secondary-dark-5);
   --color-secondary-active: var(--color-secondary-dark-6);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #eeeff2;
-  --color-console-fg-subtle: #959cab;
-  --color-console-bg: #262936;
-  --color-console-border: #383c47;
+  --color-console-fg: #f8f8f9;
+  --color-console-fg-subtle: #bec4c8;
+  --color-console-bg: #181b1d;
+  --color-console-border: #313538;
   --color-console-hover-bg: #ffffff16;
-  --color-console-active-bg: #454a57;
-  --color-console-menu-bg: #383c47;
-  --color-console-menu-border: #5c6374;
+  --color-console-active-bg: #313538;
+  --color-console-menu-bg: #272b2e;
+  --color-console-menu-border: #464a4d;
   /* named colors */
   --color-red: #db2828;
   --color-orange: #f2711c;
@@ -122,21 +122,21 @@
   --color-brown-dark-2: #845232;
   --color-black-dark-2: #161617;
   /* ansi colors used for actions console and console files */
-  --color-ansi-black: var(--color-black);
-  --color-ansi-red: var(--color-red);
-  --color-ansi-green: var(--color-green);
-  --color-ansi-yellow: var(--color-yellow);
-  --color-ansi-blue: var(--color-blue);
-  --color-ansi-magenta: var(--color-pink);
-  --color-ansi-cyan: var(--color-teal);
+  --color-ansi-black: #1f2326;
+  --color-ansi-red: #cc4848;
+  --color-ansi-green: #87ab63;
+  --color-ansi-yellow: #cc9903;
+  --color-ansi-blue: #3a8ac6;
+  --color-ansi-magenta: #d22e8b;
+  --color-ansi-cyan: #00918a;
   --color-ansi-white: var(--color-console-fg-subtle);
-  --color-ansi-bright-black: var(--color-black-light);
-  --color-ansi-bright-red: var(--color-red-light);
-  --color-ansi-bright-green: var(--color-green-light);
-  --color-ansi-bright-yellow: var(--color-yellow-light);
-  --color-ansi-bright-blue: var(--color-blue-light);
-  --color-ansi-bright-magenta: var(--color-pink-light);
-  --color-ansi-bright-cyan: var(--color-teal-light);
+  --color-ansi-bright-black: #46494d;
+  --color-ansi-bright-red: #d15a5a;
+  --color-ansi-bright-green: #93b373;
+  --color-ansi-bright-yellow: #eaaf03;
+  --color-ansi-bright-blue: #4e96cc;
+  --color-ansi-bright-magenta: #d74397;
+  --color-ansi-bright-cyan: #00b6ad;
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
   --color-grey: #707070;
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 3801848519..97cd05b45b 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -622,6 +622,8 @@ export function initRepositoryActionView() {
   width: 70%;
   display: flex;
   flex-direction: column;
+  border: 1px solid var(--color-console-border);
+  border-radius: var(--border-radius);
 }
 
 /* begin fomantic button overrides */
@@ -681,7 +683,6 @@ export function initRepositoryActionView() {
   justify-content: space-between;
   align-items: center;
   padding: 0 12px;
-  border-bottom: 1px solid var(--color-console-border);
   background-color: var(--color-console-bg);
   position: sticky;
   top: 0;
@@ -705,6 +706,7 @@ export function initRepositoryActionView() {
   background-color: var(--color-console-bg);
   max-height: 100%;
   border-radius: 0 0 var(--border-radius) var(--border-radius);
+  border-top: 1px solid var(--color-console-border);
   z-index: 0;
 }
 
@@ -790,7 +792,7 @@ export function initRepositoryActionView() {
 /* class names 'log-time-seconds' and 'log-time-stamp' are used in the method toggleTimeDisplay */
 .job-log-line .line-num, .log-time-seconds {
   width: 48px;
-  color: var(--color-grey-light);
+  color: var(--color-text-light-3);
   text-align: right;
   user-select: none;
 }
@@ -806,7 +808,7 @@ export function initRepositoryActionView() {
 
 .job-log-line .log-time,
 .log-time-stamp {
-  color: var(--color-grey-light);
+  color: var(--color-text-light-3);
   margin-left: 10px;
   white-space: nowrap;
 }

From 9730d3a9af889f27a1026045ab650710a01841e5 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 6 Mar 2024 23:20:05 +0100
Subject: [PATCH 301/679] Update various logos and unify their filenames
 (#29637)

1. Checked all logos, updated 3 of them to newer versions.
2. Remove `open-with-` infix on the editor logos to be consistent with
other files.

<img width="626" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/3b2d9486-6e0a-45c6-b0e4-d38dc4c0b118">
---
 public/assets/img/svg/gitea-bitbucket.svg                       | 2 +-
 public/assets/img/svg/gitea-facebook.svg                        | 2 +-
 public/assets/img/svg/gitea-jetbrains.svg                       | 1 +
 public/assets/img/svg/gitea-microsoftonline.svg                 | 2 +-
 public/assets/img/svg/gitea-open-with-jetbrains.svg             | 1 -
 public/assets/img/svg/gitea-open-with-vscode.svg                | 1 -
 public/assets/img/svg/gitea-open-with-vscodium.svg              | 1 -
 public/assets/img/svg/gitea-vscode.svg                          | 1 +
 public/assets/img/svg/gitea-vscodium.svg                        | 1 +
 routers/web/repo/view.go                                        | 2 +-
 web_src/svg/gitea-bitbucket.svg                                 | 2 +-
 web_src/svg/gitea-facebook.svg                                  | 2 +-
 .../svg/{gitea-open-with-jetbrains.svg => gitea-jetbrains.svg}  | 0
 web_src/svg/gitea-microsoftonline.svg                           | 2 +-
 web_src/svg/{gitea-open-with-vscode.svg => gitea-vscode.svg}    | 0
 .../svg/{gitea-open-with-vscodium.svg => gitea-vscodium.svg}    | 0
 16 files changed, 10 insertions(+), 10 deletions(-)
 create mode 100644 public/assets/img/svg/gitea-jetbrains.svg
 delete mode 100644 public/assets/img/svg/gitea-open-with-jetbrains.svg
 delete mode 100644 public/assets/img/svg/gitea-open-with-vscode.svg
 delete mode 100644 public/assets/img/svg/gitea-open-with-vscodium.svg
 create mode 100644 public/assets/img/svg/gitea-vscode.svg
 create mode 100644 public/assets/img/svg/gitea-vscodium.svg
 rename web_src/svg/{gitea-open-with-jetbrains.svg => gitea-jetbrains.svg} (100%)
 rename web_src/svg/{gitea-open-with-vscode.svg => gitea-vscode.svg} (100%)
 rename web_src/svg/{gitea-open-with-vscodium.svg => gitea-vscodium.svg} (100%)

diff --git a/public/assets/img/svg/gitea-bitbucket.svg b/public/assets/img/svg/gitea-bitbucket.svg
index b900335ea1..83e4c5c6e7 100644
--- a/public/assets/img/svg/gitea-bitbucket.svg
+++ b/public/assets/img/svg/gitea-bitbucket.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-bitbucket__svg gitea-bitbucket__gitea-bitbucket svg gitea-bitbucket" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 295" width="16" height="16"><g fill="#205081"><path d="M128 0C57.732 0 .012 18.822.012 42.663c0 6.274 15.057 95.364 21.331 130.498 2.51 16.312 43.918 38.898 106.657 38.898 62.74 0 102.893-22.586 106.657-38.898 6.274-35.134 21.331-124.224 21.331-130.498C254.734 18.822 198.268 0 128 0m0 183.199c-22.586 0-40.153-17.567-40.153-40.153s17.567-40.153 40.153-40.153 40.153 17.567 40.153 40.153c0 21.331-17.567 40.153-40.153 40.153m0-127.988c-45.172 0-81.561-7.53-81.561-17.567 0-10.039 36.389-17.567 81.561-17.567s81.561 7.528 81.561 17.567c0 10.038-36.389 17.567-81.561 17.567"/><path d="M220.608 207.04c-2.51 0-3.764 1.255-3.764 1.255s-31.37 25.096-87.835 25.096c-56.466 0-87.835-25.096-87.835-25.096s-2.51-1.255-3.765-1.255c-2.51 0-5.019 1.255-5.019 5.02v1.254c5.02 26.35 8.784 45.172 8.784 47.682 3.764 18.822 41.408 33.88 86.58 33.88s82.816-15.058 86.58-33.88c0-2.51 3.765-21.332 8.784-47.682v-1.255c1.255-2.51 0-5.019-2.51-5.019"/><circle cx="128" cy="141.791" r="20.077"/></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42" class="svg gitea-bitbucket" width="16" height="16" aria-hidden="true"><defs><linearGradient id="gitea-bitbucket__a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#gitea-bitbucket__a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.7 2.7 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-facebook.svg b/public/assets/img/svg/gitea-facebook.svg
index cbeb76b127..6101becad2 100644
--- a/public/assets/img/svg/gitea-facebook.svg
+++ b/public/assets/img/svg/gitea-facebook.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-facebook__svg gitea-facebook__gitea-facebook svg gitea-facebook" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" width="16" height="16"><path fill="#395b97" d="M93.5 8.5q-2.177 1.203-5 1.5L10 88.5q-.297 2.823-1.5 5a552 552 0 0 1-.5-56Q11.75 11.75 37.5 8a552 552 0 0 1 56 .5" style="opacity:.995"/><path fill="#366098" d="M93.5 8.5q23.832 6.337 26 31a677 677 0 0 0-1.5 37l-35 35a32.4 32.4 0 0 0-.5 8 442 442 0 0 1-1-42h14a380 380 0 0 0 3-17h-17q-3.75-20.745 17-18v-16q-38.632-4.865-33 34h-14v17h14v42q-14.01.25-28-.5-23.177-2.93-29-25.5 1.203-2.177 1.5-5L88.5 10q2.823-.297 5-1.5" style="opacity:.976"/><path fill="#346499" d="M119.5 39.5q.25 25.005-.5 50-5.432 30.368-36.5 30a32.4 32.4 0 0 1 .5-8l35-35q.254-18.76 1.5-37" style="opacity:.918"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222" class="svg gitea-facebook" width="16" height="16" aria-hidden="true"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-jetbrains.svg b/public/assets/img/svg/gitea-jetbrains.svg
new file mode 100644
index 0000000000..5821736225
--- /dev/null
+++ b/public/assets/img/svg/gitea-jetbrains.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-jetbrains__a)"/><linearGradient id="gitea-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-jetbrains__b)"/><linearGradient id="gitea-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-jetbrains__c)"/><linearGradient id="gitea-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-microsoftonline.svg b/public/assets/img/svg/gitea-microsoftonline.svg
index ce4f1a5c8f..f2ce13ac22 100644
--- a/public/assets/img/svg/gitea-microsoftonline.svg
+++ b/public/assets/img/svg/gitea-microsoftonline.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-microsoftonline__svg gitea-microsoftonline__gitea-microsoftonline svg gitea-microsoftonline" viewBox="0 0 2075 2499.8" width="16" height="16"><path fill="#eb3c00" d="M0 2016.6V496.8L1344.4 0 2075 233.7v2045.9l-730.6 220.3zl1344.4 161.8V409.2L467.6 613.8v1198.3z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48" class="svg gitea-microsoftonline" width="16" height="16" aria-hidden="true"><path fill="url(#gitea-microsoftonline__a)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__b)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__g)" d="M4.004 30.998"/><path fill="url(#gitea-microsoftonline__h)" d="M4.004 30.998"/><defs><radialGradient id="gitea-microsoftonline__a" cx="0" cy="0" r="1" gradientTransform="rotate(110.528 5.021 11.358)scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="gitea-microsoftonline__c" cx="0" cy="0" r="1" gradientTransform="matrix(30.7198 -4.51832 2.98465 20.29248 10.43 36.351)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="gitea-microsoftonline__e" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="gitea-microsoftonline__g" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="gitea-microsoftonline__b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-open-with-jetbrains.svg b/public/assets/img/svg/gitea-open-with-jetbrains.svg
deleted file mode 100644
index 2b1491b541..0000000000
--- a/public/assets/img/svg/gitea-open-with-jetbrains.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-open-with-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-open-with-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-open-with-jetbrains__a)"/><linearGradient id="gitea-open-with-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__b)"/><linearGradient id="gitea-open-with-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__c)"/><linearGradient id="gitea-open-with-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-open-with-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-open-with-vscode.svg b/public/assets/img/svg/gitea-open-with-vscode.svg
deleted file mode 100644
index 151c45e210..0000000000
--- a/public/assets/img/svg/gitea-open-with-vscode.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-open-with-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-open-with-vscodium.svg b/public/assets/img/svg/gitea-open-with-vscodium.svg
deleted file mode 100644
index 9f70878ba6..0000000000
--- a/public/assets/img/svg/gitea-open-with-vscodium.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-open-with-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-vscode.svg b/public/assets/img/svg/gitea-vscode.svg
new file mode 100644
index 0000000000..453b9befcc
--- /dev/null
+++ b/public/assets/img/svg/gitea-vscode.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-vscodium.svg b/public/assets/img/svg/gitea-vscodium.svg
new file mode 100644
index 0000000000..6aad3d3a64
--- /dev/null
+++ b/public/assets/img/svg/gitea-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index d47c926fa1..712d12705e 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -919,7 +919,7 @@ func prepareOpenWithEditorApps(ctx *context.Context) {
 		schema, _, _ := strings.Cut(app.OpenURL, ":")
 		var iconHTML template.HTML
 		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
-			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-open-with-%s", schema), 16, "gt-mr-3")
+			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "gt-mr-3")
 		} else {
 			iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future
 		}
diff --git a/web_src/svg/gitea-bitbucket.svg b/web_src/svg/gitea-bitbucket.svg
index d3b15a9dc6..ac490c944f 100644
--- a/web_src/svg/gitea-bitbucket.svg
+++ b/web_src/svg/gitea-bitbucket.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 295" class="svg gitea-bitbucket" width="16" height="16" aria-hidden="true"><g fill="#205081"><path d="M128 0C57.732 0 .012 18.822.012 42.663c0 6.274 15.057 95.364 21.331 130.498 2.51 16.312 43.918 38.898 106.657 38.898 62.74 0 102.893-22.586 106.657-38.898 6.274-35.134 21.331-124.224 21.331-130.498C254.734 18.822 198.268 0 128 0zm0 183.199c-22.586 0-40.153-17.567-40.153-40.153s17.567-40.153 40.153-40.153 40.153 17.567 40.153 40.153c0 21.331-17.567 40.153-40.153 40.153zm0-127.988c-45.172 0-81.561-7.53-81.561-17.567 0-10.039 36.389-17.567 81.561-17.567 45.172 0 81.561 7.528 81.561 17.567 0 10.038-36.389 17.567-81.561 17.567z"/><path d="M220.608 207.04c-2.51 0-3.764 1.255-3.764 1.255s-31.37 25.096-87.835 25.096c-56.466 0-87.835-25.096-87.835-25.096s-2.51-1.255-3.765-1.255c-2.51 0-5.019 1.255-5.019 5.02v1.254c5.02 26.35 8.784 45.172 8.784 47.682 3.764 18.822 41.408 33.88 86.58 33.88s82.816-15.058 86.58-33.88c0-2.51 3.765-21.332 8.784-47.682v-1.255c1.255-2.51 0-5.019-2.51-5.019z"/><circle cx="128" cy="141.791" r="20.077"/></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42"><defs><linearGradient id="a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.71 2.71 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-facebook.svg b/web_src/svg/gitea-facebook.svg
index 8163e2a966..68cd20750a 100644
--- a/web_src/svg/gitea-facebook.svg
+++ b/web_src/svg/gitea-facebook.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" class="svg gitea-facebook" width="16" height="16" aria-hidden="true"><path fill="#395b97" d="M93.5 8.5c-1.452.802-3.118 1.302-5 1.5L10 88.5c-.198 1.882-.698 3.548-1.5 5a551.581 551.581 0 0 1-.5-56c2.5-17.167 12.333-27 29.5-29.5a551.581 551.581 0 0 1 56 .5Z" style="opacity:.995"/><path fill="#366098" d="M93.5 8.5c15.888 4.225 24.555 14.558 26 31a676.749 676.749 0 0 0-1.5 37l-35 35a32.438 32.438 0 0 0-.5 8 441.615 441.615 0 0 1-1-42h14a379.883 379.883 0 0 0 3-17h-17c-2.5-13.83 3.166-19.83 17-18v-16c-25.755-3.243-36.755 8.09-33 34h-14v17h14v42c-9.34.166-18.673 0-28-.5-15.451-1.953-25.118-10.453-29-25.5.802-1.452 1.302-3.118 1.5-5L88.5 10c1.882-.198 3.548-.698 5-1.5Z" style="opacity:.976"/><path fill="#346499" d="M119.5 39.5c.167 16.67 0 33.337-.5 50-3.622 20.245-15.788 30.245-36.5 30a32.438 32.438 0 0 1 .5-8l35-35c.169-12.507.669-24.84 1.5-37Z" style="opacity:.918"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025z"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167h1657z"/></g></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-open-with-jetbrains.svg b/web_src/svg/gitea-jetbrains.svg
similarity index 100%
rename from web_src/svg/gitea-open-with-jetbrains.svg
rename to web_src/svg/gitea-jetbrains.svg
diff --git a/web_src/svg/gitea-microsoftonline.svg b/web_src/svg/gitea-microsoftonline.svg
index 72ef94eabb..eb28296419 100644
--- a/web_src/svg/gitea-microsoftonline.svg
+++ b/web_src/svg/gitea-microsoftonline.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2075 2499.8" class="svg gitea-microsoftonline" width="16" height="16" aria-hidden="true"><path fill="#eb3c00" d="M0 2016.6V496.8L1344.4 0 2075 233.7v2045.9l-730.6 220.3L0 2016.6l1344.4 161.8V409.2L467.6 613.8v1198.3z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48"><path fill="url(#a)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#b)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#g)" d="M4.004 30.998Z"/><path fill="url(#h)" d="M4.004 30.998Z"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="translate(17.4186 10.6383) rotate(110.528) scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="translate(10.4299 36.3511) rotate(-8.36717) scale(31.0503 20.5108)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-open-with-vscode.svg b/web_src/svg/gitea-vscode.svg
similarity index 100%
rename from web_src/svg/gitea-open-with-vscode.svg
rename to web_src/svg/gitea-vscode.svg
diff --git a/web_src/svg/gitea-open-with-vscodium.svg b/web_src/svg/gitea-vscodium.svg
similarity index 100%
rename from web_src/svg/gitea-open-with-vscodium.svg
rename to web_src/svg/gitea-vscodium.svg

From c72e1a7abbba0cca34131a86273c987c47065dd0 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 7 Mar 2024 10:03:41 +0800
Subject: [PATCH 302/679] Use strict protocol check when redirect (#29642)

---
 services/context/base.go      |  2 +-
 services/context/base_test.go | 47 +++++++++++++++++++++++++++++++++++
 2 files changed, 48 insertions(+), 1 deletion(-)
 create mode 100644 services/context/base_test.go

diff --git a/services/context/base.go b/services/context/base.go
index c4aa467ff4..62fb743714 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -256,7 +256,7 @@ func (b *Base) Redirect(location string, status ...int) {
 		code = status[0]
 	}
 
-	if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
+	if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") || strings.HasPrefix(location, "//") {
 		// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
 		// 1. the first request to "/my-path" contains cookie
 		// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
diff --git a/services/context/base_test.go b/services/context/base_test.go
new file mode 100644
index 0000000000..823f20e00b
--- /dev/null
+++ b/services/context/base_test.go
@@ -0,0 +1,47 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRedirect(t *testing.T) {
+	req, _ := http.NewRequest("GET", "/", nil)
+
+	cases := []struct {
+		url  string
+		keep bool
+	}{
+		{"http://test", false},
+		{"https://test", false},
+		{"//test", false},
+		{"/://test", true},
+		{"/test", true},
+	}
+	for _, c := range cases {
+		resp := httptest.NewRecorder()
+		b, cleanup := NewBaseContext(resp, req)
+		resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String())
+		b.Redirect(c.url)
+		cleanup()
+		has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
+		assert.Equal(t, c.keep, has, "url = %q", c.url)
+	}
+
+	req, _ = http.NewRequest("GET", "/", nil)
+	resp := httptest.NewRecorder()
+	req.Header.Add("HX-Request", "true")
+	b, cleanup := NewBaseContext(resp, req)
+	b.Redirect("/other")
+	cleanup()
+	assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
+	assert.Equal(t, http.StatusNoContent, resp.Code)
+}

From c1331d1f7ab60249ed2f080b24f3e32093fa708d Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Thu, 7 Mar 2024 09:28:33 +0200
Subject: [PATCH 303/679] Remove jQuery AJAX from the repo editor (#29636)

# Preview Tab
- Removed the jQuery AJAX call and replaced with our fetch wrapper
- Tested the preview tab functionality and it works as before

# Diff Tab
- Removed the jQuery AJAX call and replaced with htmx
- Tested the diff tab functionality and it works as before

## htmx Attributes
- `hx-post="{{.RepoLink}}..."`: make a POST request to the endpoint
- `hx-indicator=".tab[data-tab='diff']"`: attach the loading indicator
to the tab body
- `hx-target=".tab[data-tab='diff']"`: target the tab body for swapping
with the response
- `hx-swap="innerHTML"`: swap the target's inner HTML
- `hx-include="#edit_area"`: include the value of the textarea (content)
in the request body
- `hx-vals='{"context":"{{.BranchLink}}"}'`: include the context in the
request body
- `hx-params="context,content"`: include only these keys in the request
body

# Demo using `fetch` and `htmx` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/585cd6e8-f329-4c9e-ab53-a540acbd7988)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 templates/repo/editor/edit.tmpl    |  4 +--
 web_src/js/features/repo-editor.js | 42 ++++++++++--------------------
 2 files changed, 16 insertions(+), 30 deletions(-)

diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index a6dce81c08..05a8d96681 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -30,7 +30,7 @@
 					<a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a>
 					<a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-markup-mode="file">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a>
 					{{if not .IsNewFile}}
-					<a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}" data-context="{{.BranchLink}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a>
+					<a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a>
 					{{end}}
 				</div>
 				<div class="ui bottom attached active tab segment" data-tab="write">
@@ -45,7 +45,7 @@
 					{{ctx.Locale.Tr "loading"}}
 				</div>
 				<div class="ui bottom attached tab segment diff edit-diff" data-tab="diff">
-					{{ctx.Locale.Tr "loading"}}
+					<div class="tw-p-16"></div>
 				</div>
 			</div>
 			{{template "repo/editor/commit_form" .}}
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index f00f817223..4fe7ed8a4d 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -4,15 +4,14 @@ import {createCodeEditor} from './codeeditor.js';
 import {hideElem, showElem} from '../utils/dom.js';
 import {initMarkupContent} from '../markup/content.js';
 import {attachRefIssueContextPopup} from './contextpopup.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
 
 function initEditPreviewTab($form) {
   const $tabMenu = $form.find('.tabular.menu');
   $tabMenu.find('.item').tab();
   const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`);
   if ($previewTab.length) {
-    $previewTab.on('click', function () {
+    $previewTab.on('click', async function () {
       const $this = $(this);
       let context = `${$this.data('context')}/`;
       const mode = $this.data('markup-mode') || 'comment';
@@ -21,43 +20,30 @@ function initEditPreviewTab($form) {
         context += treePathEl.val();
       }
       context = context.substring(0, context.lastIndexOf('/'));
-      $.post($this.data('url'), {
-        _csrf: csrfToken,
-        mode,
-        context,
-        text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
-        file_path: treePathEl.val(),
-      }, (data) => {
+
+      const formData = new FormData();
+      formData.append('mode', mode);
+      formData.append('context', context);
+      formData.append('text', $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val());
+      formData.append('file_path', treePathEl.val());
+      try {
+        const response = await POST($this.data('url'), {data: formData});
+        const data = await response.text();
         const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`);
         renderPreviewPanelContent($previewPanel, data);
-      });
+      } catch (error) {
+        console.error('Error:', error);
+      }
     });
   }
 }
 
-function initEditDiffTab($form) {
-  const $tabMenu = $form.find('.tabular.menu');
-  $tabMenu.find('.item').tab();
-  $tabMenu.find(`.item[data-tab="${$tabMenu.data('diff')}"]`).on('click', function () {
-    const $this = $(this);
-    $.post($this.data('url'), {
-      _csrf: csrfToken,
-      context: $this.data('context'),
-      content: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
-    }, (data) => {
-      const $diffPreviewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('diff')}"]`);
-      $diffPreviewPanel.html(data);
-    });
-  });
-}
-
 function initEditorForm() {
   if ($('.repository .edit.form').length === 0) {
     return;
   }
 
   initEditPreviewTab($('.repository .edit.form'));
-  initEditDiffTab($('.repository .edit.form'));
 }
 
 function getCursorPosition($e) {

From 45277486c2c6213b7766b1da708a991cdb1f3565 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 8 Mar 2024 00:43:32 +0800
Subject: [PATCH 304/679] Fix bug hidden on CI and make ci failed if tests
 failure (#29254)

The tests on migration tests failed but CI reports successfully


https://github.com/go-gitea/gitea/actions/runs/7364373807/job/20044685969#step:8:141

This PR will fix the bug on migration v283 and also the CI hidden
behaviour.

The reason is on the Makefile

`GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test
$(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(MIGRATE_TEST_PACKAGES)` will
return the error exit code.

But

`for pkg in $(shell $(GO) list
code.gitea.io/gitea/models/migrations/...); do \
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test
$(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
	done`

will not work.

This also fix #29602
---
 .github/workflows/pull-db-tests.yml           | 18 +++--
 Makefile                                      | 18 ++---
 models/migrations/base/db_test.go             |  5 +-
 .../Test_AddIssueResourceIndexTable/issue.yml |  4 ++
 .../attachment.yml                            | 11 ++++
 .../Test_AddRepoIDForAttachment/issue.yml     |  3 +
 .../Test_AddRepoIDForAttachment/release.yml   |  3 +
 .../Test_RepositoryFormat/comment.yml         |  3 +
 .../Test_RepositoryFormat/commit_status.yml   |  3 +
 .../Test_RepositoryFormat/pull_request.yml    |  5 ++
 .../Test_RepositoryFormat/release.yml         |  3 +
 .../Test_RepositoryFormat/repo_archiver.yml   |  3 +
 .../repo_indexer_status.yml                   |  3 +
 .../Test_RepositoryFormat/review_state.yml    |  3 +
 .../Test_UpdateBadgeColName/badge.yml         |  4 ++
 models/migrations/v1_16/v193_test.go          | 26 +++++---
 models/migrations/v1_22/v283.go               | 17 ++++-
 models/migrations/v1_22/v286.go               |  8 +--
 models/migrations/v1_22/v286_test.go          | 66 +++++++++++++++++--
 models/migrations/v1_22/v287_test.go          | 14 ++--
 20 files changed, 174 insertions(+), 46 deletions(-)
 create mode 100644 models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml
 create mode 100644 models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml
 create mode 100644 models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml
 create mode 100644 models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml
 create mode 100644 models/migrations/fixtures/Test_RepositoryFormat/comment.yml
 create mode 100644 models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml
 create mode 100644 models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml
 create mode 100644 models/migrations/fixtures/Test_RepositoryFormat/release.yml
 create mode 100644 models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml
 create mode 100644 models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml
 create mode 100644 models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
 create mode 100644 models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml

diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index a3886bf618..61c0391509 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -49,7 +49,10 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata
-      - run: make test-pgsql-migration test-pgsql
+      - name: run migration tests
+        run: make test-pgsql-migration
+      - name: run tests
+        run: make test-pgsql
         timeout-minutes: 50
         env:
           TAGS: bindata gogit
@@ -72,7 +75,10 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata gogit sqlite sqlite_unlock_notify
-      - run: make test-sqlite-migration test-sqlite
+      - name: run migration tests
+        run: make test-sqlite-migration
+      - name: run tests
+        run: make test-sqlite
         timeout-minutes: 50
         env:
           TAGS: bindata gogit sqlite sqlite_unlock_notify
@@ -175,8 +181,10 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata
+      - name: run migration tests
+        run: make test-mysql-migration
       - name: run tests
-        run: make test-mysql-migration integration-test-coverage
+        run: make integration-test-coverage
         env:
           TAGS: bindata
           RACE_ENABLED: true
@@ -208,7 +216,9 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata
-      - run: make test-mssql-migration test-mssql
+      - run: make test-mssql-migration
+      - name: run tests
+        run: make test-mssql
         timeout-minutes: 50
         env:
           TAGS: bindata
diff --git a/Makefile b/Makefile
index 52357ba00d..5ab8655c2f 100644
--- a/Makefile
+++ b/Makefile
@@ -115,6 +115,7 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
 
 GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
 GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
+MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
 
 FOMANTIC_WORK_DIR := web_src/fomantic
 
@@ -710,9 +711,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
 
 .PHONY: migrations.individual.mysql.test
 migrations.individual.mysql.test: $(GO_SOURCES)
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.sqlite.test\#%
 migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
@@ -720,20 +719,15 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
 
 .PHONY: migrations.individual.pgsql.test
 migrations.individual.pgsql.test: $(GO_SOURCES)
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.pgsql.test\#%
 migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql
 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
 
-
 .PHONY: migrations.individual.mssql.test
 migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg -test.failfast; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.mssql.test\#%
 migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
@@ -741,9 +735,7 @@ migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
 
 .PHONY: migrations.individual.sqlite.test
 migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.sqlite.test\#%
 migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go
index 4d61b758cf..80bf00b22a 100644
--- a/models/migrations/base/db_test.go
+++ b/models/migrations/base/db_test.go
@@ -36,12 +36,14 @@ func Test_DropTableColumns(t *testing.T) {
 		"updated_unix",
 	}
 
+	x.SetMapper(names.GonicMapper{})
+
 	for i := range columns {
-		x.SetMapper(names.GonicMapper{})
 		if err := x.Sync(new(DropTest)); err != nil {
 			t.Errorf("unable to create DropTest table: %v", err)
 			return
 		}
+
 		sess := x.NewSession()
 		if err := sess.Begin(); err != nil {
 			sess.Close()
@@ -64,7 +66,6 @@ func Test_DropTableColumns(t *testing.T) {
 			return
 		}
 		for j := range columns[i+1:] {
-			x.SetMapper(names.GonicMapper{})
 			if err := x.Sync(new(DropTest)); err != nil {
 				t.Errorf("unable to create DropTest table: %v", err)
 				return
diff --git a/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml
new file mode 100644
index 0000000000..f95d47916b
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml
@@ -0,0 +1,4 @@
+-
+  id: 1
+  repo_id: 1
+  index: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml
new file mode 100644
index 0000000000..056236ba9e
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml
@@ -0,0 +1,11 @@
+-
+  id: 1
+  uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
+  issue_id: 1
+  release_id: 0
+
+-
+  id: 2
+  uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12
+  issue_id: 0
+  release_id: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml
new file mode 100644
index 0000000000..7f3255096d
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  repo_id: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml
new file mode 100644
index 0000000000..7f3255096d
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  repo_id: 1
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/comment.yml b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml
new file mode 100644
index 0000000000..ca0aaec4cc
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  context_hash: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml
new file mode 100644
index 0000000000..380cc079ee
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml
@@ -0,0 +1,5 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
+  merge_base: 19fe5caf872476db265596eaac1dc35ad1c6422d
+  merged_commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/release.yml b/models/migrations/fixtures/Test_RepositoryFormat/release.yml
new file mode 100644
index 0000000000..ffabe4ab9e
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/release.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  sha1: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml
new file mode 100644
index 0000000000..f04cb3b340
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml
new file mode 100644
index 0000000000..7025144106
--- /dev/null
+++ b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml
@@ -0,0 +1,4 @@
+-
+  id: 1
+  description: the badge
+  image_url: https://gitea.com/myimage.png
diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go
index 17669a012e..d99bbc2962 100644
--- a/models/migrations/v1_16/v193_test.go
+++ b/models/migrations/v1_16/v193_test.go
@@ -15,7 +15,6 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 	type Attachment struct {
 		ID         int64  `xorm:"pk autoincr"`
 		UUID       string `xorm:"uuid UNIQUE"`
-		RepoID     int64  `xorm:"INDEX"` // this should not be zero
 		IssueID    int64  `xorm:"INDEX"` // maybe zero when creating
 		ReleaseID  int64  `xorm:"INDEX"` // maybe zero when creating
 		UploaderID int64  `xorm:"INDEX DEFAULT 0"`
@@ -44,12 +43,21 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 		return
 	}
 
-	var issueAttachments []*Attachment
-	err := x.Where("issue_id > 0").Find(&issueAttachments)
+	type NewAttachment struct {
+		ID         int64  `xorm:"pk autoincr"`
+		UUID       string `xorm:"uuid UNIQUE"`
+		RepoID     int64  `xorm:"INDEX"` // this should not be zero
+		IssueID    int64  `xorm:"INDEX"` // maybe zero when creating
+		ReleaseID  int64  `xorm:"INDEX"` // maybe zero when creating
+		UploaderID int64  `xorm:"INDEX DEFAULT 0"`
+	}
+
+	var issueAttachments []*NewAttachment
+	err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments)
 	assert.NoError(t, err)
 	for _, attach := range issueAttachments {
-		assert.Greater(t, attach.RepoID, 0)
-		assert.Greater(t, attach.IssueID, 0)
+		assert.Greater(t, attach.RepoID, int64(0))
+		assert.Greater(t, attach.IssueID, int64(0))
 		var issue Issue
 		has, err := x.ID(attach.IssueID).Get(&issue)
 		assert.NoError(t, err)
@@ -57,12 +65,12 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 		assert.EqualValues(t, attach.RepoID, issue.RepoID)
 	}
 
-	var releaseAttachments []*Attachment
-	err = x.Where("release_id > 0").Find(&releaseAttachments)
+	var releaseAttachments []*NewAttachment
+	err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments)
 	assert.NoError(t, err)
 	for _, attach := range releaseAttachments {
-		assert.Greater(t, attach.RepoID, 0)
-		assert.Greater(t, attach.IssueID, 0)
+		assert.Greater(t, attach.RepoID, int64(0))
+		assert.Greater(t, attach.ReleaseID, int64(0))
 		var release Release
 		has, err := x.ID(attach.ReleaseID).Get(&release)
 		assert.NoError(t, err)
diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go
index b2b94845d9..0a45c51245 100644
--- a/models/migrations/v1_22/v283.go
+++ b/models/migrations/v1_22/v283.go
@@ -4,7 +4,10 @@
 package v1_22 //nolint
 
 import (
+	"fmt"
+
 	"xorm.io/xorm"
+	"xorm.io/xorm/schemas"
 )
 
 func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
@@ -20,8 +23,18 @@ func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
 		return err
 	}
 	for _, issueUser := range duplicatedIssueUsers {
-		if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil {
-			return err
+		if x.Dialect().URI().DBType == schemas.MSSQL {
+			if _, err := x.Exec(fmt.Sprintf("delete from issue_user where id in (SELECT top %d id FROM issue_user WHERE issue_id = ? and uid = ?)", issueUser.Cnt-1), issueUser.IssueID, issueUser.UID); err != nil {
+				return err
+			}
+		} else {
+			var ids []int64
+			if err := x.SQL("SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1).Find(&ids); err != nil {
+				return err
+			}
+			if _, err := x.Table("issue_user").In("id", ids).Delete(); err != nil {
+				return err
+			}
 		}
 	}
 
diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go
index ef19f64221..fbbd87344f 100644
--- a/models/migrations/v1_22/v286.go
+++ b/models/migrations/v1_22/v286.go
@@ -36,9 +36,9 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
 		if setting.Database.Type.IsMSSQL() {
 			// drop indexes that need to be re-created afterwards
 			droppedIndexes := []string{
-				"DROP INDEX commit_status.IDX_commit_status_context_hash",
-				"DROP INDEX review_state.UQE_review_state_pull_commit_user",
-				"DROP INDEX repo_archiver.UQE_repo_archiver_s",
+				"DROP INDEX IF EXISTS [IDX_commit_status_context_hash] ON [commit_status]",
+				"DROP INDEX IF EXISTS [UQE_review_state_pull_commit_user] ON [review_state]",
+				"DROP INDEX IF EXISTS [UQE_repo_archiver_s] ON [repo_archiver]",
 			}
 			for _, s := range droppedIndexes {
 				_, err := db.Exec(s)
@@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
 			if setting.Database.Type.IsMySQL() {
 				_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
 			} else if setting.Database.Type.IsMSSQL() {
-				_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
+				_, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1]))
 			} else {
 				_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1]))
 			}
diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go
index e36a18a116..7c353747e3 100644
--- a/models/migrations/v1_22/v286_test.go
+++ b/models/migrations/v1_22/v286_test.go
@@ -17,14 +17,72 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) {
 		ID int64 `xorm:"pk autoincr"`
 	}
 
+	type CommitStatus struct {
+		ID          int64
+		ContextHash string
+	}
+
+	type RepoArchiver struct {
+		ID       int64
+		RepoID   int64
+		Type     int
+		CommitID string
+	}
+
+	type ReviewState struct {
+		ID        int64
+		CommitSHA string
+		UserID    int64
+		PullID    int64
+	}
+
+	type Comment struct {
+		ID        int64
+		CommitSHA string
+	}
+
+	type PullRequest struct {
+		ID             int64
+		CommitSHA      string
+		MergeBase      string
+		MergedCommitID string
+	}
+
+	type Release struct {
+		ID   int64
+		Sha1 string
+	}
+
+	type RepoIndexerStatus struct {
+		ID        int64
+		CommitSHA string
+	}
+
+	type Review struct {
+		ID       int64
+		CommitID string
+	}
+
 	// Prepare and load the testing database
-	return base.PrepareTestEnv(t, 0, new(Repository))
+	return base.PrepareTestEnv(t, 0,
+		new(Repository),
+		new(CommitStatus),
+		new(RepoArchiver),
+		new(ReviewState),
+		new(Review),
+		new(Comment),
+		new(PullRequest),
+		new(Release),
+		new(RepoIndexerStatus),
+	)
 }
 
 func Test_RepositoryFormat(t *testing.T) {
 	x, deferable := PrepareOldRepository(t)
 	defer deferable()
 
+	assert.NoError(t, AdjustDBForSha256(x))
+
 	type Repository struct {
 		ID               int64  `xorm:"pk autoincr"`
 		ObjectFormatName string `xorg:"not null default('sha1')"`
@@ -37,12 +95,10 @@ func Test_RepositoryFormat(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, 4, count)
 
-	assert.NoError(t, AdjustDBForSha256(x))
-
-	repo.ID = 20
 	repo.ObjectFormatName = "sha256"
 	_, err = x.Insert(repo)
 	assert.NoError(t, err)
+	id := repo.ID
 
 	count, err = x.Count(new(Repository))
 	assert.NoError(t, err)
@@ -55,7 +111,7 @@ func Test_RepositoryFormat(t *testing.T) {
 	assert.EqualValues(t, "sha1", repo.ObjectFormatName)
 
 	repo = new(Repository)
-	ok, err = x.ID(20).Get(repo)
+	ok, err = x.ID(id).Get(repo)
 	assert.NoError(t, err)
 	assert.EqualValues(t, true, ok)
 	assert.EqualValues(t, "sha256", repo.ObjectFormatName)
diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go
index 19c7ae3b91..9c7b10947d 100644
--- a/models/migrations/v1_22/v287_test.go
+++ b/models/migrations/v1_22/v287_test.go
@@ -20,20 +20,20 @@ func Test_UpdateBadgeColName(t *testing.T) {
 	}
 
 	// Prepare and load the testing database
-	x, deferable := base.PrepareTestEnv(t, 0, new(BadgeUnique), new(Badge))
+	x, deferable := base.PrepareTestEnv(t, 0, new(Badge))
 	defer deferable()
 	if x == nil || t.Failed() {
 		return
 	}
 
-	oldBadges := []Badge{
-		{ID: 1, Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
-		{ID: 2, Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
-		{ID: 3, Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
+	oldBadges := []*Badge{
+		{Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
+		{Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
+		{Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
 	}
 
 	for _, badge := range oldBadges {
-		_, err := x.Insert(&badge)
+		_, err := x.Insert(badge)
 		assert.NoError(t, err)
 	}
 
@@ -48,7 +48,7 @@ func Test_UpdateBadgeColName(t *testing.T) {
 	}
 
 	for i, e := range oldBadges {
-		got := got[i]
+		got := got[i+1] // 1 is in the badge.yml
 		assert.Equal(t, e.ID, got.ID)
 		assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
 	}

From 26653b196bd1d15c532af41f60351596dd4330bd Mon Sep 17 00:00:00 2001
From: oliverpool <3864879+oliverpool@users.noreply.github.com>
Date: Thu, 7 Mar 2024 23:18:38 +0100
Subject: [PATCH 305/679] Store webhook event in database (#29145)

Refactor the webhook logic, to have the type-dependent processing happen
only in one place.

---

## Current webhook flow

1. An event happens
2. It is pre-processed (depending on the webhook type) and its body is
added to a task queue
3. When the task is processed, some more logic (depending on the webhook
type as well) is applied to make an HTTP request

This means that webhook-type dependant logic is needed in step 2 and 3.
This is cumbersome and brittle to maintain.

Updated webhook flow with this PR:
1. An event happens
2. It is stored as-is and added to a task queue
3. When the task is processed, the event is processed (depending on the
webhook type) to make an HTTP request

So the only webhook-type dependent logic happens in one place (step 3)
which should be much more robust.

## Consequences of the refactor

- the raw event must be stored in the hooktask (until now, the
pre-processed body was stored)
- to ensure that previous hooktasks are correctly sent, a
`payload_version` is added (version 1: the body has already been
pre-process / version 2: the body is the raw event)

So future webhook additions will only have to deal with creating an
http.Request based on the raw event (no need to adjust the code in
multiple places, like currently).

Moreover since this processing happens when fetching from the task
queue, it ensures that the queuing of new events (upon a `git push` for
instance) does not get slowed down by a slow webhook.

As a concrete example, the PR #19307 for custom webhooks, should be
substantially smaller:
- no need to change `services/webhook/deliver.go`
- minimal change in `services/webhook/webhook.go` (add the new webhook
to the map)
- no need to change all the individual webhook files (since with this
refactor the `*webhook_model.Webhook` is provided as argument)
---
 models/fixtures/hook_task.yml                |  32 ++
 models/migrations/migrations.go              |   2 +
 models/migrations/v1_22/v290.go              |  17 +
 models/webhook/hooktask.go                   |  26 +-
 models/webhook/webhook_test.go               |  61 +--
 services/webhook/deliver.go                  | 101 +++--
 services/webhook/deliver_test.go             | 202 ++++++++-
 services/webhook/dingtalk.go                 |  58 ++-
 services/webhook/dingtalk_test.go            | 243 +++++------
 services/webhook/discord.go                  |  68 ++-
 services/webhook/discord_test.go             | 340 +++++++--------
 services/webhook/feishu.go                   |  76 ++--
 services/webhook/feishu_test.go              | 147 +++----
 services/webhook/matrix.go                   | 215 ++++-----
 services/webhook/matrix_test.go              | 165 ++++---
 services/webhook/msteams.go                  |  58 ++-
 services/webhook/msteams_test.go             | 435 +++++++++----------
 services/webhook/packagist.go                |  91 ++--
 services/webhook/packagist_test.go           | 155 ++++---
 services/webhook/payloader.go                | 112 +++--
 services/webhook/slack.go                    |  95 ++--
 services/webhook/slack_test.go               | 148 +++----
 services/webhook/telegram.go                 |  62 ++-
 services/webhook/telegram_test.go            | 147 +++----
 services/webhook/webhook.go                  |  81 +---
 services/webhook/webhook_test.go             |   4 -
 services/webhook/wechatwork.go               |  59 ++-
 templates/repo/settings/webhook/history.tmpl |   4 +-
 28 files changed, 1686 insertions(+), 1518 deletions(-)
 create mode 100644 models/migrations/v1_22/v290.go

diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml
index 6dbb10151a..d573406b36 100644
--- a/models/fixtures/hook_task.yml
+++ b/models/fixtures/hook_task.yml
@@ -3,3 +3,35 @@
   hook_id: 1
   uuid: uuid1
   is_delivered: true
+  is_succeed: false
+  request_content: >
+    {
+      "url": "/matrix-delivered",
+      "http_method":"PUT",
+      "headers": {
+        "X-Head": "42"
+      },
+      "body": "{}"
+    }
+
+-
+  id: 2
+  hook_id: 1
+  uuid: uuid2
+  is_delivered: false
+
+-
+  id: 3
+  hook_id: 1
+  uuid: uuid3
+  is_delivered: true
+  is_succeed: true
+  payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content)
+  request_content: >
+    {
+      "url": "/matrix-success",
+      "http_method":"PUT",
+      "headers": {
+        "X-Head": "42"
+      }
+    }
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index d40866f3e9..ce77432db4 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -564,6 +564,8 @@ var migrations = []Migration{
 	NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
 	// v289 -> v290
 	NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch),
+	// v290 -> v291
+	NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go
new file mode 100644
index 0000000000..e9b7f504ba
--- /dev/null
+++ b/models/migrations/v1_22/v290.go
@@ -0,0 +1,17 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+type HookTask struct {
+	PayloadVersion int `xorm:"DEFAULT 1"`
+}
+
+func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
+	// create missing column
+	return x.Sync(new(HookTask))
+}
diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go
index 2fb655ebca..ff3fdbadb2 100644
--- a/models/webhook/hooktask.go
+++ b/models/webhook/hooktask.go
@@ -5,13 +5,13 @@ package webhook
 
 import (
 	"context"
+	"errors"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -31,6 +31,7 @@ type HookRequest struct {
 	URL        string            `json:"url"`
 	HTTPMethod string            `json:"http_method"`
 	Headers    map[string]string `json:"headers"`
+	Body       string            `json:"body"`
 }
 
 // HookResponse represents hook task response information.
@@ -45,11 +46,15 @@ type HookTask struct {
 	ID             int64  `xorm:"pk autoincr"`
 	HookID         int64  `xorm:"index"`
 	UUID           string `xorm:"unique"`
-	api.Payloader  `xorm:"-"`
 	PayloadContent string `xorm:"LONGTEXT"`
-	EventType      webhook_module.HookEventType
-	IsDelivered    bool
-	Delivered      timeutil.TimeStampNano
+	// PayloadVersion number to allow for smooth version upgrades:
+	//  - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL
+	//  - PayloadVersion 2: PayloadContent contains the original event
+	PayloadVersion int `xorm:"DEFAULT 1"`
+
+	EventType   webhook_module.HookEventType
+	IsDelivered bool
+	Delivered   timeutil.TimeStampNano
 
 	// History info.
 	IsSucceed       bool
@@ -115,16 +120,12 @@ func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error)
 // it handles conversion from Payload to PayloadContent.
 func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) {
 	t.UUID = gouuid.New().String()
-	if t.Payloader != nil {
-		data, err := t.Payloader.JSONPayload()
-		if err != nil {
-			return nil, err
-		}
-		t.PayloadContent = string(data)
-	}
 	if t.Delivered == 0 {
 		t.Delivered = timeutil.TimeStampNanoNow()
 	}
+	if t.PayloadVersion == 0 {
+		return nil, errors.New("missing HookTask.PayloadVersion")
+	}
 	return t, db.Insert(ctx, t)
 }
 
@@ -165,6 +166,7 @@ func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask,
 		HookID:         task.HookID,
 		PayloadContent: task.PayloadContent,
 		EventType:      task.EventType,
+		PayloadVersion: task.PayloadVersion,
 	})
 }
 
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index c70c8e99fc..f4403776ce 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -12,7 +12,6 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/optional"
-	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -35,8 +34,10 @@ func TestWebhook_History(t *testing.T) {
 	webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
 	tasks, err := webhook.History(db.DefaultContext, 0)
 	assert.NoError(t, err)
-	if assert.Len(t, tasks, 1) {
-		assert.Equal(t, int64(1), tasks[0].ID)
+	if assert.Len(t, tasks, 3) {
+		assert.Equal(t, int64(3), tasks[0].ID)
+		assert.Equal(t, int64(2), tasks[1].ID)
+		assert.Equal(t, int64(1), tasks[2].ID)
 	}
 
 	webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
@@ -197,8 +198,10 @@ func TestHookTasks(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTasks, err := HookTasks(db.DefaultContext, 1, 1)
 	assert.NoError(t, err)
-	if assert.Len(t, hookTasks, 1) {
-		assert.Equal(t, int64(1), hookTasks[0].ID)
+	if assert.Len(t, hookTasks, 3) {
+		assert.Equal(t, int64(3), hookTasks[0].ID)
+		assert.Equal(t, int64(2), hookTasks[1].ID)
+		assert.Equal(t, int64(1), hookTasks[2].ID)
 	}
 
 	hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1)
@@ -209,8 +212,8 @@ func TestHookTasks(t *testing.T) {
 func TestCreateHookTask(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:    3,
-		Payloader: &api.PushPayload{},
+		HookID:         3,
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -232,10 +235,10 @@ func TestUpdateHookTask(t *testing.T) {
 func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      3,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNanoNow(),
+		HookID:         3,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNanoNow(),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -249,9 +252,9 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
 func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: false,
+		HookID:         4,
+		IsDelivered:    false,
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -265,10 +268,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
 func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNanoNow(),
+		HookID:         4,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNanoNow(),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -282,10 +285,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
 func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      3,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
+		HookID:         3,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -299,9 +302,9 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
 func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: false,
+		HookID:         4,
+		IsDelivered:    false,
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -315,10 +318,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
 func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
+		HookID:         4,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 935981d29c..b2c0a73784 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -32,36 +32,17 @@ import (
 	"github.com/gobwas/glob"
 )
 
-// Deliver deliver hook task
-func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
-	w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
-	if err != nil {
-		return err
-	}
-
-	defer func() {
-		err := recover()
-		if err == nil {
-			return
-		}
-		// There was a panic whilst delivering a hook...
-		log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
-	}()
-
-	t.IsDelivered = true
-
-	var req *http.Request
-
+func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
 	switch w.HTTPMethod {
 	case "":
-		log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL)
+		log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
 		fallthrough
 	case http.MethodPost:
 		switch w.ContentType {
 		case webhook_model.ContentTypeJSON:
 			req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
 			if err != nil {
-				return err
+				return nil, nil, err
 			}
 
 			req.Header.Set("Content-Type", "application/json")
@@ -72,50 +53,58 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
 
 			req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
 			if err != nil {
-				return err
+				return nil, nil, err
 			}
 
 			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+		default:
+			return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType)
 		}
 	case http.MethodGet:
 		u, err := url.Parse(w.URL)
 		if err != nil {
-			return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err)
+			return nil, nil, fmt.Errorf("invalid URL: %w", err)
 		}
 		vals := u.Query()
 		vals["payload"] = []string{t.PayloadContent}
 		u.RawQuery = vals.Encode()
 		req, err = http.NewRequest("GET", u.String(), nil)
 		if err != nil {
-			return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err)
+			return nil, nil, err
 		}
 	case http.MethodPut:
 		switch w.Type {
-		case webhook_module.MATRIX:
+		case webhook_module.MATRIX: // used when t.Version == 1
 			txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
 			if err != nil {
-				return err
+				return nil, nil, err
 			}
 			url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
 			req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
 			if err != nil {
-				return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err)
+				return nil, nil, err
 			}
 		default:
-			return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
+			return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
 		}
 	default:
-		return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
+		return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
 	}
 
+	body = []byte(t.PayloadContent)
+	return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+}
+
+func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
 	var signatureSHA1 string
 	var signatureSHA256 string
-	if len(w.Secret) > 0 {
-		sig1 := hmac.New(sha1.New, []byte(w.Secret))
-		sig256 := hmac.New(sha256.New, []byte(w.Secret))
-		_, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent))
+	if len(secret) > 0 {
+		sig1 := hmac.New(sha1.New, secret)
+		sig256 := hmac.New(sha256.New, secret)
+		_, err := io.MultiWriter(sig1, sig256).Write(payloadContent)
 		if err != nil {
-			log.Error("prepareWebhooks.sigWrite: %v", err)
+			// this error should never happen, since the hashes are writing to []byte and always return a nil error.
+			return fmt.Errorf("prepareWebhooks.sigWrite: %w", err)
 		}
 		signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
 		signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
@@ -136,15 +125,36 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
 	req.Header["X-GitHub-Delivery"] = []string{t.UUID}
 	req.Header["X-GitHub-Event"] = []string{event}
 	req.Header["X-GitHub-Event-Type"] = []string{eventType}
+	return nil
+}
 
-	// Add Authorization Header
-	authorization, err := w.HeaderAuthorization()
+// Deliver creates the [http.Request] (depending on the webhook type), sends it
+// and records the status and response.
+func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
+	w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
 	if err != nil {
-		log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err)
 		return err
 	}
-	if authorization != "" {
-		req.Header["Authorization"] = []string{authorization}
+
+	defer func() {
+		err := recover()
+		if err == nil {
+			return
+		}
+		// There was a panic whilst delivering a hook...
+		log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
+	}()
+
+	t.IsDelivered = true
+
+	newRequest := webhookRequesters[w.Type]
+	if t.PayloadVersion == 1 || newRequest == nil {
+		newRequest = newDefaultRequest
+	}
+
+	req, body, err := newRequest(ctx, w, t)
+	if err != nil {
+		return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
 	}
 
 	// Record delivery information.
@@ -152,11 +162,22 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
 		URL:        req.URL.String(),
 		HTTPMethod: req.Method,
 		Headers:    map[string]string{},
+		Body:       string(body),
 	}
 	for k, vals := range req.Header {
 		t.RequestInfo.Headers[k] = strings.Join(vals, ",")
 	}
 
+	// Add Authorization Header
+	authorization, err := w.HeaderAuthorization()
+	if err != nil {
+		return fmt.Errorf("cannot get Authorization header for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
+	}
+	if authorization != "" {
+		req.Header.Set("Authorization", authorization)
+		t.RequestInfo.Headers["Authorization"] = "******"
+	}
+
 	t.ResponseInfo = &webhook_model.HookResponse{
 		Headers: map[string]string{},
 	}
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index 72aa00478a..24924ab214 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -5,9 +5,11 @@ package webhook
 
 import (
 	"context"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"strings"
 	"testing"
 	"time"
 
@@ -16,7 +18,6 @@ import (
 	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/hostmatcher"
 	"code.gitea.io/gitea/modules/setting"
-	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/stretchr/testify/assert"
@@ -107,13 +108,15 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
 	assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
 	db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true)
 
-	hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush, Payloader: &api.PushPayload{}}
+	hookTask := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadVersion: 2,
+	}
 
 	hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
 	assert.NoError(t, err)
-	if !assert.NotNil(t, hookTask) {
-		return
-	}
+	assert.NotNil(t, hookTask)
 
 	assert.NoError(t, Deliver(context.Background(), hookTask))
 	select {
@@ -123,4 +126,193 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
 	}
 
 	assert.True(t, hookTask.IsSucceed)
+	assert.Equal(t, "******", hookTask.RequestInfo.Headers["Authorization"])
+}
+
+func TestWebhookDeliverHookTask(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	done := make(chan struct{}, 1)
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "PUT", r.Method)
+		switch r.URL.Path {
+		case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
+			// Version 1
+			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
+			assert.Equal(t, "", r.Header.Get("Content-Type"))
+			body, err := io.ReadAll(r.Body)
+			assert.NoError(t, err)
+			assert.Equal(t, `{"data": 42}`, string(body))
+
+		case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
+			// Version 2
+			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
+			assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
+			body, err := io.ReadAll(r.Body)
+			assert.NoError(t, err)
+			assert.Len(t, body, 2147)
+
+		default:
+			w.WriteHeader(404)
+			t.Fatalf("unexpected url path %s", r.URL.Path)
+			return
+		}
+		w.WriteHeader(200)
+		done <- struct{}{}
+	}))
+	t.Cleanup(s.Close)
+
+	hook := &webhook_model.Webhook{
+		RepoID:      3,
+		IsActive:    true,
+		Type:        webhook_module.MATRIX,
+		URL:         s.URL + "/webhook",
+		HTTPMethod:  "PUT",
+		ContentType: webhook_model.ContentTypeJSON,
+		Meta:        `{"message_type":0}`, // text
+	}
+	assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
+
+	t.Run("Version 1", func(t *testing.T) {
+		hookTask := &webhook_model.HookTask{
+			HookID:         hook.ID,
+			EventType:      webhook_module.HookEventPush,
+			PayloadContent: `{"data": 42}`,
+			PayloadVersion: 1,
+		}
+
+		hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+		assert.NoError(t, err)
+		assert.NotNil(t, hookTask)
+
+		assert.NoError(t, Deliver(context.Background(), hookTask))
+		select {
+		case <-done:
+		case <-time.After(5 * time.Second):
+			t.Fatal("waited to long for request to happen")
+		}
+
+		assert.True(t, hookTask.IsSucceed)
+	})
+
+	t.Run("Version 2", func(t *testing.T) {
+		p := pushTestPayload()
+		data, err := p.JSONPayload()
+		assert.NoError(t, err)
+
+		hookTask := &webhook_model.HookTask{
+			HookID:         hook.ID,
+			EventType:      webhook_module.HookEventPush,
+			PayloadContent: string(data),
+			PayloadVersion: 2,
+		}
+
+		hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+		assert.NoError(t, err)
+		assert.NotNil(t, hookTask)
+
+		assert.NoError(t, Deliver(context.Background(), hookTask))
+		select {
+		case <-done:
+		case <-time.After(5 * time.Second):
+			t.Fatal("waited to long for request to happen")
+		}
+
+		assert.True(t, hookTask.IsSucceed)
+	})
+}
+
+func TestWebhookDeliverSpecificTypes(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	type hookCase struct {
+		gotBody chan []byte
+	}
+
+	cases := map[string]hookCase{
+		webhook_module.SLACK: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.DISCORD: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.DINGTALK: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.TELEGRAM: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.MSTEAMS: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.FEISHU: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.MATRIX: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.WECHATWORK: {
+			gotBody: make(chan []byte, 1),
+		},
+		webhook_module.PACKAGIST: {
+			gotBody: make(chan []byte, 1),
+		},
+	}
+
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)
+
+		typ := strings.Split(r.URL.Path, "/")[1] // take first segment (after skipping leading slash)
+		hc := cases[typ]
+		require.NotNil(t, hc.gotBody, r.URL.Path)
+		body, err := io.ReadAll(r.Body)
+		assert.NoError(t, err)
+		w.WriteHeader(200)
+		hc.gotBody <- body
+	}))
+	t.Cleanup(s.Close)
+
+	p := pushTestPayload()
+	data, err := p.JSONPayload()
+	assert.NoError(t, err)
+
+	for typ, hc := range cases {
+		typ := typ
+		hc := hc
+		t.Run(typ, func(t *testing.T) {
+			t.Parallel()
+			hook := &webhook_model.Webhook{
+				RepoID:      3,
+				IsActive:    true,
+				Type:        typ,
+				URL:         s.URL + "/" + typ,
+				HTTPMethod:  "POST",
+				ContentType: 0, // set to 0 so that falling back to default request fails with "invalid content type"
+				Meta:        "{}",
+			}
+			assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
+
+			hookTask := &webhook_model.HookTask{
+				HookID:         hook.ID,
+				EventType:      webhook_module.HookEventPush,
+				PayloadContent: string(data),
+				PayloadVersion: 2,
+			}
+
+			hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+			assert.NoError(t, err)
+			assert.NotNil(t, hookTask)
+
+			assert.NoError(t, Deliver(context.Background(), hookTask))
+			select {
+			case gotBody := <-hc.gotBody:
+				assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload")
+				assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "request body was not saved")
+			case <-time.After(5 * time.Second):
+				t.Fatal("waited to long for request to happen")
+			}
+
+			assert.True(t, hookTask.IsSucceed)
+		})
+	}
 }
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index d615e7254f..c57d04415a 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -4,12 +4,14 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -22,19 +24,8 @@ type (
 	DingtalkPayload dingtalk.Payload
 )
 
-var _ PayloadConvertor = &DingtalkPayload{}
-
-// JSONPayload Marshals the DingtalkPayload to json
-func (d *DingtalkPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(d, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
 // Create implements PayloadConvertor Create method
-func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -43,7 +34,7 @@ func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -52,14 +43,14 @@ func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (d *DingtalkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return createDingtalkPayload(title, title, fmt.Sprintf("view forked repo %s", p.Repo.FullName), p.Repo.HTMLURL), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -100,14 +91,14 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) {
 	text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view issue", p.Issue.HTMLURL), nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Wiki(p *api.WikiPayload) (DingtalkPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
 	url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
 
@@ -115,27 +106,27 @@ func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) IssueComment(p *api.IssueCommentPayload) (DingtalkPayload, error) {
 	text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+p.Comment.Body, "view issue comment", p.Comment.HTMLURL), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) {
 	text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view pull request", p.PullRequest.HTMLURL), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (dc dingtalkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DingtalkPayload, error) {
 	var text, title string
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return DingtalkPayload{}, err
 		}
 
 		title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -146,14 +137,14 @@ func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module
 }
 
 // Repository implements PayloadConvertor Repository method
-func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayload, error) {
 	switch p.Action {
 	case api.HookRepoCreated:
 		title := fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
 		return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil
 	case api.HookRepoDeleted:
 		title := fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
-		return &DingtalkPayload{
+		return DingtalkPayload{
 			MsgType: "text",
 			Text: struct {
 				Content string `json:"content"`
@@ -163,24 +154,24 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
 		}, nil
 	}
 
-	return nil, nil
+	return DingtalkPayload{}, nil
 }
 
 // Release implements PayloadConvertor Release method
-func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Release(p *api.ReleasePayload) (DingtalkPayload, error) {
 	text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
 }
 
-func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) {
 	text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
 }
 
-func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
-	return &DingtalkPayload{
+func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
+	return DingtalkPayload{
 		MsgType: "actionCard",
 		ActionCard: dingtalk.ActionCard{
 			Text:        strings.TrimSpace(text),
@@ -195,7 +186,10 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk
 	}
 }
 
-// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
-func GetDingtalkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(DingtalkPayload), p, event)
+type dingtalkConvertor struct{}
+
+var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
+
+func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(dingtalkConvertor{}, w, t, true)
 }
diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go
index a03fa46f14..25f47347d0 100644
--- a/services/webhook/dingtalk_test.go
+++ b/services/webhook/dingtalk_test.go
@@ -4,9 +4,12 @@
 package webhook
 
 import (
+	"context"
 	"net/url"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -24,248 +27,226 @@ func TestDingTalkPayload(t *testing.T) {
 		}
 		return ""
 	}
+	dc := dingtalkConvertor{}
 
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Create(p)
+		pl, err := dc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Title)
+		assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Delete(p)
+		pl, err := dc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Title)
+		assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Fork(p)
+		pl, err := dc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view forked repo test/repo", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Text)
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Title)
+		assert.Equal(t, "view forked repo test/repo", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Push(p)
+		pl, err := dc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view commits", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.ActionCard.Title)
+		assert.Equal(t, "view commits", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(DingtalkPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.ActionCard.Text)
+		assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+		assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.ActionCard.Text)
+		assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+		assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.ActionCard.Text)
+		assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+		assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := dc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.ActionCard.Text)
+		assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
+		assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.ActionCard.Text)
+		assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
+		assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(DingtalkPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.ActionCard.Title)
+		assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Repository(p)
+		pl, err := dc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view repository", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Title)
+		assert.Equal(t, "view repository", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Package(p)
+		pl, err := dc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view package", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Text)
+		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view package", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(DingtalkPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Release(p)
+		pl, err := dc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view release", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 }
 
 func TestDingTalkJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(DingtalkPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &DingtalkPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.DINGTALK,
+		URL:        "https://dingtalk.example.com/",
+		Meta:       ``,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newDingtalkRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://dingtalk.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body DingtalkPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.ActionCard.Text)
 }
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index e2ac1410b8..659754d5e0 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -4,8 +4,10 @@
 package webhook
 
 import (
+	"context"
 	"errors"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strconv"
 	"strings"
@@ -98,19 +100,8 @@ var (
 	redColor         = color("ff3232")
 )
 
-// JSONPayload Marshals the DiscordPayload to json
-func (d *DiscordPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(d, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &DiscordPayload{}
-
 // Create implements PayloadConvertor Create method
-func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -119,7 +110,7 @@ func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (d discordConvertor) Delete(p *api.DeletePayload) (DiscordPayload, error) {
 	// deleted tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -128,14 +119,14 @@ func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (d discordConvertor) Fork(p *api.ForkPayload) (DiscordPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -170,35 +161,35 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) {
 	title, _, text, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, title, text, p.Issue.HTMLURL, color), nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (d discordConvertor) IssueComment(p *api.IssueCommentPayload) (DiscordPayload, error) {
 	title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (d discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) {
 	title, _, text, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (d discordConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DiscordPayload, error) {
 	var text, title string
 	var color int
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return DiscordPayload{}, err
 		}
 
 		title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -220,7 +211,7 @@ func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.
 }
 
 // Repository implements PayloadConvertor Repository method
-func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) {
 	var title, url string
 	var color int
 	switch p.Action {
@@ -237,7 +228,7 @@ func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) {
 	text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
 	htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
 
@@ -250,30 +241,35 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
 }
 
 // Release implements PayloadConvertor Release method
-func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (d discordConvertor) Release(p *api.ReleasePayload) (DiscordPayload, error) {
 	text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
 }
 
-func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) {
 	text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
 }
 
-// GetDiscordPayload converts a discord webhook into a DiscordPayload
-func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(DiscordPayload)
+type discordConvertor struct {
+	Username  string
+	AvatarURL string
+}
 
-	discord := &DiscordMeta{}
-	if err := json.Unmarshal([]byte(meta), &discord); err != nil {
-		return s, errors.New("GetDiscordPayload meta json:" + err.Error())
+var _ payloadConvertor[DiscordPayload] = discordConvertor{}
+
+func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &DiscordMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err)
 	}
-	s.Username = discord.Username
-	s.AvatarURL = discord.IconURL
-
-	return convertPayloader(s, p, event)
+	sc := discordConvertor{
+		Username:  meta.Username,
+		AvatarURL: meta.IconURL,
+	}
+	return newJSONRequest(sc, w, t, true)
 }
 
 func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
@@ -291,8 +287,8 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
 	}
 }
 
-func (d *DiscordPayload) createPayload(s *api.User, title, text, url string, color int) *DiscordPayload {
-	return &DiscordPayload{
+func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload {
+	return DiscordPayload{
 		Username:  d.Username,
 		AvatarURL: d.AvatarURL,
 		Embeds: []DiscordEmbed{
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index b567cbc395..c04b95383b 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -15,295 +18,274 @@ import (
 )
 
 func TestDiscordPayload(t *testing.T) {
+	dc := discordConvertor{}
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Create(p)
+		pl, err := dc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] branch test created", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] branch test created", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Delete(p)
+		pl, err := dc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] branch test deleted", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Fork(p)
+		pl, err := dc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Push(p)
+		pl, err := dc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
+		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(DiscordPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "issue body", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Embeds[0].Title)
+		assert.Equal(t, "issue body", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "more info needed", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Embeds[0].Title)
+		assert.Equal(t, "more info needed", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := dc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "fixes bug #2", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Embeds[0].Title)
+		assert.Equal(t, "fixes bug #2", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "changes requested", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Embeds[0].Title)
+		assert.Equal(t, "changes requested", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(DiscordPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "good job", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Embeds[0].Title)
+		assert.Equal(t, "good job", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Repository(p)
+		pl, err := dc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Repository created", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Repository created", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Package(p)
+		pl, err := dc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "Package created: GiteaContainer:latest", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(DiscordPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Embeds[0].Title)
+		assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Embeds[0].Title)
+		assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Release(p)
+		pl, err := dc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Release created: v1.0", pl.Embeds[0].Title)
+		assert.Equal(t, "Note of first stable release", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 }
 
 func TestDiscordJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(DiscordPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &DiscordPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.DISCORD,
+		URL:        "https://discord.example.com/",
+		Meta:       `{}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newDiscordRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://discord.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body DiscordPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description)
 }
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 556443e70b..1ec436894b 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -4,11 +4,13 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
@@ -23,8 +25,8 @@ type (
 	}
 )
 
-func newFeishuTextPayload(text string) *FeishuPayload {
-	return &FeishuPayload{
+func newFeishuTextPayload(text string) FeishuPayload {
+	return FeishuPayload{
 		MsgType: "text",
 		Content: struct {
 			Text string `json:"text"`
@@ -34,19 +36,8 @@ func newFeishuTextPayload(text string) *FeishuPayload {
 	}
 }
 
-// JSONPayload Marshals the FeishuPayload to json
-func (f *FeishuPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(f, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &FeishuPayload{}
-
 // Create implements PayloadConvertor Create method
-func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Create(p *api.CreatePayload) (FeishuPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	text := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -55,7 +46,7 @@ func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Delete(p *api.DeletePayload) (FeishuPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	text := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -64,14 +55,14 @@ func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (f *FeishuPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Fork(p *api.ForkPayload) (FeishuPayload, error) {
 	text := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return newFeishuTextPayload(text), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -96,48 +87,40 @@ func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) {
 	title, link, by, operator, result, assignees := getIssuesInfo(p)
-	var res api.Payloader
 	if assignees != "" {
 		if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body))
-		} else {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body))
+			return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)), nil
 		}
-	} else {
-		res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body))
+		return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)), nil
 	}
-	return res, nil
+	return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)), nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (fc feishuConvertor) IssueComment(p *api.IssueCommentPayload) (FeishuPayload, error) {
 	title, link, by, operator := getIssuesCommentInfo(p)
 	return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) {
 	title, link, by, operator, result, assignees := getPullRequestInfo(p)
-	var res api.Payloader
 	if assignees != "" {
 		if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body))
-		} else {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body))
+			return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)), nil
 		}
-	} else {
-		res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body))
+		return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)), nil
 	}
-	return res, nil
+	return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) {
 	action, err := parseHookPullRequestEventType(event)
 	if err != nil {
-		return nil, err
+		return FeishuPayload{}, err
 	}
 
 	title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -147,7 +130,7 @@ func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.H
 }
 
 // Repository implements PayloadConvertor Repository method
-func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) {
 	var text string
 	switch p.Action {
 	case api.HookRepoCreated:
@@ -158,30 +141,33 @@ func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
 		return newFeishuTextPayload(text), nil
 	}
 
-	return nil, nil
+	return FeishuPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (f *FeishuPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
 
 	return newFeishuTextPayload(text), nil
 }
 
 // Release implements PayloadConvertor Release method
-func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) {
 	text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
 
 	return newFeishuTextPayload(text), nil
 }
 
-func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) {
 	text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
 
 	return newFeishuTextPayload(text), nil
 }
 
-// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
-func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(FeishuPayload), p, event)
+type feishuConvertor struct{}
+
+var _ payloadConvertor[FeishuPayload] = feishuConvertor{}
+
+func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(feishuConvertor{}, w, t, true)
 }
diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go
index 98bc50dede..ef18333fd4 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,199 +17,177 @@ import (
 )
 
 func TestFeishuPayload(t *testing.T) {
+	fc := feishuConvertor{}
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Create(p)
+		pl, err := fc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, `[test/repo] branch test created`, pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, `[test/repo] branch test created`, pl.Content.Text)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Delete(p)
+		pl, err := fc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, `[test/repo] branch test deleted`, pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, `[test/repo] branch test deleted`, pl.Content.Text)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Fork(p)
+		pl, err := fc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Content.Text)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Push(p)
+		pl, err := fc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Content.Text)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(FeishuPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := fc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = fc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := fc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.Content.Text)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := fc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.Content.Text)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := fc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.Content.Text)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(FeishuPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := fc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.Content.Text)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Repository(p)
+		pl, err := fc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Repository created", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Repository created", pl.Content.Text)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Package(p)
+		pl, err := fc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.Content.Text)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(FeishuPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := fc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.Content.Text)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = fc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.Content.Text)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = fc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.Content.Text)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Release(p)
+		pl, err := fc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.Content.Text)
 	})
 }
 
 func TestFeishuJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(FeishuPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &FeishuPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.FEISHU,
+		URL:        "https://feishu.example.com/",
+		Meta:       `{}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newFeishuRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://feishu.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body FeishuPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
 }
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 602d16ef39..0329804a8b 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -4,11 +4,12 @@
 package webhook
 
 import (
+	"bytes"
+	"context"
 	"crypto/sha1"
 	"encoding/hex"
-	"errors"
 	"fmt"
-	"html"
+	"net/http"
 	"net/url"
 	"regexp"
 	"strings"
@@ -23,6 +24,37 @@ import (
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
 
+func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &MatrixMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err)
+	}
+	mc := matrixConvertor{
+		MsgType: messageTypeText[meta.MessageType],
+	}
+	payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	body, err := json.MarshalIndent(payload, "", "  ")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	txnID, err := getMatrixTxnID(body)
+	if err != nil {
+		return nil, nil, err
+	}
+	req, err := http.NewRequest(http.MethodPut, w.URL+"/"+txnID, bytes.NewReader(body))
+	if err != nil {
+		return nil, nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially
+}
+
 const matrixPayloadSizeLimit = 1024 * 64
 
 // MatrixMeta contains the Matrix metadata
@@ -46,8 +78,6 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
 	return s
 }
 
-var _ PayloadConvertor = &MatrixPayload{}
-
 // MatrixPayload contains payload for a Matrix room
 type MatrixPayload struct {
 	Body          string               `json:"body"`
@@ -57,90 +87,79 @@ type MatrixPayload struct {
 	Commits       []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
 }
 
-// JSONPayload Marshals the MatrixPayload to json
-func (m *MatrixPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(m, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
+var _ payloadConvertor[MatrixPayload] = matrixConvertor{}
+
+type matrixConvertor struct {
+	MsgType string
 }
 
-// MatrixLinkFormatter creates a link compatible with Matrix
-func MatrixLinkFormatter(url, text string) string {
-	return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
+func (m matrixConvertor) newPayload(text string, commits ...*api.PayloadCommit) (MatrixPayload, error) {
+	return MatrixPayload{
+		Body:          getMessageBody(text),
+		MsgType:       m.MsgType,
+		Format:        "org.matrix.custom.html",
+		FormattedBody: text,
+		Commits:       commits,
+	}, nil
 }
 
-// MatrixLinkToRef Matrix-formatter link to a repo ref
-func MatrixLinkToRef(repoURL, ref string) string {
-	refName := git.RefName(ref).ShortName()
-	switch {
-	case strings.HasPrefix(ref, git.BranchPrefix):
-		return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
-	case strings.HasPrefix(ref, git.TagPrefix):
-		return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
-	default:
-		return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
-	}
-}
-
-// Create implements PayloadConvertor Create method
-func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
-	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+// Create implements payloadConvertor Create method
+func (m matrixConvertor) Create(p *api.CreatePayload) (MatrixPayload, error) {
+	repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
 	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
 // Delete composes Matrix payload for delete a branch or tag.
-func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (m matrixConvertor) Delete(p *api.DeletePayload) (MatrixPayload, error) {
 	refName := git.RefName(p.Ref).ShortName()
-	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+	repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
 // Fork composes Matrix payload for forked by a repository.
-func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
-	baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
-	forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+func (m matrixConvertor) Fork(p *api.ForkPayload) (MatrixPayload, error) {
+	baseLink := htmlLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+	forkLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Issue implements PayloadConvertor Issue method
-func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
-	text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
+// Issue implements payloadConvertor Issue method
+func (m matrixConvertor) Issue(p *api.IssuePayload) (MatrixPayload, error) {
+	text, _, _, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// IssueComment implements PayloadConvertor IssueComment method
-func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
-	text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
+// IssueComment implements payloadConvertor IssueComment method
+func (m matrixConvertor) IssueComment(p *api.IssueCommentPayload) (MatrixPayload, error) {
+	text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Wiki implements PayloadConvertor Wiki method
-func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
-	text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true)
+// Wiki implements payloadConvertor Wiki method
+func (m matrixConvertor) Wiki(p *api.WikiPayload) (MatrixPayload, error) {
+	text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Release implements PayloadConvertor Release method
-func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
-	text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
+// Release implements payloadConvertor Release method
+func (m matrixConvertor) Release(p *api.ReleasePayload) (MatrixPayload, error) {
+	text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Push implements PayloadConvertor Push method
-func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+// Push implements payloadConvertor Push method
+func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) {
 	var commitDesc string
 
 	if p.TotalCommits == 1 {
@@ -149,13 +168,13 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 		commitDesc = fmt.Sprintf("%d commits", p.TotalCommits)
 	}
 
-	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+	repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
 	text := fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink)
 
 	// for each commit, generate a new line text
 	for i, commit := range p.Commits {
-		text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
+		text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
 		// add linebreak to each commit but the last
 		if i < len(p.Commits)-1 {
 			text += "<br>"
@@ -163,41 +182,41 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 
 	}
 
-	return getMatrixPayload(text, p.Commits, m.MsgType), nil
+	return m.newPayload(text, p.Commits...)
 }
 
-// PullRequest implements PayloadConvertor PullRequest method
-func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
-	text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
+// PullRequest implements payloadConvertor PullRequest method
+func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) {
+	text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Review implements PayloadConvertor Review method
-func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
-	senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+// Review implements payloadConvertor Review method
+func (m matrixConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MatrixPayload, error) {
+	senderLink := htmlLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
 	title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
-	titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title)
-	repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
+	titleLink := htmlLinkFormatter(p.PullRequest.HTMLURL, title)
+	repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
 	var text string
 
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return MatrixPayload{}, err
 		}
 
 		text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
 	}
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Repository implements PayloadConvertor Repository method
-func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
-	senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
-	repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
+// Repository implements payloadConvertor Repository method
+func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, error) {
+	senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+	repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
 	var text string
 
 	switch p.Action {
@@ -206,13 +225,12 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
 	case api.HookRepoDeleted:
 		text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
 	}
-
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
-	senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
-	packageLink := MatrixLinkFormatter(p.Package.HTMLURL, p.Package.Name)
+func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
+	senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+	packageLink := htmlLinkFormatter(p.Package.HTMLURL, p.Package.Name)
 	var text string
 
 	switch p.Action {
@@ -222,31 +240,7 @@ func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
 		text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink)
 	}
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
-}
-
-// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
-func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(MatrixPayload)
-
-	matrix := &MatrixMeta{}
-	if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
-		return s, errors.New("GetMatrixPayload meta json:" + err.Error())
-	}
-
-	s.MsgType = messageTypeText[matrix.MessageType]
-
-	return convertPayloader(s, p, event)
-}
-
-func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload {
-	p := MatrixPayload{}
-	p.FormattedBody = text
-	p.Body = getMessageBody(text)
-	p.Format = "org.matrix.custom.html"
-	p.MsgType = msgType
-	p.Commits = commits
-	return &p
+	return m.newPayload(text)
 }
 
 var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
@@ -271,3 +265,16 @@ func getMatrixTxnID(payload []byte) (string, error) {
 
 	return hex.EncodeToString(h.Sum(nil)), nil
 }
+
+// MatrixLinkToRef Matrix-formatter link to a repo ref
+func MatrixLinkToRef(repoURL, ref string) string {
+	refName := git.RefName(ref).ShortName()
+	switch {
+	case strings.HasPrefix(ref, git.BranchPrefix):
+		return htmlLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
+	case strings.HasPrefix(ref, git.TagPrefix):
+		return htmlLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
+	default:
+		return htmlLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
+	}
+}
diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go
index 99a22fbd7e..058f8e3c5f 100644
--- a/services/webhook/matrix_test.go
+++ b/services/webhook/matrix_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,217 +17,213 @@ import (
 )
 
 func TestMatrixPayload(t *testing.T) {
+	mc := matrixConvertor{
+		MsgType: "m.text",
+	}
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Create(p)
+		pl, err := mc.Create(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] branch created by user1`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] branch created by user1`, pl.FormattedBody)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Delete(p)
+		pl, err := mc.Delete(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:test] branch deleted by user1`, pl.FormattedBody)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Fork(p)
+		pl, err := mc.Fork(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.Body)
+		assert.Equal(t, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Push(p)
+		pl, err := mc.Push(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1`, pl.FormattedBody)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(MatrixPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := mc.Issue(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = mc.Issue(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := mc.PullRequest(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(MatrixPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Repository(p)
+		pl, err := mc.Repository(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Package(p)
+		pl, err := mc.Package(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer</a>] Package published by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer</a>] Package published by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(MatrixPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := mc.Wiki(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Release(p)
+		pl, err := mc.Release(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 }
 
 func TestMatrixJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(MatrixPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &MatrixPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:   3,
+		IsActive: true,
+		Type:     webhook_module.MATRIX,
+		URL:      "https://matrix.example.com/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message",
+		Meta:     `{"message_type":0}`, // text
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newMatrixRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "PUT", req.Method)
+	assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path)
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body MatrixPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", body.Body)
 }
 
 func Test_getTxnID(t *testing.T) {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 37810b4cd3..99d0106184 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -4,12 +4,14 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -56,19 +58,8 @@ type (
 	}
 )
 
-// JSONPayload Marshals the MSTeamsPayload to json
-func (m *MSTeamsPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(m, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &MSTeamsPayload{}
-
 // Create implements PayloadConvertor Create method
-func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -85,7 +76,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) {
 	// deleted tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -102,7 +93,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return createMSTeamsPayload(
@@ -117,7 +108,7 @@ func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
 }
 
 // Push implements PayloadConvertor Push method
-func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -160,7 +151,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) {
 	title, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -175,7 +166,7 @@ func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) {
 	title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -190,7 +181,7 @@ func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) {
 	title, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -205,14 +196,14 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
 }
 
 // Review implements PayloadConvertor Review method
-func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) {
 	var text, title string
 	var color int
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return MSTeamsPayload{}, err
 		}
 
 		title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -242,7 +233,7 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.
 }
 
 // Repository implements PayloadConvertor Repository method
-func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) {
 	var title, url string
 	var color int
 	switch p.Action {
@@ -267,7 +258,7 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) {
 	title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -282,7 +273,7 @@ func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
 }
 
 // Release implements PayloadConvertor Release method
-func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) {
 	title, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -296,7 +287,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
 	), nil
 }
 
-func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) {
 	title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -310,12 +301,7 @@ func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
 	), nil
 }
 
-// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
-func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(MSTeamsPayload), p, event)
-}
-
-func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) *MSTeamsPayload {
+func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
 	facts := make([]MSTeamsFact, 0, 2)
 	if r != nil {
 		facts = append(facts, MSTeamsFact{
@@ -327,7 +313,7 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
 		facts = append(facts, *fact)
 	}
 
-	return &MSTeamsPayload{
+	return MSTeamsPayload{
 		Type:       "MessageCard",
 		Context:    "https://schema.org/extensions",
 		ThemeColor: fmt.Sprintf("%x", color),
@@ -356,3 +342,11 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
 		},
 	}
 }
+
+type msteamsConvertor struct{}
+
+var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
+
+func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(msteamsConvertor{}, w, t, true)
+}
diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go
index 8d1aed6040..01e08b918e 100644
--- a/services/webhook/msteams_test.go
+++ b/services/webhook/msteams_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,22 +17,20 @@ import (
 )
 
 func TestMSTeamsPayload(t *testing.T) {
+	mc := msteamsConvertor{}
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Create(p)
+		pl, err := mc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] branch test created", pl.Title)
+		assert.Equal(t, "[test/repo] branch test created", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "branch:" {
@@ -38,27 +39,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Delete(p)
+		pl, err := mc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] branch test deleted", pl.Title)
+		assert.Equal(t, "[test/repo] branch test deleted", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "branch:" {
@@ -67,27 +65,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Fork(p)
+		pl, err := mc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.Title)
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "Forkee:" {
@@ -96,27 +91,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Push(p)
+		pl, err := mc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.Title)
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "Commit count:" {
@@ -125,28 +117,25 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(MSTeamsPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := mc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "issue body", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Title)
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "issue body", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -155,23 +144,21 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = mc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Title)
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -180,27 +167,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "more info needed", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Title)
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "more info needed", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -209,27 +193,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := mc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "fixes bug #2", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Title)
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "fixes bug #2", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Pull request #:" {
@@ -238,27 +219,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "changes requested", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Title)
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "changes requested", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -267,28 +245,25 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "good job", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Title)
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "good job", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Pull request #:" {
@@ -297,155 +272,139 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Repository(p)
+		pl, err := mc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Repository created", pl.Title)
+		assert.Equal(t, "[test/repo] Repository created", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 1)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Package(p)
+		pl, err := mc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "Package created: GiteaContainer:latest", pl.Title)
+		assert.Equal(t, "Package created: GiteaContainer:latest", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 1)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Package:" {
 				assert.Equal(t, p.Package.Name, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(MSTeamsPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := mc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Title)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Title)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Title)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Release(p)
+		pl, err := mc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Release created: v1.0", pl.Title)
+		assert.Equal(t, "[test/repo] Release created: v1.0", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Tag:" {
@@ -454,21 +413,43 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.PotentialAction[0].Targets[0].URI)
 	})
 }
 
 func TestMSTeamsJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(MSTeamsPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &MSTeamsPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.MSTEAMS,
+		URL:        "https://msteams.example.com/",
+		Meta:       ``,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://msteams.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body MSTeamsPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[test/repo:test] 2 new commits", body.Summary)
 }
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 714a4c076e..7880d8b606 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -4,7 +4,9 @@
 package webhook
 
 import (
-	"errors"
+	"context"
+	"fmt"
+	"net/http"
 
 	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/json"
@@ -38,84 +40,85 @@ func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta {
 	return s
 }
 
-// JSONPayload Marshals the PackagistPayload to json
-func (f *PackagistPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(f, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &PackagistPayload{}
-
 // Create implements PayloadConvertor Create method
-func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Delete implements PayloadConvertor Delete method
-func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Fork implements PayloadConvertor Fork method
-func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Push implements PayloadConvertor Push method
-func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) {
-	return f, nil
+// https://packagist.org/about
+func (pc packagistConvertor) Push(_ *api.PushPayload) (PackagistPayload, error) {
+	return PackagistPayload{
+		PackagistRepository: struct {
+			URL string `json:"url"`
+		}{
+			URL: pc.PackageURL,
+		},
+	}, nil
 }
 
 // Issue implements PayloadConvertor Issue method
-func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Review implements PayloadConvertor Review method
-func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Repository implements PayloadConvertor Repository method
-func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Release implements PayloadConvertor Release method
-func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
-func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
-// GetPackagistPayload converts a packagist webhook into a PackagistPayload
-func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(PackagistPayload)
+type packagistConvertor struct {
+	PackageURL string
+}
 
-	packagist := &PackagistMeta{}
-	if err := json.Unmarshal([]byte(meta), &packagist); err != nil {
-		return s, errors.New("GetPackagistPayload meta json:" + err.Error())
+var _ payloadConvertor[PackagistPayload] = packagistConvertor{}
+
+func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &PackagistMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err)
 	}
-	s.PackagistRepository.URL = packagist.PackageURL
-	return convertPayloader(s, p, event)
+	pc := packagistConvertor{
+		PackageURL: meta.PackageURL,
+	}
+	return newJSONRequest(pc, w, t, true)
 }
diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go
index 26d01b0555..e9b0695baa 100644
--- a/services/webhook/packagist_test.go
+++ b/services/webhook/packagist_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,155 +17,199 @@ import (
 )
 
 func TestPackagistPayload(t *testing.T) {
+	pc := packagistConvertor{
+		PackageURL: "https://packagist.org/packages/example",
+	}
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Create(p)
+		pl, err := pc.Create(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Delete(p)
+		pl, err := pc.Delete(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Fork(p)
+		pl, err := pc.Fork(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(PackagistPayload)
-		d.PackagistRepository.URL = "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN"
-		pl, err := d.Push(p)
+		pl, err := pc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &PackagistPayload{}, pl)
 
-		assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", pl.(*PackagistPayload).PackagistRepository.URL)
+		assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(PackagistPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := pc.Issue(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = pc.Issue(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := pc.IssueComment(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := pc.PullRequest(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := pc.IssueComment(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(PackagistPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Repository(p)
+		pl, err := pc.Repository(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Package(p)
+		pl, err := pc.Package(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(PackagistPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := pc.Wiki(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = pc.Wiki(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = pc.Wiki(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Release(p)
+		pl, err := pc.Release(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 }
 
 func TestPackagistJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(PackagistPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &PackagistPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.PACKAGIST,
+		URL:        "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
+		Meta:       `{"package_url":"https://packagist.org/packages/example"}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body PackagistPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "https://packagist.org/packages/example", body.PackagistRepository.URL)
+}
+
+func TestPackagistEmptyPayload(t *testing.T) {
+	p := createTestPayload()
+	data, err := p.JSONPayload()
+	require.NoError(t, err)
+
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.PACKAGIST,
+		URL:        "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
+		Meta:       `{"package_url":"https://packagist.org/packages/example"}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventCreate,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
+	require.NoError(t, err)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body PackagistPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "", body.PackagistRepository.URL)
 }
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index bd482c04ea..abf9946cca 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -4,58 +4,104 @@
 package webhook
 
 import (
+	"bytes"
+	"fmt"
+	"net/http"
+
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
 
-// PayloadConvertor defines the interface to convert system webhook payload to external payload
-type PayloadConvertor interface {
-	api.Payloader
-	Create(*api.CreatePayload) (api.Payloader, error)
-	Delete(*api.DeletePayload) (api.Payloader, error)
-	Fork(*api.ForkPayload) (api.Payloader, error)
-	Issue(*api.IssuePayload) (api.Payloader, error)
-	IssueComment(*api.IssueCommentPayload) (api.Payloader, error)
-	Push(*api.PushPayload) (api.Payloader, error)
-	PullRequest(*api.PullRequestPayload) (api.Payloader, error)
-	Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error)
-	Repository(*api.RepositoryPayload) (api.Payloader, error)
-	Release(*api.ReleasePayload) (api.Payloader, error)
-	Wiki(*api.WikiPayload) (api.Payloader, error)
-	Package(*api.PackagePayload) (api.Payloader, error)
+// payloadConvertor defines the interface to convert system payload to webhook payload
+type payloadConvertor[T any] interface {
+	Create(*api.CreatePayload) (T, error)
+	Delete(*api.DeletePayload) (T, error)
+	Fork(*api.ForkPayload) (T, error)
+	Issue(*api.IssuePayload) (T, error)
+	IssueComment(*api.IssueCommentPayload) (T, error)
+	Push(*api.PushPayload) (T, error)
+	PullRequest(*api.PullRequestPayload) (T, error)
+	Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error)
+	Repository(*api.RepositoryPayload) (T, error)
+	Release(*api.ReleasePayload) (T, error)
+	Wiki(*api.WikiPayload) (T, error)
+	Package(*api.PackagePayload) (T, error)
 }
 
-func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
+func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
+	var p P
+	if err := json.Unmarshal(data, &p); err != nil {
+		var t T
+		return t, fmt.Errorf("could not unmarshal payload: %w", err)
+	}
+	return convert(p)
+}
+
+func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) {
 	switch event {
 	case webhook_module.HookEventCreate:
-		return s.Create(p.(*api.CreatePayload))
+		return convertUnmarshalledJSON(rc.Create, data)
 	case webhook_module.HookEventDelete:
-		return s.Delete(p.(*api.DeletePayload))
+		return convertUnmarshalledJSON(rc.Delete, data)
 	case webhook_module.HookEventFork:
-		return s.Fork(p.(*api.ForkPayload))
+		return convertUnmarshalledJSON(rc.Fork, data)
 	case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
-		return s.Issue(p.(*api.IssuePayload))
+		return convertUnmarshalledJSON(rc.Issue, data)
 	case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
-		pl, ok := p.(*api.IssueCommentPayload)
-		if ok {
-			return s.IssueComment(pl)
-		}
-		return s.PullRequest(p.(*api.PullRequestPayload))
+		// previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload))
+		// however I couldn't find in notifier.go such a payload with an HookEvent***Comment event
+
+		// History (most recent first):
+		//  - refactored in https://github.com/go-gitea/gitea/pull/12310
+		//  - assertion added in https://github.com/go-gitea/gitea/pull/12046
+		//  - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996
+		//    > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload
+
+		// In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload)
+		return convertUnmarshalledJSON(rc.IssueComment, data)
 	case webhook_module.HookEventPush:
-		return s.Push(p.(*api.PushPayload))
+		return convertUnmarshalledJSON(rc.Push, data)
 	case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
 		webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
-		return s.PullRequest(p.(*api.PullRequestPayload))
+		return convertUnmarshalledJSON(rc.PullRequest, data)
 	case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
-		return s.Review(p.(*api.PullRequestPayload), event)
+		return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) {
+			return rc.Review(p, event)
+		}, data)
 	case webhook_module.HookEventRepository:
-		return s.Repository(p.(*api.RepositoryPayload))
+		return convertUnmarshalledJSON(rc.Repository, data)
 	case webhook_module.HookEventRelease:
-		return s.Release(p.(*api.ReleasePayload))
+		return convertUnmarshalledJSON(rc.Release, data)
 	case webhook_module.HookEventWiki:
-		return s.Wiki(p.(*api.WikiPayload))
+		return convertUnmarshalledJSON(rc.Wiki, data)
 	case webhook_module.HookEventPackage:
-		return s.Package(p.(*api.PackagePayload))
+		return convertUnmarshalledJSON(rc.Package, data)
 	}
-	return s, nil
+	var t T
+	return t, fmt.Errorf("newPayload unsupported event: %s", event)
+}
+
+func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
+	payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	body, err := json.MarshalIndent(payload, "", "  ")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := http.NewRequest(w.HTTPMethod, w.URL, bytes.NewReader(body))
+	if err != nil {
+		return nil, nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	if withDefaultHeaders {
+		return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+	}
+	return req, body, nil
 }
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 945b0662d8..ba8bac27d9 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -4,8 +4,9 @@
 package webhook
 
 import (
-	"errors"
+	"context"
 	"fmt"
+	"net/http"
 	"regexp"
 	"strings"
 
@@ -39,7 +40,6 @@ func GetSlackHook(w *webhook_model.Webhook) *SlackMeta {
 type SlackPayload struct {
 	Channel     string            `json:"channel"`
 	Text        string            `json:"text"`
-	Color       string            `json:"-"`
 	Username    string            `json:"username"`
 	IconURL     string            `json:"icon_url"`
 	UnfurlLinks int               `json:"unfurl_links"`
@@ -56,15 +56,6 @@ type SlackAttachment struct {
 	Text      string `json:"text"`
 }
 
-// JSONPayload Marshals the SlackPayload to json
-func (s *SlackPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(s, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
 // SlackTextFormatter replaces &, <, > with HTML characters
 // see: https://api.slack.com/docs/formatting
 func SlackTextFormatter(s string) string {
@@ -98,10 +89,8 @@ func SlackLinkToRef(repoURL, ref string) string {
 	return SlackLinkFormatter(url, refName)
 }
 
-var _ PayloadConvertor = &SlackPayload{}
-
-// Create implements PayloadConvertor Create method
-func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+// Create implements payloadConvertor Create method
+func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) {
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
 	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
@@ -110,7 +99,7 @@ func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete composes Slack payload for delete a branch or tag.
-func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (s slackConvertor) Delete(p *api.DeletePayload) (SlackPayload, error) {
 	refName := git.RefName(p.Ref).ShortName()
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
@@ -119,7 +108,7 @@ func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork composes Slack payload for forked by a repository.
-func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (s slackConvertor) Fork(p *api.ForkPayload) (SlackPayload, error) {
 	baseLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
 	forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
@@ -127,8 +116,8 @@ func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
 	return s.createPayload(text, nil), nil
 }
 
-// Issue implements PayloadConvertor Issue method
-func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+// Issue implements payloadConvertor Issue method
+func (s slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) {
 	text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true)
 
 	var attachments []SlackAttachment
@@ -146,8 +135,8 @@ func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
 	return s.createPayload(text, attachments), nil
 }
 
-// IssueComment implements PayloadConvertor IssueComment method
-func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+// IssueComment implements payloadConvertor IssueComment method
+func (s slackConvertor) IssueComment(p *api.IssueCommentPayload) (SlackPayload, error) {
 	text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, []SlackAttachment{{
@@ -158,28 +147,28 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader,
 	}}), nil
 }
 
-// Wiki implements PayloadConvertor Wiki method
-func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+// Wiki implements payloadConvertor Wiki method
+func (s slackConvertor) Wiki(p *api.WikiPayload) (SlackPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, nil), nil
 }
 
-// Release implements PayloadConvertor Release method
-func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+// Release implements payloadConvertor Release method
+func (s slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) {
 	text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, nil), nil
 }
 
-func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
 	text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, nil), nil
 }
 
-// Push implements PayloadConvertor Push method
-func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+// Push implements payloadConvertor Push method
+func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
 	// n new commits
 	var (
 		commitDesc   string
@@ -219,8 +208,8 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 	}}), nil
 }
 
-// PullRequest implements PayloadConvertor PullRequest method
-func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+// PullRequest implements payloadConvertor PullRequest method
+func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) {
 	text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true)
 
 	var attachments []SlackAttachment
@@ -238,8 +227,8 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er
 	return s.createPayload(text, attachments), nil
 }
 
-// Review implements PayloadConvertor Review method
-func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+// Review implements payloadConvertor Review method
+func (s slackConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (SlackPayload, error) {
 	senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
 	title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
 	titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
@@ -250,7 +239,7 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return SlackPayload{}, err
 		}
 
 		text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink)
@@ -259,8 +248,8 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
 	return s.createPayload(text, nil), nil
 }
 
-// Repository implements PayloadConvertor Repository method
-func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+// Repository implements payloadConvertor Repository method
+func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, error) {
 	senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
 	repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
 	var text string
@@ -275,8 +264,8 @@ func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, erro
 	return s.createPayload(text, nil), nil
 }
 
-func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) *SlackPayload {
-	return &SlackPayload{
+func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
+	return SlackPayload{
 		Channel:     s.Channel,
 		Text:        text,
 		Username:    s.Username,
@@ -285,21 +274,27 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment)
 	}
 }
 
-// GetSlackPayload converts a slack webhook into a SlackPayload
-func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(SlackPayload)
+type slackConvertor struct {
+	Channel  string
+	Username string
+	IconURL  string
+	Color    string
+}
 
-	slack := &SlackMeta{}
-	if err := json.Unmarshal([]byte(meta), &slack); err != nil {
-		return s, errors.New("GetSlackPayload meta json:" + err.Error())
+var _ payloadConvertor[SlackPayload] = slackConvertor{}
+
+func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &SlackMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err)
 	}
-
-	s.Channel = slack.Channel
-	s.Username = slack.Username
-	s.IconURL = slack.IconURL
-	s.Color = slack.Color
-
-	return convertPayloader(s, p, event)
+	sc := slackConvertor{
+		Channel:  meta.Channel,
+		Username: meta.Username,
+		IconURL:  meta.IconURL,
+		Color:    meta.Color,
+	}
+	return newJSONRequest(sc, w, t, true)
 }
 
 var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go
index b1340963e2..7ebf16aba2 100644
--- a/services/webhook/slack_test.go
+++ b/services/webhook/slack_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,201 +17,180 @@ import (
 )
 
 func TestSlackPayload(t *testing.T) {
+	sc := slackConvertor{}
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Create(p)
+		pl, err := sc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] branch created by user1", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] branch created by user1", pl.Text)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Delete(p)
+		pl, err := sc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:test] branch deleted by user1", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:test] branch deleted by user1", pl.Text)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Fork(p)
+		pl, err := sc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "<http://localhost:3000/test/repo2|test/repo2> is forked to <http://localhost:3000/test/repo|test/repo>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "<http://localhost:3000/test/repo2|test/repo2> is forked to <http://localhost:3000/test/repo|test/repo>", pl.Text)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Push(p)
+		pl, err := sc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", pl.Text)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(SlackPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := sc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue opened: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue opened: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = sc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue closed: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue closed: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := sc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on issue <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on issue <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := sc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request opened: <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request opened: <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := sc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on pull request <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on pull request <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(SlackPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := sc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Repository(p)
+		pl, err := sc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Package(p)
+		pl, err := sc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "Package created: <http://localhost:3000/user1/-/packages/container/GiteaContainer/latest|GiteaContainer:latest> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "Package created: <http://localhost:3000/user1/-/packages/container/GiteaContainer/latest|GiteaContainer:latest> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(SlackPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := sc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New wiki page '<http://localhost:3000/test/repo/wiki/index|index>' (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New wiki page '<http://localhost:3000/test/repo/wiki/index|index>' (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.Text)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = sc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' edited (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' edited (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.Text)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = sc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' deleted by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' deleted by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Release(p)
+		pl, err := sc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 }
 
 func TestSlackJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(SlackPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &SlackPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.SLACK,
+		URL:        "https://slack.example.com/",
+		Meta:       `{}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newSlackRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://slack.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body SlackPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", body.Text)
 }
 
 func TestIsValidSlackChannel(t *testing.T) {
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 1bdc74e183..e4a5b5a424 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -4,14 +4,15 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"strings"
 
 	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/markup"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
@@ -41,22 +42,8 @@ func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta {
 	return s
 }
 
-var _ PayloadConvertor = &TelegramPayload{}
-
-// JSONPayload Marshals the TelegramPayload to json
-func (t *TelegramPayload) JSONPayload() ([]byte, error) {
-	t.ParseMode = "HTML"
-	t.DisableWebPreview = true
-	t.Message = markup.Sanitize(t.Message)
-	data, err := json.MarshalIndent(t, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
 // Create implements PayloadConvertor Create method
-func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (t telegramConvertor) Create(p *api.CreatePayload) (TelegramPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf(`[<a href="%s">%s</a>] %s <a href="%s">%s</a> created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType,
@@ -66,7 +53,7 @@ func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (t telegramConvertor) Delete(p *api.DeletePayload) (TelegramPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf(`[<a href="%s">%s</a>] %s <a href="%s">%s</a> deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType,
@@ -76,14 +63,14 @@ func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (t *TelegramPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (t telegramConvertor) Fork(p *api.ForkPayload) (TelegramPayload, error) {
 	title := fmt.Sprintf(`%s is forked to <a href="%s">%s</a>`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName)
 
 	return createTelegramPayload(title), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -121,34 +108,34 @@ func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (t *TelegramPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (t telegramConvertor) Issue(p *api.IssuePayload) (TelegramPayload, error) {
 	text, _, attachmentText, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text + "\n\n" + attachmentText), nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (t *TelegramPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (t telegramConvertor) IssueComment(p *api.IssueCommentPayload) (TelegramPayload, error) {
 	text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text + "\n" + p.Comment.Body), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (t *TelegramPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (t telegramConvertor) PullRequest(p *api.PullRequestPayload) (TelegramPayload, error) {
 	text, _, attachmentText, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text + "\n" + attachmentText), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (t telegramConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (TelegramPayload, error) {
 	var text, attachmentText string
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return TelegramPayload{}, err
 		}
 
 		text = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -159,7 +146,7 @@ func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module
 }
 
 // Repository implements PayloadConvertor Repository method
-func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload, error) {
 	var title string
 	switch p.Action {
 	case api.HookRepoCreated:
@@ -169,36 +156,39 @@ func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
 		title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
 		return createTelegramPayload(title), nil
 	}
-	return nil, nil
+	return TelegramPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (t *TelegramPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (t telegramConvertor) Wiki(p *api.WikiPayload) (TelegramPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text), nil
 }
 
 // Release implements PayloadConvertor Release method
-func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (t telegramConvertor) Release(p *api.ReleasePayload) (TelegramPayload, error) {
 	text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text), nil
 }
 
-func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, error) {
 	text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text), nil
 }
 
-// GetTelegramPayload converts a telegram webhook into a TelegramPayload
-func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(TelegramPayload), p, event)
-}
-
-func createTelegramPayload(message string) *TelegramPayload {
-	return &TelegramPayload{
+func createTelegramPayload(message string) TelegramPayload {
+	return TelegramPayload{
 		Message: strings.TrimSpace(message),
 	}
 }
+
+type telegramConvertor struct{}
+
+var _ payloadConvertor[TelegramPayload] = telegramConvertor{}
+
+func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(telegramConvertor{}, w, t, true)
+}
diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go
index 5b9927d057..27ab96cd09 100644
--- a/services/webhook/telegram_test.go
+++ b/services/webhook/telegram_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,199 +17,177 @@ import (
 )
 
 func TestTelegramPayload(t *testing.T) {
+	tc := telegramConvertor{}
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Create(p)
+		pl, err := tc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> created`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> created`, pl.Message)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Delete(p)
+		pl, err := tc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> deleted`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> deleted`, pl.Message)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Fork(p)
+		pl, err := tc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `test/repo2 is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `test/repo2 is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.Message)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Push(p)
+		pl, err := tc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1", pl.Message)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(TelegramPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := tc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Issue opened: <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\n\nissue body", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Issue opened: <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\n\nissue body", pl.Message)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = tc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := tc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on issue <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nmore info needed", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on issue <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nmore info needed", pl.Message)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := tc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Pull request opened: <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nfixes bug #2", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Pull request opened: <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nfixes bug #2", pl.Message)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := tc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on pull request <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nchanges requested", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on pull request <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nchanges requested", pl.Message)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(TelegramPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.Message)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Repository(p)
+		pl, err := tc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created`, pl.Message)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Package(p)
+		pl, err := tc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `Package created: <a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer:latest</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `Package created: <a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer:latest</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(TelegramPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := tc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = tc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = tc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Release(p)
+		pl, err := tc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 }
 
 func TestTelegramJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(TelegramPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &TelegramPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.TELEGRAM,
+		URL:        "https://telegram.example.com/",
+		Meta:       ``,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newTelegramRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://telegram.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body TelegramPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1", body.Message)
 }
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index 35c760dc62..e0e8fa2fc1 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net/http"
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
@@ -26,48 +27,16 @@ import (
 	"github.com/gobwas/glob"
 )
 
-type webhook struct {
-	name           webhook_module.HookType
-	payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error)
-}
-
-var webhooks = map[webhook_module.HookType]*webhook{
-	webhook_module.SLACK: {
-		name:           webhook_module.SLACK,
-		payloadCreator: GetSlackPayload,
-	},
-	webhook_module.DISCORD: {
-		name:           webhook_module.DISCORD,
-		payloadCreator: GetDiscordPayload,
-	},
-	webhook_module.DINGTALK: {
-		name:           webhook_module.DINGTALK,
-		payloadCreator: GetDingtalkPayload,
-	},
-	webhook_module.TELEGRAM: {
-		name:           webhook_module.TELEGRAM,
-		payloadCreator: GetTelegramPayload,
-	},
-	webhook_module.MSTEAMS: {
-		name:           webhook_module.MSTEAMS,
-		payloadCreator: GetMSTeamsPayload,
-	},
-	webhook_module.FEISHU: {
-		name:           webhook_module.FEISHU,
-		payloadCreator: GetFeishuPayload,
-	},
-	webhook_module.MATRIX: {
-		name:           webhook_module.MATRIX,
-		payloadCreator: GetMatrixPayload,
-	},
-	webhook_module.WECHATWORK: {
-		name:           webhook_module.WECHATWORK,
-		payloadCreator: GetWechatworkPayload,
-	},
-	webhook_module.PACKAGIST: {
-		name:           webhook_module.PACKAGIST,
-		payloadCreator: GetPackagistPayload,
-	},
+var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){
+	webhook_module.SLACK:      newSlackRequest,
+	webhook_module.DISCORD:    newDiscordRequest,
+	webhook_module.DINGTALK:   newDingtalkRequest,
+	webhook_module.TELEGRAM:   newTelegramRequest,
+	webhook_module.MSTEAMS:    newMSTeamsRequest,
+	webhook_module.FEISHU:     newFeishuRequest,
+	webhook_module.MATRIX:     newMatrixRequest,
+	webhook_module.WECHATWORK: newWechatworkRequest,
+	webhook_module.PACKAGIST:  newPackagistRequest,
 }
 
 // IsValidHookTaskType returns true if a webhook registered
@@ -75,7 +44,7 @@ func IsValidHookTaskType(name string) bool {
 	if name == webhook_module.GITEA || name == webhook_module.GOGS {
 		return true
 	}
-	_, ok := webhooks[name]
+	_, ok := webhookRequesters[name]
 	return ok
 }
 
@@ -159,7 +128,9 @@ func checkBranch(w *webhook_model.Webhook, branch string) bool {
 	return g.Match(branch)
 }
 
-// PrepareWebhook creates a hook task and enqueues it for processing
+// PrepareWebhook creates a hook task and enqueues it for processing.
+// The payload is saved as-is. The adjustments depending on the webhook type happen
+// right before delivery, in the [Deliver] method.
 func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
 	// Skip sending if webhooks are disabled.
 	if setting.DisableWebhooks {
@@ -193,25 +164,19 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
 		}
 	}
 
-	var payloader api.Payloader
-	var err error
-	webhook, ok := webhooks[w.Type]
-	if ok {
-		payloader, err = webhook.payloadCreator(p, event, w.Meta)
-		if err != nil {
-			return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err)
-		}
-	} else {
-		payloader = p
+	payload, err := p.JSONPayload()
+	if err != nil {
+		return fmt.Errorf("JSONPayload for %s: %w", event, err)
 	}
 
 	task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
-		HookID:    w.ID,
-		Payloader: payloader,
-		EventType: event,
+		HookID:         w.ID,
+		PayloadContent: string(payload),
+		EventType:      event,
+		PayloadVersion: 2,
 	})
 	if err != nil {
-		return fmt.Errorf("CreateHookTask: %w", err)
+		return fmt.Errorf("CreateHookTask for %s: %w", event, err)
 	}
 
 	return enqueueHookTask(task.ID)
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 338b94360b..5f5c146232 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -77,7 +77,3 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
 		unittest.AssertNotExistsBean(t, hookTask)
 	}
 }
-
-// TODO TestHookTask_deliver
-
-// TODO TestDeliverHooks
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 80245c7e77..46e7856ecf 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -4,11 +4,13 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
@@ -28,20 +30,8 @@ type (
 	}
 )
 
-// SetSecret sets the Wechatwork secret
-func (f *WechatworkPayload) SetSecret(_ string) {}
-
-// JSONPayload Marshals the WechatworkPayload to json
-func (f *WechatworkPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(f, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-func newWechatworkMarkdownPayload(title string) *WechatworkPayload {
-	return &WechatworkPayload{
+func newWechatworkMarkdownPayload(title string) WechatworkPayload {
+	return WechatworkPayload{
 		Msgtype: "markdown",
 		Markdown: struct {
 			Content string `json:"content"`
@@ -51,10 +41,8 @@ func newWechatworkMarkdownPayload(title string) *WechatworkPayload {
 	}
 }
 
-var _ PayloadConvertor = &WechatworkPayload{}
-
 // Create implements PayloadConvertor Create method
-func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Create(p *api.CreatePayload) (WechatworkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -63,7 +51,7 @@ func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error)
 }
 
 // Delete implements PayloadConvertor Delete method
-func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Delete(p *api.DeletePayload) (WechatworkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -72,14 +60,14 @@ func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error)
 }
 
 // Fork implements PayloadConvertor Fork method
-func (f *WechatworkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Fork(p *api.ForkPayload) (WechatworkPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return newWechatworkMarkdownPayload(title), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -108,7 +96,7 @@ func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Issue(p *api.IssuePayload) (WechatworkPayload, error) {
 	text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
 	var content string
 	content += fmt.Sprintf(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\"> %s</font> \n [%s](%s)", text, attachmentText, issueTitle, p.Issue.HTMLURL, p.Issue.HTMLURL)
@@ -117,7 +105,7 @@ func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) IssueComment(p *api.IssueCommentPayload) (WechatworkPayload, error) {
 	text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
 	var content string
 	content += fmt.Sprintf(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\">%s</font> \n [%s](%s)", text, p.Comment.Body, issueTitle, p.Comment.HTMLURL, p.Comment.HTMLURL)
@@ -126,7 +114,7 @@ func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloa
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) PullRequest(p *api.PullRequestPayload) (WechatworkPayload, error) {
 	text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
 	pr := fmt.Sprintf("> <font color=\"info\"> %s </font> \r\n > <font color=\"comment\">%s </font> \r\n > <font color=\"comment\">%s </font> \r\n",
 		text, issueTitle, attachmentText)
@@ -135,13 +123,13 @@ func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloade
 }
 
 // Review implements PayloadConvertor Review method
-func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (wc wechatworkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (WechatworkPayload, error) {
 	var text, title string
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return WechatworkPayload{}, err
 		}
 		title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
 		text = p.Review.Content
@@ -151,7 +139,7 @@ func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_modu
 }
 
 // Repository implements PayloadConvertor Repository method
-func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPayload, error) {
 	var title string
 	switch p.Action {
 	case api.HookRepoCreated:
@@ -162,30 +150,33 @@ func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader,
 		return newWechatworkMarkdownPayload(title), nil
 	}
 
-	return nil, nil
+	return WechatworkPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (f *WechatworkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Wiki(p *api.WikiPayload) (WechatworkPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
 
 	return newWechatworkMarkdownPayload(text), nil
 }
 
 // Release implements PayloadConvertor Release method
-func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Release(p *api.ReleasePayload) (WechatworkPayload, error) {
 	text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
 
 	return newWechatworkMarkdownPayload(text), nil
 }
 
-func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, error) {
 	text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
 
 	return newWechatworkMarkdownPayload(text), nil
 }
 
-// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
-func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(WechatworkPayload), p, event)
+type wechatworkConvertor struct{}
+
+var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
+
+func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(wechatworkConvertor{}, w, t, true)
 }
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl
index 3c21a42421..4e0f0e9c3e 100644
--- a/templates/repo/settings/webhook/history.tmpl
+++ b/templates/repo/settings/webhook/history.tmpl
@@ -19,6 +19,8 @@
 						<div class="flex-text-inline">
 							{{if .IsSucceed}}
 								<span class="text green">{{svg "octicon-check"}}</span>
+							{{else if not .IsDelivered}}
+								<span class="text orange">{{svg "octicon-stopwatch"}}</span>
 							{{else}}
 								<span class="text red">{{svg "octicon-alert"}}</span>
 							{{end}}
@@ -62,7 +64,7 @@
 {{range $key, $val := .RequestInfo.Headers}}<strong>{{$key}}:</strong> {{$val}}
 {{end}}</pre>
 								<h5>{{ctx.Locale.Tr "repo.settings.webhook.payload"}}</h5>
-								<pre class="webhook-info"><code class="json">{{.PayloadContent}}</code></pre>
+								<pre class="webhook-info"><code class="json">{{or .RequestInfo.Body .PayloadContent}}</code></pre>
 							{{else}}
 								-
 							{{end}}

From 29a8c8de779924694fadad80b31cc855dd62c0f2 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 8 Mar 2024 11:19:35 +0800
Subject: [PATCH 306/679] Partially enable MSSQL case-sensitive collation
 support (#29238)

Follow #28662
---
 go.mod                                 | 10 +++++-----
 go.sum                                 | 20 ++++++++++----------
 models/db/collation.go                 |  3 +--
 models/project/board.go                |  2 +-
 tests/integration/db_collation_test.go |  6 ------
 5 files changed, 17 insertions(+), 24 deletions(-)

diff --git a/go.mod b/go.mod
index d58890de28..dfd9a95ea3 100644
--- a/go.mod
+++ b/go.mod
@@ -49,7 +49,7 @@ require (
 	github.com/go-ldap/ldap/v3 v3.4.6
 	github.com/go-sql-driver/mysql v1.7.1
 	github.com/go-swagger/go-swagger v0.30.5
-	github.com/go-testfixtures/testfixtures/v3 v3.9.0
+	github.com/go-testfixtures/testfixtures/v3 v3.10.0
 	github.com/go-webauthn/webauthn v0.10.0
 	github.com/gobwas/glob v0.2.3
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
@@ -57,7 +57,7 @@ require (
 	github.com/golang-jwt/jwt/v5 v5.2.0
 	github.com/google/go-github/v57 v57.0.0
 	github.com/google/pprof v0.0.0-20240117000934-35fc243c5815
-	github.com/google/uuid v1.5.0
+	github.com/google/uuid v1.6.0
 	github.com/gorilla/feeds v1.1.2
 	github.com/gorilla/sessions v1.2.2
 	github.com/hashicorp/go-version v1.6.0
@@ -73,7 +73,7 @@ require (
 	github.com/lib/pq v1.10.9
 	github.com/markbates/goth v1.78.0
 	github.com/mattn/go-isatty v0.0.20
-	github.com/mattn/go-sqlite3 v1.14.19
+	github.com/mattn/go-sqlite3 v1.14.22
 	github.com/meilisearch/meilisearch-go v0.26.1
 	github.com/mholt/archiver/v3 v3.5.1
 	github.com/microcosm-cc/bluemonday v1.0.26
@@ -129,7 +129,7 @@ require (
 	dario.cat/mergo v1.0.0 // indirect
 	git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
 	github.com/ClickHouse/ch-go v0.61.1 // indirect
-	github.com/ClickHouse/clickhouse-go/v2 v2.17.1 // indirect
+	github.com/ClickHouse/clickhouse-go/v2 v2.18.0 // indirect
 	github.com/DataDog/zstd v1.5.5 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.2.1 // indirect
@@ -241,7 +241,7 @@ require (
 	github.com/oklog/ulid v1.3.1 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
-	github.com/paulmach/orb v0.11.0 // indirect
+	github.com/paulmach/orb v0.11.1 // indirect
 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
 	github.com/pierrec/lz4/v4 v4.1.21 // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
diff --git a/go.sum b/go.sum
index 87072571e5..0d8e7dc699 100644
--- a/go.sum
+++ b/go.sum
@@ -78,8 +78,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/ClickHouse/ch-go v0.61.1 h1:j5rx3qnvcnYjhnP1IdXE/vdIRQiqgwAzyqOaasA6QCw=
 github.com/ClickHouse/ch-go v0.61.1/go.mod h1:myxt/JZgy2BYHFGQqzmaIpbfr5CMbs3YHVULaWQj5YU=
-github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4=
-github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ=
+github.com/ClickHouse/clickhouse-go/v2 v2.18.0 h1:O1LicIeg2JS2V29fKRH4+yT3f6jvvcJBm506dpVQ4mQ=
+github.com/ClickHouse/clickhouse-go/v2 v2.18.0/go.mod h1:ztQvX6wm7kAbhJslS87EXEhOVNY/TObXwyURnGju5FQ=
 github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
 github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
 github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
@@ -384,8 +384,8 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.m
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
 github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY=
-github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s=
+github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc=
+github.com/go-testfixtures/testfixtures/v3 v3.10.0/go.mod h1:z8RoleoNtibi6Ar8ziCW7e6PQ+jWiqbUWvuv8AMe4lo=
 github.com/go-webauthn/webauthn v0.10.0 h1:yuW2e1tXnRAwAvKrR4q4LQmc6XtCMH639/ypZGhZCwk=
 github.com/go-webauthn/webauthn v0.10.0/go.mod h1:l0NiauXhL6usIKqNLCUM3Qir43GK7ORg8ggold0Uv/Y=
 github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ=
@@ -488,8 +488,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -643,8 +643,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
-github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A=
 github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
 github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
@@ -718,8 +718,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
 github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
-github.com/paulmach/orb v0.11.0 h1:JfVXJUBeH9ifc/OrhBY0lL16QsmPgpCHMlqSSYhcgAA=
-github.com/paulmach/orb v0.11.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
+github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
+github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
 github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
diff --git a/models/db/collation.go b/models/db/collation.go
index 2f5ff2bf05..c128cf5029 100644
--- a/models/db/collation.go
+++ b/models/db/collation.go
@@ -166,8 +166,7 @@ func preprocessDatabaseCollation(x *xorm.Engine) {
 
 	// try to alter database collation to expected if the database is empty, it might fail in some cases (and it isn't necessary to succeed)
 	// at the moment, there is no "altering" solution for MSSQL, site admin should manually change the database collation
-	// and there is a bug https://github.com/go-testfixtures/testfixtures/pull/182 mssql: Invalid object name 'information_schema.tables'.
-	if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 && x.Dialect().URI().DBType == schemas.MYSQL {
+	if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 {
 		if err = alterDatabaseCollation(x, r.ExpectedCollation); err != nil {
 			log.Error("Failed to change database collation to %q: %v", r.ExpectedCollation, err)
 		} else {
diff --git a/models/project/board.go b/models/project/board.go
index 3e2d8e0472..c0e6529880 100644
--- a/models/project/board.go
+++ b/models/project/board.go
@@ -232,7 +232,7 @@ func UpdateBoard(ctx context.Context, board *Board) error {
 func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
 	boards := make([]*Board, 0, 5)
 
-	if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("Sorting").Find(&boards); err != nil {
+	if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
 		return nil, err
 	}
 
diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go
index 468d13508d..75a4c1594f 100644
--- a/tests/integration/db_collation_test.go
+++ b/tests/integration/db_collation_test.go
@@ -22,12 +22,6 @@ type TestCollationTbl struct {
 func TestDatabaseCollation(t *testing.T) {
 	x := db.GetEngine(db.DefaultContext).(*xorm.Engine)
 
-	// there are blockers for MSSQL to use case-sensitive collation, see the comments in db/collation.go
-	if setting.Database.Type.IsMSSQL() {
-		t.Skip("there are blockers for MSSQL to use case-sensitive collation")
-		return
-	}
-
 	// all created tables should use case-sensitive collation by default
 	_, _ = x.Exec("DROP TABLE IF EXISTS test_collation_tbl")
 	err := x.Sync(&TestCollationTbl{})

From ce8a98f8789a7e4e9ee97ab0abac6064d78fb1f6 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 8 Mar 2024 12:28:21 +0800
Subject: [PATCH 307/679] Fix 500 when deleting account with incorrect password
 or unsupported login type (#29579)

Fix #26210

---------

Co-authored-by: Jason Song <i@wolfogre.com>
---
 options/locale/locale_en-US.ini     |  2 ++
 routers/web/user/setting/account.go | 19 +++++++++++++++++--
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index d30c8e521d..e7ba7dd8c9 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -584,6 +584,8 @@ enterred_invalid_repo_name = The repository name you entered is incorrect.
 enterred_invalid_org_name = The organization name you entered is incorrect.
 enterred_invalid_owner_name = The new owner name is not valid.
 enterred_invalid_password = The password you entered is incorrect.
+unset_password = The login user has not set the password.
+unsupported_login_type = The login type is not supported to delete account.
 user_not_exist = The user does not exist.
 team_not_exist = The team does not exist.
 last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization.
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index abb5873e98..d69bda6663 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -19,6 +19,8 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/db"
+	"code.gitea.io/gitea/services/auth/source/smtp"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -242,11 +244,24 @@ func DeleteAccount(ctx *context.Context) {
 	ctx.Data["PageIsSettingsAccount"] = true
 
 	if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil {
-		if user_model.IsErrUserNotExist(err) {
+		switch {
+		case user_model.IsErrUserNotExist(err):
+			loadAccountData(ctx)
+
+			ctx.RenderWithErr(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil)
+		case errors.Is(err, smtp.ErrUnsupportedLoginType):
+			loadAccountData(ctx)
+
+			ctx.RenderWithErr(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil)
+		case errors.As(err, &db.ErrUserPasswordNotSet{}):
+			loadAccountData(ctx)
+
+			ctx.RenderWithErr(ctx.Tr("form.unset_password"), tplSettingsAccount, nil)
+		case errors.As(err, &db.ErrUserPasswordInvalid{}):
 			loadAccountData(ctx)
 
 			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
-		} else {
+		default:
 			ctx.ServerError("UserSignIn", err)
 		}
 		return

From 7cf7a499be80931ae34588201e9605085898e9b8 Mon Sep 17 00:00:00 2001
From: charles <30816317+charles7668@users.noreply.github.com>
Date: Fri, 8 Mar 2024 13:02:13 +0800
Subject: [PATCH 308/679] Fixing the issue when status check per rule matches
 multiple actions (#29631)

Close #29628
rule
```
Test / Build*
Test / Build *
Test / Build 2*
Test / Build 1*
```

![image](https://github.com/go-gitea/gitea/assets/30816317/19bef0a9-fa97-43c5-887b-dece76064aa8)
rule2
```
Test / Build*
Test / Build 1*
```

![image](https://github.com/go-gitea/gitea/assets/30816317/19bef0a9-fa97-43c5-887b-dece76064aa8)

rule3
```
Test / Build*
Test / Build 1*
NotExist*
```

![image](https://github.com/go-gitea/gitea/assets/30816317/f6a5e832-2e1b-4049-915b-45bec5ef070c)

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
---
 services/pull/commit_status.go      | 18 ++++----
 services/pull/commit_status_test.go | 65 +++++++++++++++++++++++++++++
 2 files changed, 76 insertions(+), 7 deletions(-)
 create mode 100644 services/pull/commit_status_test.go

diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index 3282f4f379..07e9eb7959 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -35,9 +35,9 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
 			}
 		}
 
-		for _, commitStatus := range commitStatuses {
+		for _, gp := range requiredContextsGlob {
 			var targetStatus structs.CommitStatusState
-			for _, gp := range requiredContextsGlob {
+			for _, commitStatus := range commitStatuses {
 				if gp.Match(commitStatus.Context) {
 					targetStatus = commitStatus.State
 					matchedCount++
@@ -45,16 +45,20 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
 				}
 			}
 
-			if targetStatus != "" && targetStatus.NoBetterThan(returnedStatus) {
+			// If required rule not match any action, then it is pending
+			if targetStatus == "" {
+				if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
+					returnedStatus = structs.CommitStatusPending
+				}
+				break
+			}
+
+			if targetStatus.NoBetterThan(returnedStatus) {
 				returnedStatus = targetStatus
 			}
 		}
 	}
 
-	if matchedCount != len(requiredContexts) {
-		return structs.CommitStatusPending
-	}
-
 	if matchedCount == 0 {
 		status := git_model.CalcCommitStatus(commitStatuses)
 		if status != nil {
diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go
new file mode 100644
index 0000000000..592acdd55c
--- /dev/null
+++ b/services/pull/commit_status_test.go
@@ -0,0 +1,65 @@
+// Copyright 2024 The Gitea Authors.
+// All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+	"testing"
+
+	git_model "code.gitea.io/gitea/models/git"
+	"code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMergeRequiredContextsCommitStatus(t *testing.T) {
+	testCases := [][]*git_model.CommitStatus{
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 3", State: structs.CommitStatusSuccess},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusPending},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusFailure},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusSuccess},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusSuccess},
+		},
+	}
+	testCasesRequiredContexts := [][]string{
+		{"Build*"},
+		{"Build*", "Build 2t*"},
+		{"Build*", "Build 2t*"},
+		{"Build*", "Build 2t*", "Build 3*"},
+		{"Build*", "Build *", "Build 2t*", "Build 1*"},
+	}
+
+	testCasesExpected := []structs.CommitStatusState{
+		structs.CommitStatusSuccess,
+		structs.CommitStatusPending,
+		structs.CommitStatusFailure,
+		structs.CommitStatusPending,
+		structs.CommitStatusSuccess,
+	}
+
+	for i, commitStatuses := range testCases {
+		if MergeRequiredContextsCommitStatus(commitStatuses, testCasesRequiredContexts[i]) != testCasesExpected[i] {
+			assert.Fail(t, "Test case failed", "Test case %d failed", i+1)
+		}
+	}
+}

From c8f4897f7f5de5b391be806f4738de1f0d9c4c09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?=
 <72873130+zokkis@users.noreply.github.com>
Date: Fri, 8 Mar 2024 06:36:27 +0100
Subject: [PATCH 309/679] Filter for default-branch selection (#29388)

Filter for default-branch selection (fixes #4751)

before:

![image](https://github.com/go-gitea/gitea/assets/72873130/dcae266d-2e04-41bf-8739-64a85c9007f6)

after:

![image](https://github.com/go-gitea/gitea/assets/72873130/5f27c0a7-1d30-4ccd-b4bb-6c34fff1b79f)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 templates/repo/settings/branches.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index 78421ec009..73aff887f3 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -16,7 +16,7 @@
 					{{.CsrfTokenHtml}}
 					<input type="hidden" name="action" value="default_branch">
 					{{if not .Repository.IsEmpty}}
-						<div class="ui dropdown selection gt-f1 gt-mr-3 tw-max-w-96">
+						<div class="ui dropdown selection search gt-f1 gt-mr-3 tw-max-w-96">
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 							<input type="hidden" name="branch" value="{{.Repository.DefaultBranch}}">
 							<div class="default text">{{.Repository.DefaultBranch}}</div>

From a1f5dd767729e30d07ab42fda80c19f30a72679f Mon Sep 17 00:00:00 2001
From: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Date: Fri, 8 Mar 2024 14:14:35 +0800
Subject: [PATCH 310/679] Make runs-on support variable expression (#29468)

As title.
Close issue: https://gitea.com/gitea/act_runner/issues/445
Follow: https://gitea.com/gitea/act/pulls/91

Move `getSecretsOfTask` and `getVariablesOfTask` under `models` because
of circular dependency issues.
---
 go.mod                              |  2 +-
 go.sum                              |  4 +-
 models/actions/variable.go          | 33 ++++++++++++
 models/secret/secret.go             | 39 ++++++++++++++
 routers/api/actions/runner/utils.go | 80 +++++------------------------
 services/actions/notifier_helper.go | 13 ++++-
 6 files changed, 99 insertions(+), 72 deletions(-)

diff --git a/go.mod b/go.mod
index dfd9a95ea3..9b70a191ce 100644
--- a/go.mod
+++ b/go.mod
@@ -302,7 +302,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
 
 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
 
-replace github.com/nektos/act => gitea.com/gitea/act v0.2.51
+replace github.com/nektos/act => gitea.com/gitea/act v0.259.1
 
 replace github.com/gorilla/feeds => github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5
 
diff --git a/go.sum b/go.sum
index 0d8e7dc699..a44809dde5 100644
--- a/go.sum
+++ b/go.sum
@@ -48,8 +48,8 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
 git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
-gitea.com/gitea/act v0.2.51 h1:gXc/B4OlTciTTzAx9cmNyw04n2SDO7exPjAsR5Idu+c=
-gitea.com/gitea/act v0.2.51/go.mod h1:CoaX2053jqBlD6JMgu4d4UgFL/rp2I14Kt5mMqcs0Z0=
+gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw=
+gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8=
 gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo=
 gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc=
 gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0=
diff --git a/models/actions/variable.go b/models/actions/variable.go
index 12717e0ae4..14ded60fac 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -82,3 +83,35 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error)
 		})
 	return count != 0, err
 }
+
+func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
+	variables := map[string]string{}
+
+	// Global
+	globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{})
+	if err != nil {
+		log.Error("find global variables: %v", err)
+		return nil, err
+	}
+
+	// Org / User level
+	ownerVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{OwnerID: run.Repo.OwnerID})
+	if err != nil {
+		log.Error("find variables of org: %d, error: %v", run.Repo.OwnerID, err)
+		return nil, err
+	}
+
+	// Repo level
+	repoVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{RepoID: run.RepoID})
+	if err != nil {
+		log.Error("find variables of repo: %d, error: %v", run.RepoID, err)
+		return nil, err
+	}
+
+	// Level precedence: Repo > Org / User > Global
+	for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) {
+		variables[v.Name] = v.Data
+	}
+
+	return variables, nil
+}
diff --git a/models/secret/secret.go b/models/secret/secret.go
index 41e860d7f6..35bed500b9 100644
--- a/models/secret/secret.go
+++ b/models/secret/secret.go
@@ -9,7 +9,10 @@ import (
 	"fmt"
 	"strings"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	actions_module "code.gitea.io/gitea/modules/actions"
+	"code.gitea.io/gitea/modules/log"
 	secret_module "code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -112,3 +115,39 @@ func UpdateSecret(ctx context.Context, secretID int64, data string) error {
 	}
 	return err
 }
+
+func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) {
+	secrets := map[string]string{}
+
+	secrets["GITHUB_TOKEN"] = task.Token
+	secrets["GITEA_TOKEN"] = task.Token
+
+	if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
+		// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
+		// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
+		// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
+		return secrets, nil
+	}
+
+	ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
+	if err != nil {
+		log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
+		return nil, err
+	}
+	repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: task.Job.Run.RepoID})
+	if err != nil {
+		log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
+		return nil, err
+	}
+
+	for _, secret := range append(ownerSecrets, repoSecrets...) {
+		v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data)
+		if err != nil {
+			log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
+			return nil, err
+		}
+		secrets[secret.Name] = v
+	}
+
+	return secrets, nil
+}
diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go
index a7cb31288c..ff6ec5bd54 100644
--- a/routers/api/actions/runner/utils.go
+++ b/routers/api/actions/runner/utils.go
@@ -15,7 +15,6 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
-	secret_module "code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/services/actions"
 
@@ -32,14 +31,24 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
 		return nil, false, nil
 	}
 
+	secrets, err := secret_model.GetSecretsOfTask(ctx, t)
+	if err != nil {
+		return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
+	}
+
+	vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
+	if err != nil {
+		return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
+	}
+
 	actions.CreateCommitStatus(ctx, t.Job)
 
 	task := &runnerv1.Task{
 		Id:              t.ID,
 		WorkflowPayload: t.Job.WorkflowPayload,
 		Context:         generateTaskContext(t),
-		Secrets:         getSecretsOfTask(ctx, t),
-		Vars:            getVariablesOfTask(ctx, t),
+		Secrets:         secrets,
+		Vars:            vars,
 	}
 
 	if needs, err := findTaskNeeds(ctx, t); err != nil {
@@ -55,71 +64,6 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
 	return task, true, nil
 }
 
-func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
-	secrets := map[string]string{}
-
-	secrets["GITHUB_TOKEN"] = task.Token
-	secrets["GITEA_TOKEN"] = task.Token
-
-	if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
-		// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
-		// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
-		// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
-		return secrets
-	}
-
-	ownerSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
-	if err != nil {
-		log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
-		// go on
-	}
-	repoSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID})
-	if err != nil {
-		log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
-		// go on
-	}
-
-	for _, secret := range append(ownerSecrets, repoSecrets...) {
-		if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil {
-			log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
-			// go on
-		} else {
-			secrets[secret.Name] = v
-		}
-	}
-
-	return secrets
-}
-
-func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
-	variables := map[string]string{}
-
-	// Global
-	globalVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{})
-	if err != nil {
-		log.Error("find global variables: %v", err)
-	}
-
-	// Org / User level
-	ownerVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
-	if err != nil {
-		log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err)
-	}
-
-	// Repo level
-	repoVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID})
-	if err != nil {
-		log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err)
-	}
-
-	// Level precedence: Repo > Org / User > Global
-	for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) {
-		variables[v.Name] = v.Data
-	}
-
-	return variables
-}
-
 func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
 	event := map[string]any{}
 	_ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event)
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index b0d848b5ad..d84191dca2 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -296,7 +296,18 @@ func handleWorkflows(
 			run.NeedApproval = need
 		}
 
-		jobs, err := jobparser.Parse(dwf.Content)
+		if err := run.LoadAttributes(ctx); err != nil {
+			log.Error("LoadAttributes: %v", err)
+			continue
+		}
+
+		vars, err := actions_model.GetVariablesOfRun(ctx, run)
+		if err != nil {
+			log.Error("GetVariablesOfRun: %v", err)
+			continue
+		}
+
+		jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars))
 		if err != nil {
 			log.Error("jobparser.Parse: %v", err)
 			continue

From 25b842df261452a29570ba89ffc3a4842d73f68c Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 8 Mar 2024 15:30:10 +0800
Subject: [PATCH 311/679] Move get/set default branch from git package to
 gitrepo package to hide repopath (#29126)

---
 models/issues/pull.go             |  7 +------
 modules/git/repo_branch.go        | 11 ++---------
 modules/gitrepo/branch.go         | 17 +++++++++++++++++
 routers/api/v1/repo/repo.go       |  4 ++--
 routers/private/default_branch.go |  3 ++-
 routers/web/repo/wiki.go          |  2 +-
 services/context/repo.go          |  2 +-
 services/mirror/mirror_pull.go    |  2 +-
 services/repository/adopt.go      | 22 +++++++++++-----------
 services/repository/branch.go     |  4 ++--
 services/repository/create.go     |  7 +------
 services/repository/generate.go   |  7 +------
 services/repository/migrate.go    |  9 +--------
 services/repository/push.go       |  2 +-
 services/wiki/wiki.go             | 16 ++++++++--------
 15 files changed, 52 insertions(+), 63 deletions(-)

diff --git a/models/issues/pull.go b/models/issues/pull.go
index 7d299eac27..80b149da5c 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -901,12 +901,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullReque
 	}
 	defer repo.Close()
 
-	branch, err := repo.GetDefaultBranch()
-	if err != nil {
-		return err
-	}
-
-	commit, err := repo.GetBranchCommit(branch)
+	commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
 	if err != nil {
 		return err
 	}
diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go
index 979c5dec91..552ae2bb8c 100644
--- a/modules/git/repo_branch.go
+++ b/modules/git/repo_branch.go
@@ -55,15 +55,8 @@ func (repo *Repository) GetHEADBranch() (*Branch, error) {
 	}, nil
 }
 
-// SetDefaultBranch sets default branch of repository.
-func (repo *Repository) SetDefaultBranch(name string) error {
-	_, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").AddDynamicArguments(BranchPrefix + name).RunStdString(&RunOpts{Dir: repo.Path})
-	return err
-}
-
-// GetDefaultBranch gets default branch of repository.
-func (repo *Repository) GetDefaultBranch() (string, error) {
-	stdout, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repo.Path})
+func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) {
+	stdout, _, err := NewCommand(ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repoPath})
 	if err != nil {
 		return "", err
 	}
diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go
index dcaf92668d..e13a4c82e1 100644
--- a/modules/gitrepo/branch.go
+++ b/modules/gitrepo/branch.go
@@ -30,3 +30,20 @@ func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (str
 
 	return gitRepo.GetBranchCommitID(branch)
 }
+
+// SetDefaultBranch sets default branch of repository.
+func SetDefaultBranch(ctx context.Context, repo Repository, name string) error {
+	_, _, err := git.NewCommand(ctx, "symbolic-ref", "HEAD").
+		AddDynamicArguments(git.BranchPrefix + name).
+		RunStdString(&git.RunOpts{Dir: repoPath(repo)})
+	return err
+}
+
+// GetDefaultBranch gets default branch of repository.
+func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) {
+	return git.GetDefaultBranch(ctx, repoPath(repo))
+}
+
+func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) {
+	return git.GetDefaultBranch(ctx, wikiPath(repo))
+}
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 5f1af92041..80504b9c33 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -720,7 +720,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
 
 	if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
 		var err error
-		ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
+		ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
 			return err
@@ -731,7 +731,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
 	// Default branch only updated if changed and exist or the repository is empty
 	if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
 		if !repo.IsEmpty {
-			if err := ctx.Repo.GitRepo.SetDefaultBranch(*opts.DefaultBranch); err != nil {
+			if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil {
 				if !git.IsErrUnsupportedVersion(err) {
 					ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err)
 					return err
diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go
index 2e323129ef..33890be6a9 100644
--- a/routers/private/default_branch.go
+++ b/routers/private/default_branch.go
@@ -9,6 +9,7 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/private"
 	gitea_context "code.gitea.io/gitea/services/context"
 )
@@ -20,7 +21,7 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
 	branch := ctx.Params(":branch")
 
 	ctx.Repo.Repository.DefaultBranch = branch
-	if err := ctx.Repo.GitRepo.SetDefaultBranch(ctx.Repo.Repository.DefaultBranch); err != nil {
+	if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
 		if !git.IsErrUnsupportedVersion(err) {
 			ctx.JSON(http.StatusInternalServerError, private.Response{
 				Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 88b63da88d..df15f61b17 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -102,7 +102,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
 	commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
 	if git.IsErrNotExist(errCommit) {
 		// if the default branch recorded in database is out of sync, then re-sync it
-		gitRepoDefaultBranch, errBranch := wikiGitRepo.GetDefaultBranch()
+		gitRepoDefaultBranch, errBranch := gitrepo.GetWikiDefaultBranch(ctx, ctx.Repo.Repository)
 		if errBranch != nil {
 			return wikiGitRepo, nil, errBranch
 		}
diff --git a/services/context/repo.go b/services/context/repo.go
index 0b15c95e59..56e9fada0e 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -681,7 +681,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 		if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) {
 			ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
 		} else {
-			ctx.Repo.BranchName, _ = gitRepo.GetDefaultBranch()
+			ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository)
 			if ctx.Repo.BranchName == "" {
 				// If it still can't get a default branch, fall back to default branch from setting.
 				// Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug.
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index 3418cf90df..de4a58f27b 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -593,7 +593,7 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, gi
 			m.Repo.DefaultBranch = firstName
 		}
 		// Update the git repository default branch
-		if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil {
+		if err := gitrepo.SetDefaultBranch(ctx, m.Repo, m.Repo.DefaultBranch); err != nil {
 			if !git.IsErrUnsupportedVersion(err) {
 				log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err)
 				desc := fmt.Sprintf("Failed to update default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err)
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 7ca68776b5..0ac3c774b7 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -127,24 +127,17 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
 
 	repo.IsEmpty = false
 
-	// Don't bother looking this repo in the context it won't be there
-	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
-	if err != nil {
-		return fmt.Errorf("openRepository: %w", err)
-	}
-	defer gitRepo.Close()
-
 	if len(defaultBranch) > 0 {
 		repo.DefaultBranch = defaultBranch
 
-		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 			return fmt.Errorf("setDefaultBranch: %w", err)
 		}
 	} else {
-		repo.DefaultBranch, err = gitRepo.GetDefaultBranch()
+		repo.DefaultBranch, err = gitrepo.GetDefaultBranch(ctx, repo)
 		if err != nil {
 			repo.DefaultBranch = setting.Repository.DefaultBranch
-			if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+			if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 				return fmt.Errorf("setDefaultBranch: %w", err)
 			}
 		}
@@ -188,7 +181,7 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
 			repo.DefaultBranch = setting.Repository.DefaultBranch
 		}
 
-		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 			return fmt.Errorf("setDefaultBranch: %w", err)
 		}
 	}
@@ -197,6 +190,13 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
 		return fmt.Errorf("updateRepository: %w", err)
 	}
 
+	// Don't bother looking this repo in the context it won't be there
+	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
+	if err != nil {
+		return fmt.Errorf("openRepository: %w", err)
+	}
+	defer gitRepo.Close()
+
 	if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
 		return fmt.Errorf("SyncReleasesWithTags: %w", err)
 	}
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 402814fb9a..763fb966c5 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -375,7 +375,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
 				log.Error("CancelRunningJobs: %v", err)
 			}
 
-			err2 = gitRepo.SetDefaultBranch(to)
+			err2 = gitrepo.SetDefaultBranch(ctx, repo, to)
 			if err2 != nil {
 				return err2
 			}
@@ -540,7 +540,7 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
 			log.Error("CancelRunningJobs: %v", err)
 		}
 
-		if err := gitRepo.SetDefaultBranch(newBranchName); err != nil {
+		if err := gitrepo.SetDefaultBranch(ctx, repo, newBranchName); err != nil {
 			if !git.IsErrUnsupportedVersion(err) {
 				return err
 			}
diff --git a/services/repository/create.go b/services/repository/create.go
index 8d8c39197d..971793bcc6 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -177,12 +177,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re
 
 	if len(opts.DefaultBranch) > 0 {
 		repo.DefaultBranch = opts.DefaultBranch
-		gitRepo, err := gitrepo.OpenRepository(ctx, repo)
-		if err != nil {
-			return fmt.Errorf("openRepository: %w", err)
-		}
-		defer gitRepo.Close()
-		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 			return fmt.Errorf("setDefaultBranch: %w", err)
 		}
 
diff --git a/services/repository/generate.go b/services/repository/generate.go
index c444b60b2c..9b09e271ab 100644
--- a/services/repository/generate.go
+++ b/services/repository/generate.go
@@ -272,12 +272,7 @@ func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *r
 		repo.DefaultBranch = templateRepo.DefaultBranch
 	}
 
-	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
-	if err != nil {
-		return fmt.Errorf("openRepository: %w", err)
-	}
-	defer gitRepo.Close()
-	if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+	if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 		return fmt.Errorf("setDefaultBranch: %w", err)
 	}
 	if err = UpdateRepository(ctx, repo, false); err != nil {
diff --git a/services/repository/migrate.go b/services/repository/migrate.go
index aae2ddc120..df5cc67ae1 100644
--- a/services/repository/migrate.go
+++ b/services/repository/migrate.go
@@ -57,14 +57,7 @@ func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOp
 		return "", err
 	}
 
-	wikiRepo, err := git.OpenRepository(ctx, wikiPath)
-	if err != nil {
-		cleanIncompleteWikiPath()
-		return "", fmt.Errorf("failed to open wiki repo %q, err: %w", wikiPath, err)
-	}
-	defer wikiRepo.Close()
-
-	defaultBranch, err := wikiRepo.GetDefaultBranch()
+	defaultBranch, err := git.GetDefaultBranch(ctx, wikiPath)
 	if err != nil {
 		cleanIncompleteWikiPath()
 		return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err)
diff --git a/services/repository/push.go b/services/repository/push.go
index 89a3127902..0aeb4c830b 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -182,7 +182,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 						repo.DefaultBranch = refName
 						repo.IsEmpty = false
 						if repo.DefaultBranch != setting.Repository.DefaultBranch {
-							if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+							if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 								if !git.IsErrUnsupportedVersion(err) {
 									return err
 								}
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 6f1ca120b0..1b921a44bd 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -370,6 +370,14 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
 			return fmt.Errorf("unable to update database: %w", err)
 		}
 
+		oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo)
+		if err != nil {
+			return fmt.Errorf("unable to get default branch: %w", err)
+		}
+		if oldDefBranch == newBranch {
+			return nil
+		}
+
 		gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo)
 		if errors.Is(err, util.ErrNotExist) {
 			return nil // no git repo on storage, no need to do anything else
@@ -378,14 +386,6 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
 		}
 		defer gitRepo.Close()
 
-		oldDefBranch, err := gitRepo.GetDefaultBranch()
-		if err != nil {
-			return fmt.Errorf("unable to get default branch: %w", err)
-		}
-		if oldDefBranch == newBranch {
-			return nil
-		}
-
 		err = gitRepo.RenameBranch(oldDefBranch, newBranch)
 		if err != nil {
 			return fmt.Errorf("unable to rename default branch: %w", err)

From f86e9a03673b70d660a4b7a1e53748757d7a45fa Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 8 Mar 2024 08:57:52 +0100
Subject: [PATCH 312/679] Set user's 24h preference from their current OS
 locale (#29651)

Fixes: https://github.com/go-gitea/gitea/issues/28371

Fixed by using a JS solution that formats according to `lang`, but alters the 24h format setting as per user's locale. This will work for all tooltips:

<img width="243" alt="Screenshot 2024-03-07 at 23 03 35" src="https://github.com/go-gitea/gitea/assets/115237/6d16c71c-6786-4eda-8cdc-50ec68ba62c6">
<img width="250" alt="Screenshot 2024-03-07 at 23 03 17" src="https://github.com/go-gitea/gitea/assets/115237/4e26bbb7-12df-4b81-bd37-14705e87e8f7">
<img width="310" alt="Screenshot 2024-03-07 at 23 14 34" src="https://github.com/go-gitea/gitea/assets/115237/1ef599f0-6401-4e19-b1da-59cdfc09b0f6">

I think there is only one other place in the UI where we render such absolute dates, which is in the actions view and which I've also fixed:

<img width="275" alt="Screenshot 2024-03-07 at 23 04 00" src="https://github.com/go-gitea/gitea/assets/115237/df0fbe1f-96ee-4338-ab5e-2b10e215005d">
---
 web_src/js/components/RepoActionView.vue |  4 ++--
 web_src/js/modules/tippy.js              | 10 +++++++++-
 web_src/js/utils/time.js                 | 21 +++++++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 97cd05b45b..de9625b143 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -3,7 +3,7 @@ import {SvgIcon} from '../svg.js';
 import ActionRunStatus from './ActionRunStatus.vue';
 import {createApp} from 'vue';
 import {toggleElem} from '../utils/dom.js';
-import {getCurrentLocale} from '../utils.js';
+import {formatDatetime} from '../utils/time.js';
 import {renderAnsi} from '../render/ansi.js';
 import {POST, DELETE} from '../modules/fetch.js';
 
@@ -167,7 +167,7 @@ const sfc = {
       const logTimeStamp = document.createElement('span');
       logTimeStamp.className = 'log-time-stamp';
       const date = new Date(parseFloat(line.timestamp * 1000));
-      const timeStamp = date.toLocaleString(getCurrentLocale(), {timeZoneName: 'short'});
+      const timeStamp = formatDatetime(date);
       logTimeStamp.textContent = timeStamp;
       toggleElem(logTimeStamp, this.timeVisible['log-time-stamp']);
       // for "Show seconds"
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index 27f371fd88..489afc0ae1 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -1,5 +1,6 @@
 import tippy, {followCursor} from 'tippy.js';
 import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+import {formatDatetime} from '../utils/time.js';
 
 const visibleInstances = new Set();
 
@@ -93,8 +94,15 @@ function attachTooltip(target, content = null) {
 }
 
 function switchTitleToTooltip(target) {
-  const title = target.getAttribute('title');
+  let title = target.getAttribute('title');
   if (title) {
+    // apply custom formatting to relative-time's tooltips
+    if (target.tagName.toLowerCase() === 'relative-time') {
+      const datetime = target.getAttribute('datetime');
+      if (datetime) {
+        title = formatDatetime(new Date(datetime));
+      }
+    }
     target.setAttribute('data-tooltip-content', title);
     target.setAttribute('aria-label', title);
     // keep the attribute, in case there are some other "[title]" selectors
diff --git a/web_src/js/utils/time.js b/web_src/js/utils/time.js
index 3284e893e1..1848792c98 100644
--- a/web_src/js/utils/time.js
+++ b/web_src/js/utils/time.js
@@ -1,4 +1,5 @@
 import dayjs from 'dayjs';
+import {getCurrentLocale} from '../utils.js';
 
 // Returns an array of millisecond-timestamps of start-of-week days (Sundays)
 export function startDaysBetween(startDate, endDate) {
@@ -44,3 +45,23 @@ export function fillEmptyStartDaysWithZeroes(startDays, data) {
 
   return Object.values(result);
 }
+
+let dateFormat;
+
+// format a Date object to document's locale, but with 24h format from user's current locale because this
+// option is a personal preference of the user, not something that the document's locale should dictate.
+export function formatDatetime(date) {
+  if (!dateFormat) {
+    // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
+    dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
+      day: 'numeric',
+      month: 'short',
+      year: 'numeric',
+      hour: 'numeric',
+      hour12: !Number.isInteger(Number(new Intl.DateTimeFormat([], {hour: 'numeric'}).format())),
+      minute: '2-digit',
+      timeZoneName: 'short',
+    });
+  }
+  return dateFormat.format(date);
+}

From 9dc8a6336edddca0f3f90392bbc398f4ceaeaf13 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Fri, 8 Mar 2024 17:44:50 +0900
Subject: [PATCH 313/679] Fix incorrect rendering csv file when file size is
 larger than UI.CSV.MaxFileSize (#29653)

Fix #29506
---
 modules/markup/csv/csv.go | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 7af34a6cbc..12458e954a 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -93,8 +93,10 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
 		if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil {
 			return err
 		}
-		_, err = tmpBlock.WriteString("</pre>")
-		return err
+		if _, err := tmpBlock.WriteString("</pre>"); err != nil {
+			return err
+		}
+		return tmpBlock.Flush()
 	}
 
 	rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, bytes.NewReader(rawBytes))

From b5c418f271963f8de0b8324305ea74cde7d3f3ab Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 8 Mar 2024 11:30:41 +0200
Subject: [PATCH 314/679] Don't use `<br />` in alert block (#29650)

- Follows https://github.com/go-gitea/gitea/pull/29121

When I implemented alert blocks I was always testing the markdown in
issue comments. I used `<br />` for line breaks and it looked good. I
have since learned that the markdown on README files doesn't allow these
tags. So a comment with

```md
> [!NOTE]
> If you're interested in using our APIs, we have experimental support with [documentation](https://try.gitea.io/api/swagger).
```

looked like this in a comment

![image](https://github.com/go-gitea/gitea/assets/20454870/96b1de01-2c87-4d4f-83dd-98192b83e9d0)
but looked like this in a README

![image](https://github.com/go-gitea/gitea/assets/20454870/474b636d-dd7a-4b7f-ba27-643803c71aa3)

So I changed how we render the alert block by having the alert itself
have a dedicated paragraph, so line breaks happen naturally between
paragraphs.

# Before

![image](https://github.com/go-gitea/gitea/assets/20454870/474b636d-dd7a-4b7f-ba27-643803c71aa3)

![image](https://github.com/go-gitea/gitea/assets/20454870/167a8d37-9a44-4479-9340-5dc80347b595)

# After

![image](https://github.com/go-gitea/gitea/assets/20454870/2f99fec0-98ff-4ba8-97fe-b4567041ae79)

![image](https://github.com/go-gitea/gitea/assets/20454870/ffdeae11-fb06-4d00-b497-eae135f0d7ad)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 modules/markup/markdown/goldmark.go | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index c4b23e66fc..67817ce27b 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -219,21 +219,18 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 			v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType))
 
 			// create an emphasis to make it bold
+			attentionParagraph := ast.NewParagraph()
 			emphasis := ast.NewEmphasis(2)
 			emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
-			firstParagraph.InsertBefore(firstParagraph, firstTextNode, emphasis)
 
 			// capitalize first letter
 			attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:]))
 
-			// replace the ![TYPE] with icon+Type
+			// replace the ![TYPE] with a dedicated paragraph of icon+Type
 			emphasis.AppendChild(emphasis, attentionText)
-			for i := 0; i < 2; i++ {
-				lineBreak := ast.NewText()
-				lineBreak.SetSoftLineBreak(true)
-				firstParagraph.InsertAfter(firstParagraph, emphasis, lineBreak)
-			}
-			firstParagraph.InsertBefore(firstParagraph, emphasis, NewAttention(attentionType))
+			attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
+			attentionParagraph.AppendChild(attentionParagraph, emphasis)
+			firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
 			firstParagraph.RemoveChild(firstParagraph, firstTextNode)
 			firstParagraph.RemoveChild(firstParagraph, secondTextNode)
 			firstParagraph.RemoveChild(firstParagraph, thirdTextNode)

From 114bb505a3b0819db683d4b586e950df6a17bff8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 8 Mar 2024 10:42:12 +0100
Subject: [PATCH 315/679] Style fomantic grey labels (#29458)

Fomantic grey labels in the dashboard repo lists were showing original
fomantic colors, fixed that. Also slightly tweaked the light theme
colors so it uses same opacity values as dark theme.

<img width="165" alt="Screenshot 2024-03-07 at 21 06 23"
src="https://github.com/go-gitea/gitea/assets/115237/72744d6f-2ee1-4e5d-8ba0-b482a446f535">
<img width="167" alt="Screenshot 2024-03-07 at 21 06 00"
src="https://github.com/go-gitea/gitea/assets/115237/1ba93775-e5a9-4b28-b90f-59c1e9199687">
---
 web_src/css/base.css                     | 4 +++-
 web_src/css/themes/theme-gitea-light.css | 6 +++---
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 77359b36e5..3db9cd894c 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1448,7 +1448,9 @@ strong.attention-caution, span.attention-caution {
 }
 
 .ui.label,
-.ui.menu .item > .label {
+.ui.menu .item > .label,
+.ui.grey.labels .label,
+.ui.ui.ui.grey.label {
   background: var(--color-label-bg);
   color: var(--color-label-text);
 }
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 5137e0774c..fbe2458ed6 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -228,9 +228,9 @@
   --color-nav-hover-bg: #ebebeb;
   --color-nav-text: var(--color-text);
   --color-label-text: var(--color-text);
-  --color-label-bg: #cacaca5b;
-  --color-label-hover-bg: #cacacaa0;
-  --color-label-active-bg: #cacacaff;
+  --color-label-bg: #9d9d9d4b;
+  --color-label-hover-bg: #9d9d9da0;
+  --color-label-active-bg: #9d9d9dff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-6);
   --color-active-line: #fffbdd;

From 886e90aa82521d2c2ae17d3e177c056ae32e4aa6 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 8 Mar 2024 10:47:32 +0100
Subject: [PATCH 316/679] Don't show AbortErrors on logout (#29639)

When logging out of Gitea, a error toast can be seen for a split second.
I don't know why or how it happens but I found it it's an `AbortError`
(related to
[AbortController#abort](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort)),
so let's hide it.
---
 web_src/js/features/common-global.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 211253ef9a..ee4ade1f04 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -105,8 +105,10 @@ async function fetchActionDoRequest(actionElem, url, opt) {
       showErrorToast(`server error: ${resp.status}`);
     }
   } catch (e) {
-    console.error('error when doRequest', e);
-    showErrorToast(`${i18n.network_error} ${e}`);
+    if (e.name !== 'AbortError') {
+      console.error('error when doRequest', e);
+      showErrorToast(`${i18n.network_error} ${e}`);
+    }
   }
   actionElem.classList.remove('is-loading', 'small-loading-icon');
 }

From f219ea8d0e0eccd74c786202a4ddb2784a0175ad Mon Sep 17 00:00:00 2001
From: DC <106393991+DanielMatiasCarvalho@users.noreply.github.com>
Date: Fri, 8 Mar 2024 09:53:01 +0000
Subject: [PATCH 317/679] Fix user-defined markup links targets (#29305)

This seeks to fix the bug reported on issue #29196.

Cause:
ID's with custom characters (- , _ , etc.), were not linking correctly
in the Markdown file when rendered in the browser because the ID in the
respective destinies would be different than the one in anchor, while
for IDs with only letters, the ID would be the same.

Fix:
It was suggested that to fix this bug, it should more or less like
GitHub does it. While in gitea the anchors would be put in HTML like
this:
```
<p dir="auto"><a href="#user-content-_toc152597800" rel="nofollow">Review</a></p>
<p dir="auto"><a href="#user-content-_toc152597802" rel="nofollow">Staging</a></p>
<p dir="auto"><a href="#user-content-_toc152597803" rel="nofollow">Development</a></p>
<p dir="auto"><a href="#user-content-_toc152597828" rel="nofollow">Testing</a></p>
<p dir="auto"><a href="#user-content-_toc152597829" rel="nofollow">Unit-tests</a></p>

```
In GitHub, the same anchor's href properties would be the same without
"user-content-" trailing behind.

So my code made sure to change those anchors, so it would not include
"user-content-" and then add respective Event Listeners so it would
scroll into the supposed places.

Fixes: #29196

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/markup/anchors.js | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
index 53dfa2980c..03934ea215 100644
--- a/web_src/js/markup/anchors.js
+++ b/web_src/js/markup/anchors.js
@@ -2,6 +2,7 @@ import {svg} from '../svg.js';
 
 const headingSelector = '.markup h1, .markup h2, .markup h3, .markup h4, .markup h5, .markup h6';
 
+// scroll to anchor while respecting the `user-content` prefix that exists on the target
 function scrollToAnchor(hash, initial) {
   // abort if the browser has already scrolled to another anchor during page load
   if (initial && document.querySelector(':target')) return;
@@ -19,6 +20,7 @@ function scrollToAnchor(hash, initial) {
 export function initMarkupAnchors() {
   if (!document.querySelector('.markup')) return;
 
+  // create link icons for markup headings, the resulting link href will remove `user-content-`
   for (const heading of document.querySelectorAll(headingSelector)) {
     const originalId = heading.id.replace(/^user-content-/, '');
     const a = document.createElement('a');
@@ -31,5 +33,18 @@ export function initMarkupAnchors() {
     heading.prepend(a);
   }
 
+  // handle user-defined `name` anchors like `[Link](#link)` linking to `<a name="link"></a>Link`
+  for (const a of document.querySelectorAll('.markup a[href^="#"]')) {
+    const href = a.getAttribute('href');
+    if (!href.startsWith('#user-content-')) continue;
+    const originalId = href.replace(/^#user-content-/, '');
+    a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
+    if (document.getElementsByName(originalId).length !== 1) {
+      a.addEventListener('click', (e) => {
+        scrollToAnchor(e.currentTarget.getAttribute('href'), false);
+      });
+    }
+  }
+
   scrollToAnchor(window.location.hash, true);
 }

From 930bae2300be1c62a56d6f019e519b6752d41ad1 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 8 Mar 2024 18:21:24 +0800
Subject: [PATCH 318/679] Add cache for branch divergence on branch list page
 (#29577)

The branch page for blender project will take 6s because calculating
divergence is very slow.
This PR will add a cache for the branch divergence calculation. So when
the second visit the branch list, it will take only less 200ms.
---
 services/repository/branch.go | 55 +++++++++++++++++++++++++++++++----
 services/repository/push.go   |  5 ++++
 2 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/services/repository/branch.go b/services/repository/branch.go
index 763fb966c5..8d8cfa2d19 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -16,9 +16,11 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/graceful"
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/queue"
@@ -99,7 +101,6 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 		if err != nil {
 			return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
 		}
-
 		branches = append(branches, branch)
 	}
 
@@ -109,10 +110,44 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 	if err != nil {
 		return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
 	}
-
 	return defaultBranch, branches, totalNumOfBranches, nil
 }
 
+func getDivergenceCacheKey(repoID int64, branchName string) string {
+	return fmt.Sprintf("%d-%s", repoID, branchName)
+}
+
+// getDivergenceFromCache gets the divergence from cache
+func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) {
+	data := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName))
+	res := git.DivergeObject{
+		Ahead:  -1,
+		Behind: -1,
+	}
+	s, ok := data.([]byte)
+	if !ok || len(s) == 0 {
+		return &res, false
+	}
+
+	if err := json.Unmarshal(s, &res); err != nil {
+		log.Error("json.UnMarshal failed: %v", err)
+		return &res, false
+	}
+	return &res, true
+}
+
+func putDivergenceFromCache(repoID int64, branchName string, divergence *git.DivergeObject) error {
+	bs, err := json.Marshal(divergence)
+	if err != nil {
+		return err
+	}
+	return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), bs, 30*24*60*60)
+}
+
+func DelDivergenceFromCache(repoID int64, branchName string) error {
+	return cache.GetCache().Delete(getDivergenceCacheKey(repoID, branchName))
+}
+
 func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules,
 	repoIDToRepo map[int64]*repo_model.Repository,
 	repoIDToGitRepo map[int64]*git.Repository,
@@ -130,10 +165,18 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
 
 	// it's not default branch
 	if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted {
-		var err error
-		divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName)
-		if err != nil {
-			log.Error("CountDivergingCommits: %v", err)
+		var cached bool
+		divergence, cached = getDivergenceFromCache(repo.ID, dbBranch.Name)
+		if !cached {
+			var err error
+			divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName)
+			if err != nil {
+				log.Error("CountDivergingCommits: %v", err)
+			} else {
+				if err = putDivergenceFromCache(repo.ID, dbBranch.Name, divergence); err != nil {
+					log.Error("putDivergenceFromCache: %v", err)
+				}
+			}
 		}
 	}
 
diff --git a/services/repository/push.go b/services/repository/push.go
index 0aeb4c830b..39843249a5 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -220,6 +220,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 					}
 				}
 
+				// delete cache for divergence
+				if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
+					log.Error("DelDivergenceFromCache: %v", err)
+				}
+
 				commits := repo_module.GitToPushCommits(l)
 				commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
 

From b253463e959c44cbd212fe1d662f2520ebfe38e6 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Sat, 9 Mar 2024 00:10:01 +0900
Subject: [PATCH 319/679] bump python version to 3.12 in dev container (#29670)

![image](https://github.com/go-gitea/gitea/assets/18380374/963dc021-ac9b-4713-8344-654f966c80a4)

The default version is 3.9.2, which is not supported by poetry.

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 .devcontainer/devcontainer.json | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 1051b0f2a2..a4a6ce8fcf 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -8,7 +8,9 @@
     },
     "ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
     "ghcr.io/devcontainers-contrib/features/poetry:2": {},
-    "ghcr.io/devcontainers/features/python:1": {}
+    "ghcr.io/devcontainers/features/python:1": {
+      "version": "3.12"
+    }
   },
   "customizations": {
     "vscode": {

From a3cfe6f39ba33cea305de592a006727857014c53 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 8 Mar 2024 16:15:58 +0100
Subject: [PATCH 320/679] Support pasting URLs over markdown text (#29566)

Support pasting URLs over selection text in the textarea editor. Does
not work in EasyMDE and I don't intend to support it. Image paste works
as usual in both Textarea and EasyMDE.

The new `replaceTextareaSelection` function changes textarea content via
[`insertText`](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#using_inserttext)
command, which preserves history, e.g. `CTRL-Z` works and is also
demostrated below. We should later refactor the image paste code to use
the same function because it currently destroys history.

Overriding the formatting via `Shift` key is supported as well, e.g.
`Ctrl+Shift+V` will insert the URL as-is, like on GitHub.


![urlpaste](https://github.com/go-gitea/gitea/assets/115237/522b1023-6797-401c-9e4a-498570adfc88)
---
 .../js/features/comp/ComboMarkdownEditor.js   | 17 +++++-
 .../features/comp/{ImagePaste.js => Paste.js} | 60 ++++++++++---------
 web_src/js/utils/dom.js                       | 36 +++++++++++
 web_src/js/utils/url.js                       | 12 ++++
 web_src/js/utils/url.test.js                  |  9 ++-
 5 files changed, 103 insertions(+), 31 deletions(-)
 rename web_src/js/features/comp/{ImagePaste.js => Paste.js} (72%)

diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index 4c973358e3..1e7b554b98 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -3,7 +3,7 @@ import '@github/text-expander-element';
 import $ from 'jquery';
 import {attachTribute} from '../tribute.js';
 import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js';
-import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
+import {initEasyMDEPaste, initTextareaPaste} from './Paste.js';
 import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
 import {renderPreviewPanelContent} from '../repo-editor.js';
 import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
@@ -84,6 +84,17 @@ class ComboMarkdownEditor {
       if (el.nodeName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button');
     }
 
+    this.textarea.addEventListener('keydown', (e) => {
+      if (e.shiftKey) {
+        e.target._shiftDown = true;
+      }
+    });
+    this.textarea.addEventListener('keyup', (e) => {
+      if (!e.shiftKey) {
+        e.target._shiftDown = false;
+      }
+    });
+
     const monospaceButton = this.container.querySelector('.markdown-switch-monospace');
     const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true';
     const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text');
@@ -108,7 +119,7 @@ class ComboMarkdownEditor {
     });
 
     if (this.dropzone) {
-      initTextareaImagePaste(this.textarea, this.dropzone);
+      initTextareaPaste(this.textarea, this.dropzone);
     }
   }
 
@@ -241,7 +252,7 @@ class ComboMarkdownEditor {
     });
     this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights);
     await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
-    initEasyMDEImagePaste(this.easyMDE, this.dropzone);
+    initEasyMDEPaste(this.easyMDE, this.dropzone);
     hideElem(this.textareaMarkdownToolbar);
   }
 
diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/Paste.js
similarity index 72%
rename from web_src/js/features/comp/ImagePaste.js
rename to web_src/js/features/comp/Paste.js
index b727880bc8..b26296d1fc 100644
--- a/web_src/js/features/comp/ImagePaste.js
+++ b/web_src/js/features/comp/Paste.js
@@ -1,6 +1,8 @@
 import {htmlEscape} from 'escape-goat';
 import {POST} from '../../modules/fetch.js';
 import {imageInfo} from '../../utils/image.js';
+import {getPastedContent, replaceTextareaSelection} from '../../utils/dom.js';
+import {isUrl} from '../../utils/url.js';
 
 async function uploadFile(file, uploadUrl) {
   const formData = new FormData();
@@ -10,17 +12,6 @@ async function uploadFile(file, uploadUrl) {
   return await res.json();
 }
 
-function clipboardPastedImages(e) {
-  if (!e.clipboardData) return [];
-
-  const files = [];
-  for (const item of e.clipboardData.items || []) {
-    if (!item.type || !item.type.startsWith('image/')) continue;
-    files.push(item.getAsFile());
-  }
-  return files;
-}
-
 function triggerEditorContentChanged(target) {
   target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
 }
@@ -91,20 +82,16 @@ class CodeMirrorEditor {
   }
 }
 
-const uploadClipboardImage = async (editor, dropzone, e) => {
+async function handleClipboardImages(editor, dropzone, images, e) {
   const uploadUrl = dropzone.getAttribute('data-upload-url');
   const filesContainer = dropzone.querySelector('.files');
 
-  if (!uploadUrl || !filesContainer) return;
+  if (!dropzone || !uploadUrl || !filesContainer || !images.length) return;
 
-  const pastedImages = clipboardPastedImages(e);
-  if (!pastedImages || pastedImages.length === 0) {
-    return;
-  }
   e.preventDefault();
   e.stopPropagation();
 
-  for (const img of pastedImages) {
+  for (const img of images) {
     const name = img.name.slice(0, img.name.lastIndexOf('.'));
 
     const placeholder = `![${name}](uploading ...)`;
@@ -131,18 +118,37 @@ const uploadClipboardImage = async (editor, dropzone, e) => {
     input.value = uuid;
     filesContainer.append(input);
   }
-};
+}
 
-export function initEasyMDEImagePaste(easyMDE, dropzone) {
-  if (!dropzone) return;
-  easyMDE.codemirror.on('paste', async (_, e) => {
-    return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), dropzone, e);
+function handleClipboardText(textarea, text, e) {
+  // when pasting links over selected text, turn it into [text](link), except when shift key is held
+  const {value, selectionStart, selectionEnd, _shiftDown} = textarea;
+  if (_shiftDown) return;
+  const selectedText = value.substring(selectionStart, selectionEnd);
+  const trimmedText = text.trim();
+  if (selectedText && isUrl(trimmedText)) {
+    e.stopPropagation();
+    e.preventDefault();
+    replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`);
+  }
+}
+
+export function initEasyMDEPaste(easyMDE, dropzone) {
+  easyMDE.codemirror.on('paste', (_, e) => {
+    const {images} = getPastedContent(e);
+    if (images.length) {
+      handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e);
+    }
   });
 }
 
-export function initTextareaImagePaste(textarea, dropzone) {
-  if (!dropzone) return;
-  textarea.addEventListener('paste', async (e) => {
-    return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e);
+export function initTextareaPaste(textarea, dropzone) {
+  textarea.addEventListener('paste', (e) => {
+    const {images, text} = getPastedContent(e);
+    if (images.length) {
+      handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
+    } else if (text) {
+      handleClipboardText(textarea, text, e);
+    }
   });
 }
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index 91535dc187..aa7c2604aa 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -243,3 +243,39 @@ export function isElemVisible(element) {
 
   return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
 }
+
+// extract text and images from "paste" event
+export function getPastedContent(e) {
+  const images = [];
+  for (const item of e.clipboardData?.items ?? []) {
+    if (item.type?.startsWith('image/')) {
+      images.push(item.getAsFile());
+    }
+  }
+  const text = e.clipboardData?.getData?.('text') ?? '';
+  return {text, images};
+}
+
+// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
+export function replaceTextareaSelection(textarea, text) {
+  const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
+  const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
+  let success = true;
+
+  textarea.contentEditable = 'true';
+  try {
+    success = document.execCommand('insertText', false, text);
+  } catch {
+    success = false;
+  }
+  textarea.contentEditable = 'false';
+
+  if (success && !textarea.value.slice(0, textarea.selectionStart ?? undefined).endsWith(text)) {
+    success = false;
+  }
+
+  if (!success) {
+    textarea.value = `${before}${text}${after}`;
+    textarea.dispatchEvent(new CustomEvent('change', {bubbles: true, cancelable: true}));
+  }
+}
diff --git a/web_src/js/utils/url.js b/web_src/js/utils/url.js
index a40737ca6f..470ece31b0 100644
--- a/web_src/js/utils/url.js
+++ b/web_src/js/utils/url.js
@@ -1,3 +1,15 @@
 export function pathEscapeSegments(s) {
   return s.split('/').map(encodeURIComponent).join('/');
 }
+
+function stripSlash(url) {
+  return url.endsWith('/') ? url.slice(0, -1) : url;
+}
+
+export function isUrl(url) {
+  try {
+    return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim();
+  } catch {
+    return false;
+  }
+}
diff --git a/web_src/js/utils/url.test.js b/web_src/js/utils/url.test.js
index 3dbedec94f..08c6373ffb 100644
--- a/web_src/js/utils/url.test.js
+++ b/web_src/js/utils/url.test.js
@@ -1,6 +1,13 @@
-import {pathEscapeSegments} from './url.js';
+import {pathEscapeSegments, isUrl} from './url.js';
 
 test('pathEscapeSegments', () => {
   expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c');
   expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c');
 });
+
+test('isUrl', () => {
+  expect(isUrl('https://example.com')).toEqual(true);
+  expect(isUrl('https://example.com/')).toEqual(true);
+  expect(isUrl('https://example.com/index.html')).toEqual(true);
+  expect(isUrl('/index.html')).toEqual(false);
+});

From 0c273f12e0b5d6dc2b80e2e6b51b2deaf15608b1 Mon Sep 17 00:00:00 2001
From: charles <30816317+charles7668@users.noreply.github.com>
Date: Fri, 8 Mar 2024 23:43:48 +0800
Subject: [PATCH 321/679] Fix commit_status problem when testing (#29672)

Close #29661

fix #29656

Co-authored-by: Giteabot <teabot@gitea.io>
---
 services/pull/commit_status.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index 07e9eb7959..653bfe6bcb 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -59,7 +59,7 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
 		}
 	}
 
-	if matchedCount == 0 {
+	if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess {
 		status := git_model.CalcCommitStatus(commitStatuses)
 		if status != nil {
 			return status.State

From 82e102f8b09faf1ac2786ccad36d4a20fcb392b8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 8 Mar 2024 22:02:05 +0100
Subject: [PATCH 322/679] Replace more gt- with tw- (#29678)

This will conclude the trivial class replacements.
---
 services/auth/source/oauth2/providers.go      |  2 +-
 templates/repo/commit_statuses.tmpl           |  2 +-
 .../repo/issue/view_content/comments.tmpl     |  8 +++----
 templates/repo/pulse.tmpl                     | 12 +++++-----
 templates/shared/issuelist.tmpl               |  6 ++---
 templates/user/dashboard/issues.tmpl          |  2 +-
 templates/user/dashboard/milestones.tmpl      |  2 +-
 web_src/css/helpers.css                       | 23 -------------------
 8 files changed, 17 insertions(+), 40 deletions(-)

diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index ac32647839..c3edae4ab6 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -59,7 +59,7 @@ func (p *AuthSourceProvider) DisplayName() string {
 
 func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
 	if p.iconURL != "" {
-		img := fmt.Sprintf(`<img class="gt-object-contain gt-mr-3" width="%d" height="%d" src="%s" alt="%s">`,
+		img := fmt.Sprintf(`<img class="tw-object-contain gt-mr-3" width="%d" height="%d" src="%s" alt="%s">`,
 			size,
 			size,
 			html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()),
diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl
index 74c20a6a2c..b035e74c2f 100644
--- a/templates/repo/commit_statuses.tmpl
+++ b/templates/repo/commit_statuses.tmpl
@@ -1,6 +1,6 @@
 {{if .Statuses}}
 	{{if and (eq (len .Statuses) 1) .Status.TargetURL}}
-		<a class="gt-vm {{.AdditionalClasses}} gt-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
+		<a class="gt-vm {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
 			{{template "repo/commit_status" .Status}}
 		</a>
 	{{else}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 86cb716bb3..8bbcb1a54a 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -80,7 +80,7 @@
 			</div>
 		{{else if eq .Type 1}}
 			<div class="timeline-item event" id="{{.HashTag}}">
-				<span class="badge gt-bg-green gt-text-white">{{svg "octicon-dot-fill"}}</span>
+				<span class="badge tw-bg-green tw-text-white">{{svg "octicon-dot-fill"}}</span>
 				{{if not .OriginalAuthor}}
 					{{template "shared/user/avatarlink" dict "user" .Poster}}
 				{{end}}
@@ -95,7 +95,7 @@
 			</div>
 		{{else if eq .Type 2}}
 			<div class="timeline-item event" id="{{.HashTag}}">
-				<span class="badge gt-bg-red gt-text-white">{{svg "octicon-circle-slash"}}</span>
+				<span class="badge tw-bg-red tw-text-white">{{svg "octicon-circle-slash"}}</span>
 				{{if not .OriginalAuthor}}
 					{{template "shared/user/avatarlink" dict "user" .Poster}}
 				{{end}}
@@ -110,7 +110,7 @@
 			</div>
 		{{else if eq .Type 28}}
 			<div class="timeline-item event" id="{{.HashTag}}">
-				<span class="badge gt-bg-purple gt-text-white">{{svg "octicon-git-merge"}}</span>
+				<span class="badge tw-bg-purple tw-text-white">{{svg "octicon-git-merge"}}</span>
 				{{if not .OriginalAuthor}}
 					{{template "shared/user/avatarlink" dict "user" .Poster}}
 				{{end}}
@@ -379,7 +379,7 @@
 						{{ctx.AvatarUtils.Avatar .Poster 40}}
 					</a>
 					{{end}}
-					<span class="badge{{if eq .Review.Type 1}} gt-bg-green gt-text-white{{else if eq .Review.Type 3}} gt-bg-red gt-text-white{{end}}">{{svg (printf "octicon-%s" .Review.Type.Icon)}}</span>
+					<span class="badge{{if eq .Review.Type 1}} tw-bg-green tw-text-white{{else if eq .Review.Type 3}} tw-bg-red tw-text-white{{end}}">{{svg (printf "octicon-%s" .Review.Type.Icon)}}</span>
 					<span class="text grey muted-links">
 						{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 						{{if eq .Review.Type 1}}
diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl
index e6a59ea8c6..5943ae0434 100644
--- a/templates/repo/pulse.tmpl
+++ b/templates/repo/pulse.tmpl
@@ -108,7 +108,7 @@
 {{end}}
 
 {{if gt .Activity.PublishedReleaseCount 0}}
-	<h4 class="divider divider-text gt-normal-case" id="published-releases">
+	<h4 class="divider divider-text tw-normal-case" id="published-releases">
 		{{svg "octicon-tag" 16 "gt-mr-3"}}
 		{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
 			(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
@@ -130,7 +130,7 @@
 {{end}}
 
 {{if gt .Activity.MergedPRCount 0}}
-	<h4 class="divider divider-text gt-normal-case" id="merged-pull-requests">
+	<h4 class="divider divider-text tw-normal-case" id="merged-pull-requests">
 		{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
 		{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
 			(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
@@ -149,7 +149,7 @@
 {{end}}
 
 {{if gt .Activity.OpenedPRCount 0}}
-	<h4 class="divider divider-text gt-normal-case" id="proposed-pull-requests">
+	<h4 class="divider divider-text tw-normal-case" id="proposed-pull-requests">
 		{{svg "octicon-git-branch" 16 "gt-mr-3"}}
 		{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
 			(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
@@ -168,7 +168,7 @@
 {{end}}
 
 {{if gt .Activity.ClosedIssueCount 0}}
-	<h4 class="divider divider-text gt-normal-case" id="closed-issues">
+	<h4 class="divider divider-text tw-normal-case" id="closed-issues">
 		{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
 		{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
 			(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
@@ -187,7 +187,7 @@
 {{end}}
 
 {{if gt .Activity.OpenedIssueCount 0}}
-	<h4 class="divider divider-text gt-normal-case" id="new-issues">
+	<h4 class="divider divider-text tw-normal-case" id="new-issues">
 		{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
 		{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
 			(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
@@ -206,7 +206,7 @@
 {{end}}
 
 {{if gt .Activity.UnresolvedIssueCount 0}}
-	<h4 class="divider divider-text gt-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
+	<h4 class="divider divider-text tw-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
 		{{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
 		{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
 	</h4>
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index e8a0079c1c..a90188297f 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -13,7 +13,7 @@
 			<div class="flex-item-main">
 				<div class="flex-item-header">
 					<div class="flex-item-title">
-						<a class="gt-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{RenderEmoji $.Context .Title | RenderCodeBlock}}</a>
+						<a class="tw-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{RenderEmoji $.Context .Title | RenderCodeBlock}}</a>
 						{{if .IsPull}}
 							{{if (index $.CommitStatuses .PullRequest.ID)}}
 								{{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}}
@@ -36,7 +36,7 @@
 						{{if .Assignees}}
 						<div class="text grey">
 							{{range .Assignees}}
-								<a class="ui assignee gt-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}">
+								<a class="ui assignee tw-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}">
 									{{ctx.AvatarUtils.Avatar . 20}}
 								</a>
 							{{end}}
@@ -44,7 +44,7 @@
 						{{end}}
 						{{if .NumComments}}
 						<div class="text grey">
-							<a class="gt-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
+							<a class="tw-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
 								{{svg "octicon-comment" 16}}{{.NumComments}}
 							</a>
 						</div>
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index fd5960c31e..7b7023cfaa 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -4,7 +4,7 @@
 	<div class="ui container">
 		<div class="flex-container">
 			<div class="flex-container-nav">
-				<div class="ui secondary vertical filter menu gt-bg-transparent">
+				<div class="ui secondary vertical filter menu tw-bg-transparent">
 					<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "home.issues.in_your_repos"}}
 						<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 737a0f7e2b..7cde02291b 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -4,7 +4,7 @@
 	<div class="ui container">
 		<div class="flex-container">
 			<div class="flex-container-nav">
-				<div class="ui secondary vertical filter menu gt-bg-transparent">
+				<div class="ui secondary vertical filter menu tw-bg-transparent">
 					<div class="item">
 						{{ctx.Locale.Tr "home.issues.in_your_repos"}}
 						<strong>{{.Total}}</strong>
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index dad0f9b127..860722823a 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -46,12 +46,6 @@ Gitea's private styles use `g-` prefix.
   text-overflow: ellipsis;
 }
 
-/* below class names match Tailwind CSS */
-.gt-object-contain { object-fit: contain !important; }
-.gt-no-underline { text-decoration-line: none !important; }
-.gt-normal-case { text-transform: none !important; }
-.gt-italic { font-style: italic !important; }
-
 .gt-font-light { font-weight: var(--font-weight-light) !important; }
 .gt-font-normal { font-weight: var(--font-weight-normal) !important; }
 .gt-font-medium { font-weight: var(--font-weight-medium) !important; }
@@ -70,23 +64,6 @@ Gitea's private styles use `g-` prefix.
 .gt-border-secondary-left { border-left: 1px solid var(--color-secondary) !important; }
 .gt-border-secondary-right { border-right: 1px solid var(--color-secondary) !important; }
 
-.gt-bg-red { background: var(--color-red) !important; }
-.gt-bg-orange { background: var(--color-orange) !important; }
-.gt-bg-yellow { background: var(--color-yellow) !important; }
-.gt-bg-olive { background: var(--color-olive) !important; }
-.gt-bg-green { background: var(--color-green) !important; }
-.gt-bg-teal { background: var(--color-teal) !important; }
-.gt-bg-blue { background: var(--color-blue) !important; }
-.gt-bg-violet { background: var(--color-violet) !important; }
-.gt-bg-purple { background: var(--color-purple) !important; }
-.gt-bg-pink { background: var(--color-pink) !important; }
-.gt-bg-brown { background: var(--color-brown) !important; }
-.gt-bg-grey { background: var(--color-grey) !important; }
-.gt-bg-gold { background: var(--color-gold) !important; }
-.gt-bg-transparent { background: transparent !important; }
-
-.gt-text-white { color: var(--color-white) !important; }
-
 .interact-fg { color: inherit !important; }
 .interact-fg:hover { color: var(--color-primary) !important; }
 .interact-fg:active { color: var(--color-primary-active) !important; }

From baeb2511741aa70d24a48fd46db936b52be9d9dd Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 9 Mar 2024 00:21:45 +0100
Subject: [PATCH 323/679] Use more specific selector for `name` links (#29679)

Followup https://github.com/go-gitea/gitea/pull/29305. As per discussion
in https://github.com/go-gitea/gitea/pull/29666#discussion_r1517506422,
make this selector only search in the current `.markup` document, as
there can be multiples displayed at the same time.

@DanielMatiasCarvalho maybe you can review.
---
 web_src/js/markup/anchors.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
index 03934ea215..6cf83eb428 100644
--- a/web_src/js/markup/anchors.js
+++ b/web_src/js/markup/anchors.js
@@ -39,7 +39,7 @@ export function initMarkupAnchors() {
     if (!href.startsWith('#user-content-')) continue;
     const originalId = href.replace(/^#user-content-/, '');
     a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
-    if (document.getElementsByName(originalId).length !== 1) {
+    if (a.closest('.markup').querySelectorAll(`a[name="${originalId}"]`).length !== 1) {
       a.addEventListener('click', (e) => {
         scrollToAnchor(e.currentTarget.getAttribute('href'), false);
       });

From 7fdc0481538151d8a5ed3ec2a32639950f5d8ac6 Mon Sep 17 00:00:00 2001
From: 6543 <m.huber@kithara.com>
Date: Sat, 9 Mar 2024 02:39:27 +0100
Subject: [PATCH 324/679] Patch in exact search for meilisearch (#29671)

meilisearch does not have an search option to contorl fuzzynes per query
right now:
 - https://github.com/meilisearch/meilisearch/issues/1192
 - https://github.com/orgs/meilisearch/discussions/377
 - https://github.com/meilisearch/meilisearch/discussions/1096

so we have to create a workaround by post-filter the search result in
gitea until this is addressed.

For future works I added an option in backend only atm, to enable
fuzzynes for issue indexer too.
And also refactored the code so the fuzzy option is equal in logic to
code indexer


---
*Sponsored by Kithara Software GmbH*
---
 modules/indexer/code/bleve/bleve.go           | 12 +--
 .../code/elasticsearch/elasticsearch.go       |  8 +-
 modules/indexer/code/indexer_test.go          |  2 +-
 modules/indexer/code/internal/indexer.go      |  4 +-
 modules/indexer/code/search.go                |  5 +-
 modules/indexer/internal/bleve/query.go       |  7 ++
 modules/indexer/issues/bleve/bleve.go         | 17 +++-
 .../issues/elasticsearch/elasticsearch.go     | 12 ++-
 modules/indexer/issues/internal/model.go      |  2 +
 .../indexer/issues/meilisearch/meilisearch.go | 91 +++++++++++++++++--
 .../issues/meilisearch/meilisearch_test.go    | 45 +++++++++
 routers/web/explore/code.go                   |  4 +-
 routers/web/repo/search.go                    |  4 +-
 routers/web/user/code.go                      |  4 +-
 14 files changed, 184 insertions(+), 33 deletions(-)

diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go
index 8ba50ed77c..107dd23598 100644
--- a/modules/indexer/code/bleve/bleve.go
+++ b/modules/indexer/code/bleve/bleve.go
@@ -233,21 +233,21 @@ func (b *Indexer) Delete(_ context.Context, repoID int64) error {
 
 // Search searches for files in the specified repo.
 // Returns the matching file-paths
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
 	var (
 		indexerQuery query.Query
 		keywordQuery query.Query
 	)
 
-	if isMatch {
-		prefixQuery := bleve.NewPrefixQuery(keyword)
-		prefixQuery.FieldVal = "Content"
-		keywordQuery = prefixQuery
-	} else {
+	if isFuzzy {
 		phraseQuery := bleve.NewMatchPhraseQuery(keyword)
 		phraseQuery.FieldVal = "Content"
 		phraseQuery.Analyzer = repoIndexerAnalyzer
 		keywordQuery = phraseQuery
+	} else {
+		prefixQuery := bleve.NewPrefixQuery(keyword)
+		prefixQuery.FieldVal = "Content"
+		keywordQuery = prefixQuery
 	}
 
 	if len(repoIDs) > 0 {
diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go
index 0f70f13485..065b0b2061 100644
--- a/modules/indexer/code/elasticsearch/elasticsearch.go
+++ b/modules/indexer/code/elasticsearch/elasticsearch.go
@@ -281,10 +281,10 @@ func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLan
 }
 
 // Search searches for codes and language stats by given conditions.
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
-	searchType := esMultiMatchTypeBestFields
-	if isMatch {
-		searchType = esMultiMatchTypePhrasePrefix
+func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+	searchType := esMultiMatchTypePhrasePrefix
+	if isFuzzy {
+		searchType = esMultiMatchTypeBestFields
 	}
 
 	kwQuery := elastic.NewMultiMatchQuery(keyword, "content").Type(searchType)
diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go
index 5eb8e61e3d..23dbd63410 100644
--- a/modules/indexer/code/indexer_test.go
+++ b/modules/indexer/code/indexer_test.go
@@ -70,7 +70,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
 
 		for _, kw := range keywords {
 			t.Run(kw.Keyword, func(t *testing.T) {
-				total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, false)
+				total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, true)
 				assert.NoError(t, err)
 				assert.Len(t, kw.IDs, int(total))
 				assert.Len(t, langs, kw.Langs)
diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go
index da3ac3623c..c92419deb2 100644
--- a/modules/indexer/code/internal/indexer.go
+++ b/modules/indexer/code/internal/indexer.go
@@ -16,7 +16,7 @@ type Indexer interface {
 	internal.Indexer
 	Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error
 	Delete(ctx context.Context, repoID int64) error
-	Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error)
+	Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error)
 }
 
 // NewDummyIndexer returns a dummy indexer
@@ -38,6 +38,6 @@ func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error {
 	return fmt.Errorf("indexer is not ready")
 }
 
-func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) {
+func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error) {
 	return 0, nil, nil, fmt.Errorf("indexer is not ready")
 }
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 2ddc2397fa..89a62a8d3e 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -124,12 +124,13 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 }
 
 // PerformSearch perform a search on a repository
-func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*internal.SearchResultLanguages, error) {
+// if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2
+func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int, []*Result, []*internal.SearchResultLanguages, error) {
 	if len(keyword) == 0 {
 		return 0, nil, nil, nil
 	}
 
-	total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch)
+	total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isFuzzy)
 	if err != nil {
 		return 0, nil, nil, err
 	}
diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go
index c7d66538c1..2a427c4020 100644
--- a/modules/indexer/internal/bleve/query.go
+++ b/modules/indexer/internal/bleve/query.go
@@ -25,6 +25,13 @@ func MatchPhraseQuery(matchPhrase, field, analyzer string) *query.MatchPhraseQue
 	return q
 }
 
+// PrefixQuery generates a match prefix query for the given prefix and field
+func PrefixQuery(matchPrefix, field string) *query.PrefixQuery {
+	q := bleve.NewPrefixQuery(matchPrefix)
+	q.FieldVal = field
+	return q
+}
+
 // BoolFieldQuery generates a bool field query for the given value and field
 func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery {
 	q := bleve.NewBoolFieldQuery(value)
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index 6a5d65cb66..aaea854efa 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -156,12 +156,19 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	var queries []query.Query
 
 	if options.Keyword != "" {
-		keywordQueries := []query.Query{
-			inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer),
-			inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer),
-			inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer),
+		if options.IsFuzzyKeyword {
+			queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
+				inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer),
+				inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer),
+				inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer),
+			}...))
+		} else {
+			queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
+				inner_bleve.PrefixQuery(options.Keyword, "title"),
+				inner_bleve.PrefixQuery(options.Keyword, "content"),
+				inner_bleve.PrefixQuery(options.Keyword, "comments"),
+			}...))
 		}
-		queries = append(queries, bleve.NewDisjunctionQuery(keywordQueries...))
 	}
 
 	if len(options.RepoIDs) > 0 || options.AllPublic {
diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go
index 3acd3ade71..0077da263a 100644
--- a/modules/indexer/issues/elasticsearch/elasticsearch.go
+++ b/modules/indexer/issues/elasticsearch/elasticsearch.go
@@ -19,6 +19,10 @@ import (
 
 const (
 	issueIndexerLatestVersion = 1
+	// multi-match-types, currently only 2 types are used
+	// Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types
+	esMultiMatchTypeBestFields   = "best_fields"
+	esMultiMatchTypePhrasePrefix = "phrase_prefix"
 )
 
 var _ internal.Indexer = &Indexer{}
@@ -141,7 +145,13 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	query := elastic.NewBoolQuery()
 
 	if options.Keyword != "" {
-		query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments"))
+
+		searchType := esMultiMatchTypePhrasePrefix
+		if options.IsFuzzyKeyword {
+			searchType = esMultiMatchTypeBestFields
+		}
+
+		query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(searchType))
 	}
 
 	if len(options.RepoIDs) > 0 {
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index 947335d8ce..d41fec4aba 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -74,6 +74,8 @@ type SearchResult struct {
 type SearchOptions struct {
 	Keyword string // keyword to search
 
+	IsFuzzyKeyword bool // if false the levenshtein distance is 0
+
 	RepoIDs   []int64 // repository IDs which the issues belong to
 	AllPublic bool    // if include all public repositories
 
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index 325883196b..c429920065 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -5,6 +5,7 @@ package meilisearch
 
 import (
 	"context"
+	"errors"
 	"strconv"
 	"strings"
 
@@ -16,12 +17,15 @@ import (
 )
 
 const (
-	issueIndexerLatestVersion = 2
+	issueIndexerLatestVersion = 3
 
 	// TODO: make this configurable if necessary
 	maxTotalHits = 10000
 )
 
+// ErrMalformedResponse is never expected as we initialize the indexer ourself and so define the types.
+var ErrMalformedResponse = errors.New("meilisearch returned unexpected malformed content")
+
 var _ internal.Indexer = &Indexer{}
 
 // Indexer implements Indexer interface
@@ -47,6 +51,9 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer {
 		},
 		DisplayedAttributes: []string{
 			"id",
+			"title",
+			"content",
+			"comments",
 		},
 		FilterableAttributes: []string{
 			"repo_id",
@@ -221,11 +228,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		return nil, err
 	}
 
-	hits := make([]internal.Match, 0, len(searchRes.Hits))
-	for _, hit := range searchRes.Hits {
-		hits = append(hits, internal.Match{
-			ID: int64(hit.(map[string]any)["id"].(float64)),
-		})
+	hits, err := nonFuzzyWorkaround(searchRes, options.Keyword, options.IsFuzzyKeyword)
+	if err != nil {
+		return nil, err
 	}
 
 	return &internal.SearchResult{
@@ -241,3 +246,77 @@ func parseSortBy(sortBy internal.SortBy) string {
 	}
 	return field + ":asc"
 }
+
+// nonFuzzyWorkaround is needed as meilisearch does not have an exact search
+// and you can only change "typo tolerance" per index. So we have to post-filter the results
+// https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#configuring-typo-tolerance
+// TODO: remove once https://github.com/orgs/meilisearch/discussions/377 is addressed
+func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, isFuzzy bool) ([]internal.Match, error) {
+	hits := make([]internal.Match, 0, len(searchRes.Hits))
+	for _, hit := range searchRes.Hits {
+		hit, ok := hit.(map[string]any)
+		if !ok {
+			return nil, ErrMalformedResponse
+		}
+
+		if !isFuzzy {
+			keyword = strings.ToLower(keyword)
+
+			// declare a anon func to check if the title, content or at least one comment contains the keyword
+			found, err := func() (bool, error) {
+				// check if title match first
+				title, ok := hit["title"].(string)
+				if !ok {
+					return false, ErrMalformedResponse
+				} else if strings.Contains(strings.ToLower(title), keyword) {
+					return true, nil
+				}
+
+				// check if content has a match
+				content, ok := hit["content"].(string)
+				if !ok {
+					return false, ErrMalformedResponse
+				} else if strings.Contains(strings.ToLower(content), keyword) {
+					return true, nil
+				}
+
+				// now check for each comment if one has a match
+				// so we first try to cast and skip if there are no comments
+				comments, ok := hit["comments"].([]any)
+				if !ok {
+					return false, ErrMalformedResponse
+				} else if len(comments) == 0 {
+					return false, nil
+				}
+
+				// now we iterate over all and report as soon as we detect one match
+				for i := range comments {
+					comment, ok := comments[i].(string)
+					if !ok {
+						return false, ErrMalformedResponse
+					}
+					if strings.Contains(strings.ToLower(comment), keyword) {
+						return true, nil
+					}
+				}
+
+				// we got no match
+				return false, nil
+			}()
+
+			if err != nil {
+				return nil, err
+			} else if !found {
+				continue
+			}
+		}
+		issueID, ok := hit["id"].(float64)
+		if !ok {
+			return nil, ErrMalformedResponse
+		}
+		hits = append(hits, internal.Match{
+			ID: int64(issueID),
+		})
+	}
+	return hits, nil
+}
diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go
index 3d7237268e..ecce704236 100644
--- a/modules/indexer/issues/meilisearch/meilisearch_test.go
+++ b/modules/indexer/issues/meilisearch/meilisearch_test.go
@@ -10,7 +10,11 @@ import (
 	"testing"
 	"time"
 
+	"code.gitea.io/gitea/modules/indexer/issues/internal"
 	"code.gitea.io/gitea/modules/indexer/issues/internal/tests"
+
+	"github.com/meilisearch/meilisearch-go"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestMeilisearchIndexer(t *testing.T) {
@@ -48,3 +52,44 @@ func TestMeilisearchIndexer(t *testing.T) {
 
 	tests.TestIndexer(t, indexer)
 }
+
+func TestNonFuzzyWorkaround(t *testing.T) {
+	// get unexpected return
+	_, err := nonFuzzyWorkaround(&meilisearch.SearchResponse{
+		Hits: []any{"aa", "bb", "cc", "dd"},
+	}, "bowling", false)
+	assert.ErrorIs(t, err, ErrMalformedResponse)
+
+	validResponse := &meilisearch.SearchResponse{
+		Hits: []any{
+			map[string]any{
+				"id":       float64(11),
+				"title":    "a title",
+				"content":  "issue body with no match",
+				"comments": []any{"hey whats up?", "I'm currently bowling", "nice"},
+			},
+			map[string]any{
+				"id":       float64(22),
+				"title":    "Bowling as title",
+				"content":  "",
+				"comments": []any{},
+			},
+			map[string]any{
+				"id":       float64(33),
+				"title":    "Bowl-ing as fuzzy match",
+				"content":  "",
+				"comments": []any{},
+			},
+		},
+	}
+
+	// nonFuzzy
+	hits, err := nonFuzzyWorkaround(validResponse, "bowling", false)
+	assert.NoError(t, err)
+	assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}}, hits)
+
+	// fuzzy
+	hits, err = nonFuzzyWorkaround(validResponse, "bowling", true)
+	assert.NoError(t, err)
+	assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits)
+}
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index 2cde8b655e..a6bc71ac9c 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -35,7 +35,7 @@ func Code(ctx *context.Context) {
 	keyword := ctx.FormTrim("q")
 
 	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
+	isFuzzy := queryType != "match"
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
@@ -77,7 +77,7 @@ func Code(ctx *context.Context) {
 	)
 
 	if (len(repoIDs) > 0) || isAdmin {
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
 		if err != nil {
 			if code_indexer.IsAvailable(ctx) {
 				ctx.ServerError("SearchResults", err)
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index c53d8fd918..766dd5726a 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -25,7 +25,7 @@ func Search(ctx *context.Context) {
 	keyword := ctx.FormTrim("q")
 
 	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
+	isFuzzy := queryType != "match"
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
@@ -43,7 +43,7 @@ func Search(ctx *context.Context) {
 	}
 
 	total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
-		language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
 	if err != nil {
 		if code_indexer.IsAvailable(ctx) {
 			ctx.ServerError("SearchResults", err)
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index eb711b76eb..8613d38b65 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -40,7 +40,7 @@ func CodeSearch(ctx *context.Context) {
 	keyword := ctx.FormTrim("q")
 
 	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
+	isFuzzy := queryType != "match"
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
@@ -75,7 +75,7 @@ func CodeSearch(ctx *context.Context) {
 	)
 
 	if len(repoIDs) > 0 {
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
 		if err != nil {
 			if code_indexer.IsAvailable(ctx) {
 				ctx.ServerError("SearchResults", err)

From 1dc7f5338623ec97d9ea395380270470847a0066 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 9 Mar 2024 13:59:16 +0200
Subject: [PATCH 325/679] Fix WebHookEditor regression from jQuery removal
 (#29692)

Make these calls optional

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/comp/WebHookEditor.js | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js
index 86d21dc815..b7ca5a0fcf 100644
--- a/web_src/js/features/comp/WebHookEditor.js
+++ b/web_src/js/features/comp/WebHookEditor.js
@@ -22,13 +22,16 @@ export function initCompWebHookEditor() {
     });
   }
 
-  const updateContentType = function () {
-    const visible = document.getElementById('http_method').value === 'POST';
-    toggleElem(document.getElementById('content_type').parentNode.parentNode, visible);
-  };
-  updateContentType();
-
-  document.getElementById('http_method').addEventListener('change', updateContentType);
+  // some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field
+  const httpMethodInput = document.getElementById('http_method');
+  if (httpMethodInput) {
+    const updateContentType = function () {
+      const visible = httpMethodInput.value === 'POST';
+      toggleElem(document.getElementById('content_type').closest('.field'), visible);
+    };
+    updateContentType();
+    httpMethodInput.addEventListener('change', updateContentType);
+  }
 
   // Test delivery
   document.getElementById('test-delivery')?.addEventListener('click', async function () {

From 1695a5ac74afc51f38fd3a1def76cff6ba8d8641 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 9 Mar 2024 13:09:22 +0100
Subject: [PATCH 326/679] Include go files in tailwind processing (#29686)

We need to scan `.go` files for tailwind classes. Does not seem to
affect build time much luckily.

Fixes:
https://github.com/go-gitea/gitea/pull/29678#discussion_r1518448600

Verified via `rg tw-object-contain public/assets/css/index.css`.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 tailwind.config.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/tailwind.config.js b/tailwind.config.js
index 63a5387d19..d783268bd7 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -31,6 +31,9 @@ export default {
     isProduction && '!./web_src/js/standalone/devtest.js',
     '!./templates/swagger/v1_json.tmpl',
     '!./templates/user/auth/oidc_wellknown.tmpl',
+    '!**/*_test.go',
+    '!./modules/{public,options,templates}/bindata.go',
+    './{build,models,modules,routers,services}/**/*.go',
     './templates/**/*.tmpl',
     './web_src/js/**/*.{js,vue}',
   ].filter(Boolean),

From 9b69f76e5a33788150f3abc3dee64010539c6b86 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 9 Mar 2024 13:14:42 +0100
Subject: [PATCH 327/679] Completely style the webkit autofill (#29683)

Previously it was only partially styled, e.g. there was black text on
white background even in dark theme caused by fomantic styles.

<img width="195" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/bc5cf516-2aef-45c3-854a-c9f5497aacca">

<img width="195" alt="Screenshot 2024-03-09 at 02 09 29"
src="https://github.com/go-gitea/gitea/assets/115237/ef0af17d-6e0b-402e-b24d-bfa34dc2f4e0">

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/base.css | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 3db9cd894c..e53e0619c8 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -872,10 +872,18 @@ img.ui.avatar,
   border-color: var(--color-error-border) !important;
 }
 
-/* A fix for text visibility issue in Chrome autofill in dark mode. */
-/* It's a problem from Formatic UI, and this rule overrides it. */
-.ui.form .field.field input:-webkit-autofill {
-  -webkit-text-fill-color: var(--color-black) !important;
+input:-webkit-autofill,
+input:-webkit-autofill:focus,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:active,
+.ui.form .field.field input:-webkit-autofill,
+.ui.form .field.field input:-webkit-autofill:focus,
+.ui.form .field.field input:-webkit-autofill:hover,
+.ui.form .field.field input:-webkit-autofill:active {
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: var(--color-text);
+  box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important;
+  border-color: var(--color-primary-light-4) !important;
 }
 
 .ui.form .field.muted {

From 6ea1c67eadaf65079958cc4ad3b014966e47dd1a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 9 Mar 2024 13:41:32 +0100
Subject: [PATCH 328/679] Update allowed attachment types (#29688)

Update to match GitHub's latest.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 docs/content/administration/config-cheat-sheet.en-us.md | 2 +-
 docs/content/administration/config-cheat-sheet.zh-cn.md | 2 +-
 modules/setting/attachment.go                           | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 8a01711949..43ec470ad0 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -832,7 +832,7 @@ Default templates for project boards:
 ## Issue and pull request attachments (`attachment`)
 
 - `ENABLED`: **true**: Whether issue and pull request attachments are enabled.
-- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
+- `ALLOWED_TYPES`: **.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
 - `MAX_SIZE`: **2048**: Maximum size (MB).
 - `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
 - `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 7b102eda8e..1f98db78aa 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -782,7 +782,7 @@ Gitea 创建以下非唯一队列:
 ## 工单和合并请求的附件 (`attachment`)
 
 - `ENABLED`: **true**: 是否允许用户上传附件。
-- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。
+- `ALLOWED_TYPES`: **.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。
 - `MAX_SIZE`: **2048**: 附件的最大限制(MB)。
 - `MAX_FILES`: **5**: 一次最多上传的附件数量。
 - `STORAGE_TYPE`: **local**: 附件的存储类型,`local` 表示本地磁盘,`minio` 表示兼容 S3 的对象存储服务,如果未设置将使用默认值 `local` 或其他在 `[storage.xxx]` 中定义的名称。
diff --git a/modules/setting/attachment.go b/modules/setting/attachment.go
index 934d4d7f46..0fdabb5032 100644
--- a/modules/setting/attachment.go
+++ b/modules/setting/attachment.go
@@ -12,7 +12,7 @@ var Attachment = struct {
 	Enabled      bool
 }{
 	Storage:      &Storage{},
-	AllowedTypes: ".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
+	AllowedTypes: ".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
 	MaxSize:      2048,
 	MaxFiles:     5,
 	Enabled:      true,
@@ -25,7 +25,7 @@ func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
 		return err
 	}
 
-	Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
+	Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
 	Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(2048)
 	Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
 	Attachment.Enabled = sec.Key("ENABLED").MustBool(true)

From a192a5ed99c2a244d0f015d62088642eb5a81d75 Mon Sep 17 00:00:00 2001
From: Chongyi Zheng <git@zcy.dev>
Date: Sat, 9 Mar 2024 08:13:08 -0500
Subject: [PATCH 329/679] Fix action runner offline label padding (#29691)

Before:

The `offline` padding is `calc(.833em - 1px)` from `basic` CSS class,
but `idle` padding is `6px`.

<img width="1035" alt="image"
src="https://github.com/go-gitea/gitea/assets/37034805/ccb42615-20d7-4032-a805-40cd9643012d">

After:

<img width="1035" alt="image"
src="https://github.com/go-gitea/gitea/assets/37034805/d6af99c8-76cb-4850-96d6-5289b06e1ca8">
---
 templates/shared/actions/runner_list.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl
index 0e8f3cb874..443c455faf 100644
--- a/templates/shared/actions/runner_list.tmpl
+++ b/templates/shared/actions/runner_list.tmpl
@@ -68,7 +68,7 @@
 					{{range .Runners}}
 					<tr>
 						<td>
-							<span class="ui {{if .IsOnline}}green{{else}}basic{{end}} label">{{.StatusLocaleName ctx.Locale}}</span>
+							<span class="ui {{if .IsOnline}}green{{end}} label">{{.StatusLocaleName ctx.Locale}}</span>
 						</td>
 						<td>{{.ID}}</td>
 						<td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td>

From 9bf693d98ddf8efa058a5fbbb6a3da5e0c12ab27 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 9 Mar 2024 18:37:29 +0100
Subject: [PATCH 330/679] Suppress error from monaco-editor (#29684)

Fixes: https://github.com/go-gitea/gitea/issues/29414

I see no way for us to catch this error, so downgrade it until
https://github.com/microsoft/monaco-editor/issues/4325 is fixed, which
will likely take a few weeks to propagate up from vscode.

The entries in `updates.config.js` will make
[`updates`](https://github.com/silverwind/updates) not upgrade these
anymore and I think it's good documentation as well to have the reasons
why we don't upgrade these dependencies.
---
 web_src/js/bootstrap.js | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
index c0047b0ac2..698d17fa36 100644
--- a/web_src/js/bootstrap.js
+++ b/web_src/js/bootstrap.js
@@ -6,10 +6,18 @@
 // This file must be imported before any lazy-loading is being attempted.
 __webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
 
+const filteredErrors = new Set([
+  'getModifierState is not a function', // https://github.com/microsoft/monaco-editor/issues/4325
+]);
+
 export function showGlobalErrorMessage(msg) {
   const pageContent = document.querySelector('.page-content');
   if (!pageContent) return;
 
+  for (const filteredError of filteredErrors) {
+    if (msg.includes(filteredError)) return;
+  }
+
   // compact the message to a data attribute to avoid too many duplicated messages
   const msgCompact = msg.replace(/\W/g, '').trim();
   let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);

From 6e8762f962c5eaaee1c92e910c95c8b85b7c1e11 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sun, 10 Mar 2024 09:32:48 +0800
Subject: [PATCH 331/679] Fix broken webhooks (#29690)

Fix #29689
---
 services/webhook/payloader.go | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index abf9946cca..54a11a5868 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -94,7 +94,12 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
 		return nil, nil, err
 	}
 
-	req, err := http.NewRequest(w.HTTPMethod, w.URL, bytes.NewReader(body))
+	method := w.HTTPMethod
+	if method == "" {
+		method = http.MethodPost
+	}
+
+	req, err := http.NewRequest(method, w.URL, bytes.NewReader(body))
 	if err != nil {
 		return nil, nil, err
 	}

From 5665a0212bf391a2b61add34a4633b5d7495cf34 Mon Sep 17 00:00:00 2001
From: Ankit R Gadiya <git@argp.in>
Date: Sun, 10 Mar 2024 22:00:14 +0530
Subject: [PATCH 332/679] fix: rendering internal file links in org (#29669)

The internal links to other files in the repository were not rendering
with the Src Prefix (/src/branch-name/file-path). This commit fixes that
by using the `SrcLink` as base if available.

Resolves #29668
---
 modules/markup/orgmode/orgmode.go      |  8 ++++++
 modules/markup/orgmode/orgmode_test.go | 38 ++++++++++++++++++++++----
 2 files changed, 40 insertions(+), 6 deletions(-)

diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index 7f253ae5f1..25f8d15ef4 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -142,10 +142,18 @@ func (r *Writer) resolveLink(kind, link string) string {
 			// so we need to try to guess the link kind again here
 			kind = org.RegularLink{URL: link}.Kind()
 		}
+
 		base := r.Ctx.Links.Base
+		if r.Ctx.IsWiki {
+			base = r.Ctx.Links.WikiLink()
+		} else if r.Ctx.Links.HasBranchInfo() {
+			base = r.Ctx.Links.SrcLink()
+		}
+
 		if kind == "image" || kind == "video" {
 			base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
 		}
+
 		link = util.URLJoin(base, link)
 	}
 	return link
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index 95f53c9cc9..75b60ed81f 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -19,6 +19,30 @@ const AppURL = "http://localhost:3000/"
 func TestRender_StandardLinks(t *testing.T) {
 	setting.AppURL = AppURL
 
+	test := func(input, expected string, isWiki bool) {
+		buffer, err := RenderString(&markup.RenderContext{
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base:       "/relative-path",
+				BranchPath: "branch/main",
+			},
+			IsWiki: isWiki,
+		}, input)
+		assert.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+	}
+
+	test("[[https://google.com/]]",
+		`<p><a href="https://google.com/">https://google.com/</a></p>`, false)
+	test("[[WikiPage][The WikiPage Desc]]",
+		`<p><a href="/relative-path/wiki/WikiPage">The WikiPage Desc</a></p>`, true)
+	test("[[ImageLink.svg][The Image Desc]]",
+		`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`, false)
+}
+
+func TestRender_InternalLinks(t *testing.T) {
+	setting.AppURL = AppURL
+
 	test := func(input, expected string) {
 		buffer, err := RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
@@ -31,12 +55,14 @@ func TestRender_StandardLinks(t *testing.T) {
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 	}
 
-	test("[[https://google.com/]]",
-		`<p><a href="https://google.com/">https://google.com/</a></p>`)
-	test("[[WikiPage][The WikiPage Desc]]",
-		`<p><a href="/relative-path/WikiPage">The WikiPage Desc</a></p>`)
-	test("[[ImageLink.svg][The Image Desc]]",
-		`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`)
+	test("[[file:test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+	test("[[./test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+	test("[[test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+	test("[[path/to/test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/path/to/test.org">Test</a></p>`)
 }
 
 func TestRender_Media(t *testing.T) {

From b5ed42864e306f22305beba50d6ff71df2aea0c6 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 10 Mar 2024 21:26:41 +0200
Subject: [PATCH 333/679] Remove jQuery AJAX from the comment edit history
 (#29703)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the comment edit history list, diff, and delete functionality
and it works as before

# Demo using `fetch` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/e8c557bc-f2b9-4d73-b55e-0850c1b19364)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-issue-content.js | 58 ++++++++++++++---------
 1 file changed, 36 insertions(+), 22 deletions(-)

diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index 7832641687..9e2b773730 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -1,8 +1,9 @@
 import $ from 'jquery';
 import {svg} from '../svg.js';
 import {showErrorToast} from '../modules/toast.js';
+import {GET, POST} from '../modules/fetch.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 let i18nTextEdited;
 let i18nTextOptions;
 let i18nTextDeleteFromHistory;
@@ -31,19 +32,27 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
   $dialog.find('.dialog-header-options').dropdown({
     showOnFocus: false,
     allowReselection: true,
-    onChange(_value, _text, $item) {
+    async onChange(_value, _text, $item) {
       const optionItem = $item.data('option-item');
       if (optionItem === 'delete') {
         if (window.confirm(i18nTextDeleteFromHistoryConfirm)) {
-          $.post(`${issueBaseUrl}/content-history/soft-delete?comment_id=${commentId}&history_id=${historyId}`, {
-            _csrf: csrfToken,
-          }).done((resp) => {
+          try {
+            const params = new URLSearchParams();
+            params.append('comment_id', commentId);
+            params.append('history_id', historyId);
+
+            const response = await POST(`${issueBaseUrl}/content-history/soft-delete?${params.toString()}`);
+            const resp = await response.json();
+
             if (resp.ok) {
               $dialog.modal('hide');
             } else {
               showErrorToast(resp.message);
             }
-          });
+          } catch (error) {
+            console.error('Error:', error);
+            showErrorToast('An error occurred while deleting the history.');
+          }
         }
       } else { // required by eslint
         showErrorToast(`unknown option item: ${optionItem}`);
@@ -54,19 +63,24 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
     }
   });
   $dialog.modal({
-    onShow() {
-      $.ajax({
-        url: `${issueBaseUrl}/content-history/detail?comment_id=${commentId}&history_id=${historyId}`,
-        data: {
-          _csrf: csrfToken,
-        },
-      }).done((resp) => {
+    async onShow() {
+      try {
+        const params = new URLSearchParams();
+        params.append('comment_id', commentId);
+        params.append('history_id', historyId);
+
+        const url = `${issueBaseUrl}/content-history/detail?${params.toString()}`;
+        const response = await GET(url);
+        const resp = await response.json();
+
         $dialog.find('.comment-diff-data').removeClass('is-loading').html(resp.diffHtml);
         // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden.
         if (resp.canSoftDelete) {
           $dialog.find('.dialog-header-options').removeClass('gt-hidden');
         }
-      });
+      } catch (error) {
+        console.error('Error:', error);
+      }
     },
     onHidden() {
       $dialog.remove();
@@ -103,7 +117,7 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) {
   });
 }
 
-export function initRepoIssueContentHistory() {
+export async function initRepoIssueContentHistory() {
   const issueIndex = $('#issueIndex').val();
   if (!issueIndex) return;
 
@@ -114,12 +128,10 @@ export function initRepoIssueContentHistory() {
   const repoLink = $('#repolink').val();
   const issueBaseUrl = `${appSubUrl}/${repoLink}/issues/${issueIndex}`;
 
-  $.ajax({
-    url: `${issueBaseUrl}/content-history/overview`,
-    data: {
-      _csrf: csrfToken,
-    },
-  }).done((resp) => {
+  try {
+    const response = await GET(`${issueBaseUrl}/content-history/overview`);
+    const resp = await response.json();
+
     i18nTextEdited = resp.i18n.textEdited;
     i18nTextDeleteFromHistory = resp.i18n.textDeleteFromHistory;
     i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm;
@@ -133,5 +145,7 @@ export function initRepoIssueContentHistory() {
       const $itemComment = $(`#issuecomment-${commentId}`);
       showContentHistoryMenu(issueBaseUrl, $itemComment, commentId);
     }
-  });
+  } catch (error) {
+    console.error('Error:', error);
+  }
 }

From 851bd18234ff3de4c603c57c3b380eb5495d8eb7 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 10 Mar 2024 20:28:59 +0100
Subject: [PATCH 334/679] Improve CSV rendering (#29638)

Before:

<img width="1332" alt="Screenshot 2024-03-06 at 21 42 17"
src="https://github.com/go-gitea/gitea/assets/115237/0ea07eee-31f8-4783-bd56-37bd8396f00d">

After:
<img width="1336" alt="Screenshot 2024-03-06 at 21 41 58"
src="https://github.com/go-gitea/gitea/assets/115237/eb7f9cc9-587f-4e3b-92bd-cc67ca639963">
---
 web_src/css/repo.css | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index d60fb4db21..03d9664331 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1420,6 +1420,7 @@
 
 .repository .data-table tr {
   border-top: 0;
+  background: none !important;
 }
 
 .repository .data-table td,
@@ -1432,6 +1433,21 @@
   border: 1px solid var(--color-secondary);
 }
 
+/* the border css competes with .markup where all tables have outer border which would add a double
+   border here, remove only the outer borders from this table */
+.repository .data-table tr:first-child :is(td,th) {
+  border-top: none !important;
+}
+.repository .data-table tr:last-child :is(td,th) {
+  border-bottom: none !important;
+}
+.repository .data-table tr :is(td,th):first-child {
+  border-left: none !important;
+}
+.repository .data-table tr :is(td,th):last-child {
+  border-right: none !important;
+}
+
 .repository .data-table td {
   white-space: pre-line;
 }
@@ -1469,7 +1485,7 @@
   min-width: 50px;
   font-family: monospace;
   line-height: 20px;
-  color: var(--color-secondary-dark-2);
+  color: var(--color-text-light-1);
   white-space: nowrap;
   vertical-align: top;
   cursor: pointer;

From 3c6fc25a77c37d50686caa495d27a31dcef7f75f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 11 Mar 2024 05:30:36 +0800
Subject: [PATCH 335/679] Use repo object format name instead of detecting from
 git repository (#29702)

It's unnecessary to detect the repository object format from git
repository. Just use the repository's object format name.
---
 modules/indexer/code/git.go         | 12 ++++--------
 routers/web/repo/branch.go          |  7 +------
 routers/web/repo/setting/webhook.go |  7 +------
 services/mirror/mirror_pull.go      |  5 +----
 services/pull/pull.go               |  2 +-
 services/release/release.go         |  5 +----
 6 files changed, 9 insertions(+), 29 deletions(-)

diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go
index 76cd78e11e..f105d032eb 100644
--- a/modules/indexer/code/git.go
+++ b/modules/indexer/code/git.go
@@ -91,11 +91,9 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s
 		return nil, runErr
 	}
 
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
 	var err error
-	objectFormat, err := git.GetObjectFormatOfRepo(ctx, repo.RepoPath())
-	if err != nil {
-		return nil, err
-	}
 	changes.Updates, err = parseGitLsTreeOutput(objectFormat, stdout)
 	return &changes, err
 }
@@ -174,10 +172,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio
 		return nil, err
 	}
 
-	objectFormat, err := git.GetObjectFormatOfRepo(ctx, repo.RepoPath())
-	if err != nil {
-		return nil, err
-	}
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
 	changes.Updates, err = parseGitLsTreeOutput(objectFormat, lsTreeStdout)
 	return &changes, err
 }
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index ae51f0596b..f879a98786 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -148,12 +148,7 @@ func RestoreBranchPost(ctx *context.Context) {
 		return
 	}
 
-	objectFormat, err := git.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository.RepoPath())
-	if err != nil {
-		log.Error("RestoreBranch: CreateBranch: %w", err)
-		ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
-		return
-	}
+	objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
 
 	// Don't return error below this
 	if err := repo_service.PushUpdate(
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index bba4d4df51..c8e621fac8 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -656,12 +656,7 @@ func TestWebhook(ctx *context.Context) {
 	commit := ctx.Repo.Commit
 	if commit == nil {
 		ghost := user_model.NewGhostUser()
-		objectFormat, err := git.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository.RepoPath())
-		if err != nil {
-			ctx.Flash.Error("GetObjectFormatOfRepo: " + err.Error())
-			ctx.Status(http.StatusInternalServerError)
-			return
-		}
+		objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
 		commit = &git.Commit{
 			ID:            objectFormat.EmptyObjectID(),
 			Author:        ghost.NewGitSig(),
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index de4a58f27b..2a38d4ba55 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -479,10 +479,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
 				log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err)
 				continue
 			}
-			objectFormat, err := git.GetObjectFormatOfRepo(ctx, m.Repo.RepoPath())
-			if err != nil {
-				log.Error("SyncMirrors [repo: %-v]: unable to GetHashTypeOfRepo: %v", m.Repo, err)
-			}
+			objectFormat := git.ObjectFormatFromName(m.Repo.ObjectFormatName)
 			notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
 				RefFullName: result.refName,
 				OldCommitID: objectFormat.EmptyObjectID().String(),
diff --git a/services/pull/pull.go b/services/pull/pull.go
index be3d25d20a..9133a72acf 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -337,7 +337,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 			}
 			if err == nil {
 				for _, pr := range prs {
-					objectFormat, _ := git.GetObjectFormatOfRepo(ctx, pr.BaseRepo.RepoPath())
+					objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
 					if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
 						changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
 						if err != nil {
diff --git a/services/release/release.go b/services/release/release.go
index a359e5078e..ba5fd1dd98 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -326,10 +326,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
 		}
 
 		refName := git.RefNameFromTag(rel.TagName)
-		objectFormat, err := git.GetObjectFormatOfRepo(ctx, repo.RepoPath())
-		if err != nil {
-			return err
-		}
+		objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 		notify_service.PushCommits(
 			ctx, doer, repo,
 			&repository.PushUpdateOptions{

From 8fc1a8f0eb642c574610a346e858d42c433ebe01 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 11 Mar 2024 14:00:50 +0900
Subject: [PATCH 336/679] Fix inconsistent rendering of block mathematical
 expressions (#29677)

Fix #28735

GitHub render `\```math\``` ` as a block now.
Add `display` class will render it as a block.

After:

![image](https://github.com/go-gitea/gitea/assets/18380374/2a1c20c7-438e-4ab1-8c66-cf91c8343087)

![image](https://github.com/go-gitea/gitea/assets/18380374/b81b8a93-8bca-46a5-b7db-e0d2f53e1342)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/markup/markdown/markdown.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index f0b1afa27e..4cca71d511 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -104,7 +104,8 @@ func SpecializedMarkdown() goldmark.Markdown {
 							}
 
 							// include language-x class as part of commonmark spec
-							_, err = w.WriteString(`<code class="chroma language-` + string(language) + `">`)
+							// the "display" class is used by "js/markup/math.js" to render the code element as a block
+							_, err = w.WriteString(`<code class="chroma language-` + string(language) + ` display">`)
 							if err != nil {
 								return
 							}

From 4129e0e79bbf30e4297efd33feb2602c40322d10 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Mon, 11 Mar 2024 14:07:36 +0800
Subject: [PATCH 337/679] Add a warning for disallowed email domains (#29658)

Resolve #29660

Follow #29522 and #29609

Add a warning for disallowed email domains when admins manually add/edit
users.

Thanks @yp05327 for the
[comment](https://github.com/go-gitea/gitea/pull/29605#issuecomment-1980105119)

![image](https://github.com/go-gitea/gitea/assets/15528715/6737b221-a3a2-4180-9ef8-b846c10f96e0)
---
 models/user/email_address.go        | 18 +++++++++---------
 options/locale/locale_en-US.ini     |  1 +
 routers/api/v1/admin/user.go        |  9 +++++++++
 routers/web/admin/users.go          |  8 ++++++++
 services/forms/user_form.go         |  8 ++------
 tests/integration/api_admin_test.go |  6 ++++--
 6 files changed, 33 insertions(+), 17 deletions(-)

diff --git a/models/user/email_address.go b/models/user/email_address.go
index 11700a0129..a9dbb8e891 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -539,17 +539,17 @@ func validateEmailBasic(email string) error {
 
 // validateEmailDomain checks whether the email domain is allowed or blocked
 func validateEmailDomain(email string) error {
-	// if there is no allow list, then check email against block list
-	if len(setting.Service.EmailDomainAllowList) == 0 &&
-		validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
-		return ErrEmailInvalid{email}
-	}
-
-	// if there is an allow list, then check email against allow list
-	if len(setting.Service.EmailDomainAllowList) > 0 &&
-		!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
+	if !IsEmailDomainAllowed(email) {
 		return ErrEmailInvalid{email}
 	}
 
 	return nil
 }
+
+func IsEmailDomainAllowed(email string) bool {
+	if len(setting.Service.EmailDomainAllowList) == 0 {
+		return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email)
+	}
+
+	return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index e7ba7dd8c9..afd613af59 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -573,6 +573,7 @@ team_name_been_taken = The team name is already taken.
 team_no_units_error = Allow access to at least one repository section.
 email_been_used = The email address is already used.
 email_invalid = The email address is invalid.
+email_domain_is_not_allowed = The domain of user email <b>%s</b> conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST. Please ensure your operation is expected.
 openid_been_used = The OpenID address "%s" is already used.
 username_password_incorrect = Username or password is incorrect.
 password_complexity = Password does not pass complexity requirements:
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 986305d423..87a5b28fad 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -147,6 +147,11 @@ func CreateUser(ctx *context.APIContext) {
 		}
 		return
 	}
+
+	if !user_model.IsEmailDomainAllowed(u.Email) {
+		ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
+	}
+
 	log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
 
 	// Send email notification.
@@ -220,6 +225,10 @@ func EditUser(ctx *context.APIContext) {
 			}
 			return
 		}
+
+		if !user_model.IsEmailDomainAllowed(*form.Email) {
+			ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
+		}
 	}
 
 	opts := &user_service.UpdateOptions{
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index 671a0d8885..6dfcfc3d9a 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -202,6 +202,11 @@ func NewUserPost(ctx *context.Context) {
 		}
 		return
 	}
+
+	if !user_model.IsEmailDomainAllowed(u.Email) {
+		ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
+	}
+
 	log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
 
 	// Send email notification.
@@ -425,6 +430,9 @@ func EditUserPost(ctx *context.Context) {
 			}
 			return
 		}
+		if !user_model.IsEmailDomainAllowed(form.Email) {
+			ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
+		}
 	}
 
 	opts := &user_service.UpdateOptions{
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 416592bfda..e2e6c208f7 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -10,9 +10,9 @@ import (
 	"strings"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/services/context"
 
@@ -109,11 +109,7 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.
 // domains in the whitelist or if it doesn't match any of
 // domains in the blocklist, if any such list is not empty.
 func (f *RegisterForm) IsEmailDomainAllowed() bool {
-	if len(setting.Service.EmailDomainAllowList) == 0 {
-		return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, f.Email)
-	}
-
-	return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, f.Email)
+	return user_model.IsEmailDomainAllowed(f.Email)
 }
 
 // MustChangePasswordForm form for updating your password after account creation
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
index 8a330a68e2..e8954f5b20 100644
--- a/tests/integration/api_admin_test.go
+++ b/tests/integration/api_admin_test.go
@@ -354,7 +354,8 @@ func TestAPICreateUser_NotAllowedEmailDomain(t *testing.T) {
 		"password":             "allowedUser1_pass",
 		"must_change_password": "true",
 	}).AddTokenAuth(token)
-	MakeRequest(t, req, http.StatusCreated)
+	resp := MakeRequest(t, req, http.StatusCreated)
+	assert.Equal(t, "the domain of user email allowedUser1@example1.org conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning"))
 
 	req = NewRequest(t, "DELETE", "/api/v1/admin/users/allowedUser1").AddTokenAuth(token)
 	MakeRequest(t, req, http.StatusNoContent)
@@ -378,7 +379,8 @@ func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) {
 		SourceID:  0,
 		Email:     &newEmail,
 	}).AddTokenAuth(token)
-	MakeRequest(t, req, http.StatusOK)
+	resp := MakeRequest(t, req, http.StatusOK)
+	assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning"))
 
 	originalEmail := "user2@example.com"
 	req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{

From e6d141182fbd1c56cab3f9369ee4d2a3deb9ab9f Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Mon, 11 Mar 2024 14:42:50 +0800
Subject: [PATCH 338/679] Sync branches first (#29714)

Follow #29493.

Sync branches to DB first, then trigger push events.
---
 routers/private/hook_post_receive.go | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index c5504126f8..3dad39f7b1 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -82,19 +82,6 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 	}
 
 	if repo != nil && len(updates) > 0 {
-		if err := repo_service.PushUpdates(updates); err != nil {
-			log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
-			for i, update := range updates {
-				log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName())
-			}
-			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
-
-			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
-				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
-			})
-			return
-		}
-
 		branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
 		for _, update := range updates {
 			if !update.RefFullName.IsBranch() {
@@ -151,6 +138,19 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 				return
 			}
 		}
+
+		if err := repo_service.PushUpdates(updates); err != nil {
+			log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
+			for i, update := range updates {
+				log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName())
+			}
+			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+
+			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+			})
+			return
+		}
 	}
 
 	// Handle Push Options

From 7f856d5d742dcb6febdb8a3f22cd9a8fecc69a4d Mon Sep 17 00:00:00 2001
From: pengqiseven <134899215+pengqiseven@users.noreply.github.com>
Date: Mon, 11 Mar 2024 17:24:23 +0800
Subject: [PATCH 339/679] remove repetitive words (#29695)

Signed-off-by: pengqiseven <912170095@qq.com>
---
 docs/content/administration/mail-templates.en-us.md | 2 +-
 models/issues/issue_search.go                       | 2 +-
 models/user/user.go                                 | 2 +-
 modules/git/commit.go                               | 2 +-
 routers/api/v1/repo/issue.go                        | 2 +-
 routers/api/v1/repo/pull_review.go                  | 2 +-
 templates/swagger/v1_json.tmpl                      | 2 +-
 7 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index 4026b89975..8e4e416e8d 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -163,7 +163,7 @@ clients don't even support HTML, so they show the text version included in the g
 
 If the template fails to render, it will be noticed only at the moment the mail is sent.
 A default subject is used if the subject template fails, and whatever was rendered successfully
-from the the _mail body_ is used, disregarding the rest.
+from the _mail body_ is used, disregarding the rest.
 
 Please check [Gitea's logs](administration/logging-config.md) for error messages in case of trouble.
 
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index c5c9cecdb9..4e1bd9e87e 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -393,7 +393,7 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
 
 func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session {
 	// Query for pull requests where you are a reviewer or commenter, excluding
-	// any pull requests already returned by the the review requested filter.
+	// any pull requests already returned by the review requested filter.
 	notPoster := builder.Neq{"issue.poster_id": reviewedID}
 	reviewed := builder.In("issue.id", builder.
 		Select("issue_id").
diff --git a/models/user/user.go b/models/user/user.go
index 0bdda8655f..22a3099643 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -425,7 +425,7 @@ func (u *User) GetDisplayName() string {
 	return u.Name
 }
 
-// GetCompleteName returns the the full name and username in the form of
+// GetCompleteName returns the full name and username in the form of
 // "Full Name (username)" if full name is not empty, otherwise it returns
 // "username".
 func (u *User) GetCompleteName() string {
diff --git a/modules/git/commit.go b/modules/git/commit.go
index 5d960e92f3..facb632bd9 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -311,7 +311,7 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error)
 	return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String())
 }
 
-// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
+// FileChangedSinceCommit Returns true if the file given has changed since the past commit
 // YOU MUST ENSURE THAT pastCommit is a valid commit ID.
 func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
 	return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index b63e7ab662..09175fe0ac 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -368,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
 	//   required: false
 	// - name: created_by
 	//   in: query
-	//   description: Only show items which were created by the the given user
+	//   description: Only show items which were created by the given user
 	//   type: string
 	// - name: assigned_by
 	//   in: query
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 5128102e61..d314c4e7f7 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -545,7 +545,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues
 		return nil, nil, true
 	}
 
-	// validate the the review is for the given PR
+	// validate the review is for the given PR
 	if review.IssueID != pr.IssueID {
 		ctx.NotFound("ReviewNotInPR")
 		return nil, nil, true
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 98198696bc..221b34b7f8 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -6428,7 +6428,7 @@
           },
           {
             "type": "string",
-            "description": "Only show items which were created by the the given user",
+            "description": "Only show items which were created by the given user",
             "name": "created_by",
             "in": "query"
           },

From e84e5db6de0306d514b1f1a9657931fb7197a188 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 12 Mar 2024 12:21:27 +0800
Subject: [PATCH 340/679] Lazy load object format with command line and don't
 do it in OpenRepository (#29712)

Most time, when invoking `git.OpenRepository`, `objectFormat` will not
be used, so it's a waste to invoke commandline to get the object format.
This PR make it a lazy operation, only invoke that when necessary.
---
 modules/git/blame_sha256_test.go   |  5 +++--
 modules/git/blame_test.go          |  4 +++-
 modules/git/commit_sha256_test.go  |  7 +++++--
 modules/git/repo_base_nogogit.go   |  5 -----
 modules/git/repo_commit.go         |  7 ++++++-
 modules/git/repo_commit_gogit.go   |  5 ++++-
 modules/git/repo_commit_nogogit.go |  9 ++++++---
 modules/git/repo_compare.go        |  6 +++++-
 modules/git/repo_compare_test.go   |  9 ++++++---
 modules/git/repo_index.go          |  6 +++++-
 modules/git/repo_tag.go            |  4 ++--
 modules/git/repo_tag_test.go       |  3 +--
 modules/git/repo_tree_gogit.go     |  7 ++++++-
 modules/git/repo_tree_nogogit.go   | 12 ++++++++++--
 modules/git/tree_nogogit.go        | 14 ++++++++++----
 15 files changed, 72 insertions(+), 31 deletions(-)

diff --git a/modules/git/blame_sha256_test.go b/modules/git/blame_sha256_test.go
index 01de0454a3..8cd345714f 100644
--- a/modules/git/blame_sha256_test.go
+++ b/modules/git/blame_sha256_test.go
@@ -118,11 +118,12 @@ func TestReadingBlameOutputSha256(t *testing.T) {
 			},
 		}
 
+		objectFormat, err := repo.GetObjectFormat()
+		assert.NoError(t, err)
 		for _, c := range cases {
 			commit, err := repo.GetCommit(c.CommitID)
 			assert.NoError(t, err)
-
-			blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
+			blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
 			assert.NoError(t, err)
 			assert.NotNil(t, blameReader)
 			defer blameReader.Close()
diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go
index 327edab767..4220c85600 100644
--- a/modules/git/blame_test.go
+++ b/modules/git/blame_test.go
@@ -118,11 +118,13 @@ func TestReadingBlameOutput(t *testing.T) {
 			},
 		}
 
+		objectFormat, err := repo.GetObjectFormat()
+		assert.NoError(t, err)
 		for _, c := range cases {
 			commit, err := repo.GetCommit(c.CommitID)
 			assert.NoError(t, err)
 
-			blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
+			blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
 			assert.NoError(t, err)
 			assert.NotNil(t, blameReader)
 			defer blameReader.Close()
diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go
index 82112cb409..3b8b6d3763 100644
--- a/modules/git/commit_sha256_test.go
+++ b/modules/git/commit_sha256_test.go
@@ -140,10 +140,13 @@ func TestHasPreviousCommitSha256(t *testing.T) {
 	commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc")
 	assert.NoError(t, err)
 
+	objectFormat, err := repo.GetObjectFormat()
+	assert.NoError(t, err)
+
 	parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c")
 	notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236")
-	assert.Equal(t, repo.objectFormat, parentSHA.Type())
-	assert.Equal(t, repo.objectFormat.Name(), "sha256")
+	assert.Equal(t, objectFormat, parentSHA.Type())
+	assert.Equal(t, objectFormat.Name(), "sha256")
 
 	haz, err := commit.HasPreviousCommit(parentSHA)
 	assert.NoError(t, err)
diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
index 7f6512200b..5511526e78 100644
--- a/modules/git/repo_base_nogogit.go
+++ b/modules/git/repo_base_nogogit.go
@@ -71,11 +71,6 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 	repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath)
 	repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath)
 
-	repo.objectFormat, err = repo.GetObjectFormat()
-	if err != nil {
-		return nil, err
-	}
-
 	return repo, nil
 }
 
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 9c9ee7768f..44273d2253 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -246,7 +246,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
 		}
 	}()
 
-	len := repo.objectFormat.FullLength()
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+
+	len := objectFormat.FullLength()
 	commits := []*Commit{}
 	shaline := make([]byte, len+1)
 	for {
diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go
index 4cab957564..84580be9a5 100644
--- a/modules/git/repo_commit_gogit.go
+++ b/modules/git/repo_commit_gogit.go
@@ -41,7 +41,10 @@ func (repo *Repository) RemoveReference(name string) error {
 
 // ConvertToHash returns a Hash object from a potential ID string
 func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
-	objectFormat := repo.objectFormat
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
 	if len(commitID) == hash.HexSize && objectFormat.IsValid(commitID) {
 		ID, err := NewIDFromString(commitID)
 		if err == nil {
diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go
index a7031184e2..ae4c21aaa3 100644
--- a/modules/git/repo_commit_nogogit.go
+++ b/modules/git/repo_commit_nogogit.go
@@ -132,8 +132,11 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID)
 
 // ConvertToGitID returns a GitHash object from a potential ID string
 func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
-	IDType := repo.objectFormat
-	if len(commitID) == IDType.FullLength() && IDType.IsValid(commitID) {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
 		ID, err := NewIDFromString(commitID)
 		if err == nil {
 			return ID, nil
@@ -142,7 +145,7 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
 
 	wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
 	defer cancel()
-	_, err := wr.Write([]byte(commitID + "\n"))
+	_, err = wr.Write([]byte(commitID + "\n"))
 	if err != nil {
 		return nil, err
 	}
diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go
index 0e9a0c70d7..b6e9d2b44a 100644
--- a/modules/git/repo_compare.go
+++ b/modules/git/repo_compare.go
@@ -283,8 +283,12 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
 // If base is undefined empty SHA (zeros), it only returns the files changed in the head commit
 // If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit
 func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
 	cmd := NewCommand(repo.Ctx, "diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z")
-	if base == repo.objectFormat.EmptyObjectID().String() {
+	if base == objectFormat.EmptyObjectID().String() {
 		cmd.AddDynamicArguments(head)
 	} else {
 		cmd.AddDynamicArguments(base, head)
diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go
index 526b213550..9983873186 100644
--- a/modules/git/repo_compare_test.go
+++ b/modules/git/repo_compare_test.go
@@ -126,17 +126,20 @@ func TestGetCommitFilesChanged(t *testing.T) {
 	assert.NoError(t, err)
 	defer repo.Close()
 
+	objectFormat, err := repo.GetObjectFormat()
+	assert.NoError(t, err)
+
 	testCases := []struct {
 		base, head string
 		files      []string
 	}{
 		{
-			repo.objectFormat.EmptyObjectID().String(),
+			objectFormat.EmptyObjectID().String(),
 			"95bb4d39648ee7e325106df01a621c530863a653",
 			[]string{"file1.txt"},
 		},
 		{
-			repo.objectFormat.EmptyObjectID().String(),
+			objectFormat.EmptyObjectID().String(),
 			"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
 			[]string{"file2.txt"},
 		},
@@ -146,7 +149,7 @@ func TestGetCommitFilesChanged(t *testing.T) {
 			[]string{"file2.txt"},
 		},
 		{
-			repo.objectFormat.EmptyTree().String(),
+			objectFormat.EmptyTree().String(),
 			"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
 			[]string{"file1.txt", "file2.txt"},
 		},
diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go
index 47705a92af..6aaab242c1 100644
--- a/modules/git/repo_index.go
+++ b/modules/git/repo_index.go
@@ -94,6 +94,10 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) {
 
 // RemoveFilesFromIndex removes given filenames from the index - it does not check whether they are present.
 func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return err
+	}
 	cmd := NewCommand(repo.Ctx, "update-index", "--remove", "-z", "--index-info")
 	stdout := new(bytes.Buffer)
 	stderr := new(bytes.Buffer)
@@ -101,7 +105,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
 	for _, file := range filenames {
 		if file != "" {
 			buffer.WriteString("0 ")
-			buffer.WriteString(repo.objectFormat.EmptyObjectID().String())
+			buffer.WriteString(objectFormat.EmptyObjectID().String())
 			buffer.WriteByte('\t')
 			buffer.WriteString(file)
 			buffer.WriteByte('\000')
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index ae5dbd171f..e8c5ce6fb8 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -141,7 +141,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
 			break
 		}
 
-		tag, err := parseTagRef(repo.objectFormat, ref)
+		tag, err := parseTagRef(ref)
 		if err != nil {
 			return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err)
 		}
@@ -161,7 +161,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
 }
 
 // parseTagRef parses a tag from a 'git for-each-ref'-produced reference.
-func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, err error) {
+func parseTagRef(ref map[string]string) (tag *Tag, err error) {
 	tag = &Tag{
 		Type: ref["objecttype"],
 		Name: ref["refname:lstrip=2"],
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
index 9816e311a8..785c3442a7 100644
--- a/modules/git/repo_tag_test.go
+++ b/modules/git/repo_tag_test.go
@@ -194,7 +194,6 @@ func TestRepository_GetAnnotatedTag(t *testing.T) {
 }
 
 func TestRepository_parseTagRef(t *testing.T) {
-	sha1 := Sha1ObjectFormat
 	tests := []struct {
 		name string
 
@@ -351,7 +350,7 @@ Add changelog of v1.9.1 (#7859)
 	for _, test := range tests {
 		tc := test // don't close over loop variable
 		t.Run(tc.name, func(t *testing.T) {
-			got, err := parseTagRef(sha1, tc.givenRef)
+			got, err := parseTagRef(tc.givenRef)
 
 			if tc.wantErr {
 				require.Error(t, err)
diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go
index 6391959e6a..dc97ce1344 100644
--- a/modules/git/repo_tree_gogit.go
+++ b/modules/git/repo_tree_gogit.go
@@ -21,7 +21,12 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
 
 // GetTree find the tree object in the repository.
 func (repo *Repository) GetTree(idStr string) (*Tree, error) {
-	if len(idStr) != repo.objectFormat.FullLength() {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(idStr) != objectFormat.FullLength() {
 		res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path})
 		if err != nil {
 			return nil, err
diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go
index 582247b4a4..e82012de6f 100644
--- a/modules/git/repo_tree_nogogit.go
+++ b/modules/git/repo_tree_nogogit.go
@@ -51,7 +51,11 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
 	case "tree":
 		tree := NewTree(repo, id)
 		tree.ResolvedID = id
-		tree.entries, err = catBatchParseTreeEntries(repo.objectFormat, tree, rd, size)
+		objectFormat, err := repo.GetObjectFormat()
+		if err != nil {
+			return nil, err
+		}
+		tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, size)
 		if err != nil {
 			return nil, err
 		}
@@ -69,7 +73,11 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
 
 // GetTree find the tree object in the repository.
 func (repo *Repository) GetTree(idStr string) (*Tree, error) {
-	if len(idStr) != repo.objectFormat.FullLength() {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	if len(idStr) != objectFormat.FullLength() {
 		res, err := repo.GetRefCommitID(idStr)
 		if err != nil {
 			return nil, err
diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go
index 28d02c7e81..a591485082 100644
--- a/modules/git/tree_nogogit.go
+++ b/modules/git/tree_nogogit.go
@@ -77,8 +77,11 @@ func (t *Tree) ListEntries() (Entries, error) {
 		return nil, runErr
 	}
 
-	var err error
-	t.entries, err = parseTreeEntries(t.repo.objectFormat, stdout, t)
+	objectFormat, err := t.repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	t.entries, err = parseTreeEntries(objectFormat, stdout, t)
 	if err == nil {
 		t.entriesParsed = true
 	}
@@ -101,8 +104,11 @@ func (t *Tree) listEntriesRecursive(extraArgs TrustedCmdArgs) (Entries, error) {
 		return nil, runErr
 	}
 
-	var err error
-	t.entriesRecursive, err = parseTreeEntries(t.repo.objectFormat, stdout, t)
+	objectFormat, err := t.repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	t.entriesRecursive, err = parseTreeEntries(objectFormat, stdout, t)
 	if err == nil {
 		t.entriesRecursiveParsed = true
 	}

From 75a9f61f89caada64f6398130844281e4f088a73 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 12 Mar 2024 06:29:51 +0200
Subject: [PATCH 341/679] Remove jQuery AJAX from the issue branch reference
 selection (#29722)

- Replaced a single jQuery AJAX instance with our fetch wrapper
- Tested the issue branch reference selection and it works as before

# Demo using `fetch` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/7e195632-41f8-494b-b599-f6291860f330)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-legacy.js | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 8fcc78c177..60950fd171 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -24,6 +24,7 @@ import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
 import {hideElem, showElem} from '../utils/dom.js';
 import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {attachRefIssueContextPopup} from './contextpopup.js';
+import {POST} from '../modules/fetch.js';
 
 const {csrfToken} = window.config;
 
@@ -65,7 +66,7 @@ export function initRepoCommentForm() {
     const $selectBranch = $('.ui.select-branch');
     const $branchMenu = $selectBranch.find('.reference-list-menu');
     const $isNewIssue = $branchMenu.hasClass('new-issue');
-    $branchMenu.find('.item:not(.no-select)').on('click', function () {
+    $branchMenu.find('.item:not(.no-select)').on('click', async function () {
       const selectedValue = $(this).data('id');
       const editMode = $('#editing_mode').val();
       $($(this).data('id-selector')).val(selectedValue);
@@ -76,7 +77,14 @@ export function initRepoCommentForm() {
 
       if (editMode === 'true') {
         const form = $('#update_issueref_form');
-        $.post(form.attr('action'), {_csrf: csrfToken, ref: selectedValue}, () => window.location.reload());
+        const params = new URLSearchParams();
+        params.append('ref', selectedValue);
+        try {
+          await POST(form.attr('action'), {data: params});
+          window.location.reload();
+        } catch (error) {
+          console.error('Error:', error);
+        }
       } else if (editMode === '') {
         $selectBranch.find('.ui .branch-name').text(selectedValue);
       }

From aed3b53abdd02a3ffbf9e8eb90272ff567333073 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 12 Mar 2024 12:57:19 +0800
Subject: [PATCH 342/679] Some performance optimization on dashboard and issues
 page (#29010)

This PR do some loading speed optimization for feeds user interface
pages.
- Load action users batchly but not one by one.
- Load action repositories batchly but not one by one.
- Load action's Repo Owners batchly but not one by one.
- Load action's possible issues batchly but not one by one.
- Load action's possible comments batchly but not one by one.
---
 models/activities/action.go      | 116 +++++++++++++-------------
 models/activities/action_list.go | 136 ++++++++++++++++++++++++++-----
 models/issues/issue_list.go      |  10 +++
 models/repo/repo.go              |   3 +
 models/repo/repo_list.go         |  35 ++++++++
 modules/util/slice.go            |   9 ++
 routers/web/repo/repo.go         |   6 +-
 7 files changed, 234 insertions(+), 81 deletions(-)

diff --git a/models/activities/action.go b/models/activities/action.go
index 36205eedd1..7e2ef4c9ae 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -148,6 +148,7 @@ type Action struct {
 	Repo        *repo_model.Repository `xorm:"-"`
 	CommentID   int64                  `xorm:"INDEX"`
 	Comment     *issues_model.Comment  `xorm:"-"`
+	Issue       *issues_model.Issue    `xorm:"-"` // get the issue id from content
 	IsDeleted   bool                   `xorm:"NOT NULL DEFAULT false"`
 	RefName     string
 	IsPrivate   bool               `xorm:"NOT NULL DEFAULT false"`
@@ -290,11 +291,6 @@ func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string {
 	return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx))
 }
 
-// GetCommentHTMLURL returns link to action comment.
-func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
-	return a.getCommentHTMLURL(ctx)
-}
-
 func (a *Action) loadComment(ctx context.Context) (err error) {
 	if a.CommentID == 0 || a.Comment != nil {
 		return nil
@@ -303,7 +299,8 @@ func (a *Action) loadComment(ctx context.Context) (err error) {
 	return err
 }
 
-func (a *Action) getCommentHTMLURL(ctx context.Context) string {
+// GetCommentHTMLURL returns link to action comment.
+func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
 	if a == nil {
 		return "#"
 	}
@@ -311,34 +308,19 @@ func (a *Action) getCommentHTMLURL(ctx context.Context) string {
 	if a.Comment != nil {
 		return a.Comment.HTMLURL(ctx)
 	}
-	if len(a.GetIssueInfos()) == 0 {
+
+	if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
 		return "#"
 	}
-	// Return link to issue
-	issueIDString := a.GetIssueInfos()[0]
-	issueID, err := strconv.ParseInt(issueIDString, 10, 64)
-	if err != nil {
+	if err := a.Issue.LoadRepo(ctx); err != nil {
 		return "#"
 	}
 
-	issue, err := issues_model.GetIssueByID(ctx, issueID)
-	if err != nil {
-		return "#"
-	}
-
-	if err = issue.LoadRepo(ctx); err != nil {
-		return "#"
-	}
-
-	return issue.HTMLURL()
+	return a.Issue.HTMLURL()
 }
 
 // GetCommentLink returns link to action comment.
 func (a *Action) GetCommentLink(ctx context.Context) string {
-	return a.getCommentLink(ctx)
-}
-
-func (a *Action) getCommentLink(ctx context.Context) string {
 	if a == nil {
 		return "#"
 	}
@@ -346,26 +328,15 @@ func (a *Action) getCommentLink(ctx context.Context) string {
 	if a.Comment != nil {
 		return a.Comment.Link(ctx)
 	}
-	if len(a.GetIssueInfos()) == 0 {
+
+	if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
 		return "#"
 	}
-	// Return link to issue
-	issueIDString := a.GetIssueInfos()[0]
-	issueID, err := strconv.ParseInt(issueIDString, 10, 64)
-	if err != nil {
+	if err := a.Issue.LoadRepo(ctx); err != nil {
 		return "#"
 	}
 
-	issue, err := issues_model.GetIssueByID(ctx, issueID)
-	if err != nil {
-		return "#"
-	}
-
-	if err = issue.LoadRepo(ctx); err != nil {
-		return "#"
-	}
-
-	return issue.Link()
+	return a.Issue.Link()
 }
 
 // GetBranch returns the action's repository branch.
@@ -393,6 +364,10 @@ func (a *Action) GetCreate() time.Time {
 	return a.CreatedUnix.AsTime()
 }
 
+func (a *Action) IsIssueEvent() bool {
+	return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request")
+}
+
 // GetIssueInfos returns a list of associated information with the action.
 func (a *Action) GetIssueInfos() []string {
 	// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
@@ -403,27 +378,52 @@ func (a *Action) GetIssueInfos() []string {
 	return ret
 }
 
-// GetIssueTitle returns the title of first issue associated with the action.
-func (a *Action) GetIssueTitle(ctx context.Context) string {
-	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
-	issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
-	if err != nil {
-		log.Error("GetIssueByIndex: %v", err)
-		return "500 when get issue"
+func (a *Action) getIssueIndex() int64 {
+	infos := a.GetIssueInfos()
+	if len(infos) == 0 {
+		return 0
 	}
-	return issue.Title
+	index, _ := strconv.ParseInt(infos[0], 10, 64)
+	return index
 }
 
-// GetIssueContent returns the content of first issue associated with
-// this action.
-func (a *Action) GetIssueContent(ctx context.Context) string {
-	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
-	issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
-	if err != nil {
-		log.Error("GetIssueByIndex: %v", err)
-		return "500 when get issue"
+func (a *Action) LoadIssue(ctx context.Context) error {
+	if a.Issue != nil {
+		return nil
 	}
-	return issue.Content
+	if index := a.getIssueIndex(); index > 0 {
+		issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
+		if err != nil {
+			return err
+		}
+		a.Issue = issue
+		a.Issue.Repo = a.Repo
+	}
+	return nil
+}
+
+// GetIssueTitle returns the title of first issue associated with the action.
+func (a *Action) GetIssueTitle(ctx context.Context) string {
+	if err := a.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return "<500 when get issue>"
+	}
+	if a.Issue == nil {
+		return "<Issue not found>"
+	}
+	return a.Issue.Title
+}
+
+// GetIssueContent returns the content of first issue associated with this action.
+func (a *Action) GetIssueContent(ctx context.Context) string {
+	if err := a.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return "<500 when get issue>"
+	}
+	if a.Issue == nil {
+		return "<Content not found>"
+	}
+	return a.Issue.Content
 }
 
 // GetFeedsOptions options for retrieving feeds
@@ -463,7 +463,7 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
 		return nil, 0, fmt.Errorf("FindAndCount: %w", err)
 	}
 
-	if err := ActionList(actions).loadAttributes(ctx); err != nil {
+	if err := ActionList(actions).LoadAttributes(ctx); err != nil {
 		return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
 	}
 
diff --git a/models/activities/action_list.go b/models/activities/action_list.go
index 3d74397c69..fdf0f35d4f 100644
--- a/models/activities/action_list.go
+++ b/models/activities/action_list.go
@@ -6,11 +6,16 @@ package activities
 import (
 	"context"
 	"fmt"
+	"strconv"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/util"
+
+	"xorm.io/builder"
 )
 
 // ActionList defines a list of actions
@@ -24,7 +29,7 @@ func (actions ActionList) getUserIDs() []int64 {
 	return userIDs.Values()
 }
 
-func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model.User, error) {
+func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) {
 	if len(actions) == 0 {
 		return nil, nil
 	}
@@ -52,7 +57,7 @@ func (actions ActionList) getRepoIDs() []int64 {
 	return repoIDs.Values()
 }
 
-func (actions ActionList) loadRepositories(ctx context.Context) error {
+func (actions ActionList) LoadRepositories(ctx context.Context) error {
 	if len(actions) == 0 {
 		return nil
 	}
@@ -63,11 +68,11 @@ func (actions ActionList) loadRepositories(ctx context.Context) error {
 	if err != nil {
 		return fmt.Errorf("find repository: %w", err)
 	}
-
 	for _, action := range actions {
 		action.Repo = repoMaps[action.RepoID]
 	}
-	return nil
+	repos := repo_model.RepositoryList(util.ValuesOfMap(repoMaps))
+	return repos.LoadUnits(ctx)
 }
 
 func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) {
@@ -75,37 +80,124 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*
 		userMap = make(map[int64]*user_model.User)
 	}
 
+	userSet := make(container.Set[int64], len(actions))
 	for _, action := range actions {
 		if action.Repo == nil {
 			continue
 		}
-		repoOwner, ok := userMap[action.Repo.OwnerID]
-		if !ok {
-			repoOwner, err = user_model.GetUserByID(ctx, action.Repo.OwnerID)
-			if err != nil {
-				if user_model.IsErrUserNotExist(err) {
-					continue
-				}
-				return err
-			}
-			userMap[repoOwner.ID] = repoOwner
+		if _, ok := userMap[action.Repo.OwnerID]; !ok {
+			userSet.Add(action.Repo.OwnerID)
+		}
+	}
+
+	if err := db.GetEngine(ctx).
+		In("id", userSet.Values()).
+		Find(&userMap); err != nil {
+		return fmt.Errorf("find user: %w", err)
+	}
+
+	for _, action := range actions {
+		if action.Repo != nil {
+			action.Repo.Owner = userMap[action.Repo.OwnerID]
 		}
-		action.Repo.Owner = repoOwner
 	}
 
 	return nil
 }
 
-// loadAttributes loads all attributes
-func (actions ActionList) loadAttributes(ctx context.Context) error {
-	userMap, err := actions.loadUsers(ctx)
+// LoadAttributes loads all attributes
+func (actions ActionList) LoadAttributes(ctx context.Context) error {
+	// the load sequence cannot be changed because of the dependencies
+	userMap, err := actions.LoadActUsers(ctx)
 	if err != nil {
 		return err
 	}
-
-	if err := actions.loadRepositories(ctx); err != nil {
+	if err := actions.LoadRepositories(ctx); err != nil {
 		return err
 	}
-
-	return actions.loadRepoOwner(ctx, userMap)
+	if err := actions.loadRepoOwner(ctx, userMap); err != nil {
+		return err
+	}
+	if err := actions.LoadIssues(ctx); err != nil {
+		return err
+	}
+	return actions.LoadComments(ctx)
+}
+
+func (actions ActionList) LoadComments(ctx context.Context) error {
+	if len(actions) == 0 {
+		return nil
+	}
+
+	commentIDs := make([]int64, 0, len(actions))
+	for _, action := range actions {
+		if action.CommentID > 0 {
+			commentIDs = append(commentIDs, action.CommentID)
+		}
+	}
+
+	commentsMap := make(map[int64]*issues_model.Comment, len(commentIDs))
+	if err := db.GetEngine(ctx).In("id", commentIDs).Find(&commentsMap); err != nil {
+		return fmt.Errorf("find comment: %w", err)
+	}
+
+	for _, action := range actions {
+		if action.CommentID > 0 {
+			action.Comment = commentsMap[action.CommentID]
+			if action.Comment != nil {
+				action.Comment.Issue = action.Issue
+			}
+		}
+	}
+	return nil
+}
+
+func (actions ActionList) LoadIssues(ctx context.Context) error {
+	if len(actions) == 0 {
+		return nil
+	}
+
+	conditions := builder.NewCond()
+	issueNum := 0
+	for _, action := range actions {
+		if action.IsIssueEvent() {
+			infos := action.GetIssueInfos()
+			if len(infos) == 0 {
+				continue
+			}
+			index, _ := strconv.ParseInt(infos[0], 10, 64)
+			if index > 0 {
+				conditions = conditions.Or(builder.Eq{
+					"repo_id": action.RepoID,
+					"`index`": index,
+				})
+				issueNum++
+			}
+		}
+	}
+	if !conditions.IsValid() {
+		return nil
+	}
+
+	issuesMap := make(map[string]*issues_model.Issue, issueNum)
+	issues := make([]*issues_model.Issue, 0, issueNum)
+	if err := db.GetEngine(ctx).Where(conditions).Find(&issues); err != nil {
+		return fmt.Errorf("find issue: %w", err)
+	}
+	for _, issue := range issues {
+		issuesMap[fmt.Sprintf("%d-%d", issue.RepoID, issue.Index)] = issue
+	}
+
+	for _, action := range actions {
+		if !action.IsIssueEvent() {
+			continue
+		}
+		if index := action.getIssueIndex(); index > 0 {
+			if issue, ok := issuesMap[fmt.Sprintf("%d-%d", action.RepoID, index)]; ok {
+				action.Issue = issue
+				action.Issue.Repo = action.Repo
+			}
+		}
+	}
+	return nil
 }
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index a932ac2554..0fb8447ff7 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -476,6 +476,16 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
 	}
 	trackedTimes := make(map[int64]int64, len(issues))
 
+	reposMap := make(map[int64]*repo_model.Repository, len(issues))
+	for _, issue := range issues {
+		reposMap[issue.RepoID] = issue.Repo
+	}
+	repos := repo_model.RepositoryListOfMap(reposMap)
+
+	if err := repos.LoadUnits(ctx); err != nil {
+		return err
+	}
+
 	ids := make([]int64, 0, len(issues))
 	for _, issue := range issues {
 		if issue.Repo.IsTimetrackerEnabled(ctx) {
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 1d17e565ae..5d5707d1ac 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -531,6 +531,9 @@ func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) {
 		return nil
 	}
 
+	if repo.BaseRepo != nil {
+		return nil
+	}
 	repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID)
 	return err
 }
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index 6b452291ea..cb7cd47a8d 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -63,6 +63,41 @@ func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
 	return RepositoryList(ValuesRepository(repoMap))
 }
 
+func (repos RepositoryList) LoadUnits(ctx context.Context) error {
+	if len(repos) == 0 {
+		return nil
+	}
+
+	// Load units.
+	units := make([]*RepoUnit, 0, len(repos)*6)
+	if err := db.GetEngine(ctx).
+		In("repo_id", repos.IDs()).
+		Find(&units); err != nil {
+		return fmt.Errorf("find units: %w", err)
+	}
+
+	unitsMap := make(map[int64][]*RepoUnit, len(repos))
+	for _, unit := range units {
+		if !unit.Type.UnitGlobalDisabled() {
+			unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit)
+		}
+	}
+
+	for _, repo := range repos {
+		repo.Units = unitsMap[repo.ID]
+	}
+
+	return nil
+}
+
+func (repos RepositoryList) IDs() []int64 {
+	repoIDs := make([]int64, len(repos))
+	for i := range repos {
+		repoIDs[i] = repos[i].ID
+	}
+	return repoIDs
+}
+
 // LoadAttributes loads the attributes for the given RepositoryList
 func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
 	if len(repos) == 0 {
diff --git a/modules/util/slice.go b/modules/util/slice.go
index a7073fedee..f00e84bf06 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -53,3 +53,12 @@ func Sorted[S ~[]E, E cmp.Ordered](values S) S {
 	slices.Sort(values)
 	return values
 }
+
+// TODO: Replace with "maps.Values" once available
+func ValuesOfMap[K comparable, V any](m map[K]V) []V {
+	values := make([]V, 0, len(m))
+	for _, v := range m {
+		values = append(values, v)
+	}
+	return values
+}
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index b54d29c580..7a626a7065 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -547,9 +547,13 @@ func InitiateDownload(ctx *context.Context) {
 
 // SearchRepo repositories via options
 func SearchRepo(ctx *context.Context) {
+	page := ctx.FormInt("page")
+	if page <= 0 {
+		page = 1
+	}
 	opts := &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
-			Page:     ctx.FormInt("page"),
+			Page:     page,
 			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 		},
 		Actor:              ctx.Doer,

From d8bd6f34f09bc9a6602bebb33bdc9e1f255a0d7c Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 12 Mar 2024 15:23:44 +0800
Subject: [PATCH 343/679] Do some performance optimize for issues list and view
 issue/pull (#29515)

This PR do some performance optimzations.

- [x] Add `index` for the column `comment_id` of `Attachment` table to
accelerate query from the database.
- [x] Remove unnecessary database queries when viewing issues. Before
some conditions which id = 0 will be sent to the database
- [x] Remove duplicated load posters
- [x] Batch loading attachements, isread of comments on viewing issue

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
---
 models/issues/comment.go             |  8 +--
 models/issues/comment_code.go        |  2 +-
 models/issues/comment_list.go        | 78 +++++++++++++++++++++-------
 models/issues/issue_list.go          | 25 +++++++--
 models/migrations/migrations.go      |  2 +
 models/migrations/v1_22/v291.go      | 14 +++++
 models/repo/attachment.go            |  2 +-
 routers/api/v1/repo/issue_comment.go |  4 --
 routers/web/repo/issue.go            | 39 ++++++--------
 routers/web/repo/pull_review.go      |  8 ++-
 10 files changed, 121 insertions(+), 61 deletions(-)
 create mode 100644 models/migrations/v1_22/v291.go

diff --git a/models/issues/comment.go b/models/issues/comment.go
index e37f844b5c..6f65a5dbbc 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -673,7 +673,8 @@ func (c *Comment) LoadTime(ctx context.Context) error {
 	return err
 }
 
-func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) {
+// LoadReactions loads comment reactions
+func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) (err error) {
 	if c.Reactions != nil {
 		return nil
 	}
@@ -691,11 +692,6 @@ func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository
 	return nil
 }
 
-// LoadReactions loads comment reactions
-func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) error {
-	return c.loadReactions(ctx, repo)
-}
-
 func (c *Comment) loadReview(ctx context.Context) (err error) {
 	if c.ReviewID == 0 {
 		return nil
diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index 384a595dd9..74a7a86f26 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -122,7 +122,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 }
 
 // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
-func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) {
+func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) (CommentList, error) {
 	opts := FindCommentsOptions{
 		Type:     CommentTypeCode,
 		IssueID:  issue.ID,
diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index 30a437ea50..0047b054ba 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -19,7 +19,9 @@ type CommentList []*Comment
 func (comments CommentList) getPosterIDs() []int64 {
 	posterIDs := make(container.Set[int64], len(comments))
 	for _, comment := range comments {
-		posterIDs.Add(comment.PosterID)
+		if comment.PosterID > 0 {
+			posterIDs.Add(comment.PosterID)
+		}
 	}
 	return posterIDs.Values()
 }
@@ -41,18 +43,12 @@ func (comments CommentList) LoadPosters(ctx context.Context) error {
 	return nil
 }
 
-func (comments CommentList) getCommentIDs() []int64 {
-	ids := make([]int64, 0, len(comments))
-	for _, comment := range comments {
-		ids = append(ids, comment.ID)
-	}
-	return ids
-}
-
 func (comments CommentList) getLabelIDs() []int64 {
 	ids := make(container.Set[int64], len(comments))
 	for _, comment := range comments {
-		ids.Add(comment.LabelID)
+		if comment.LabelID > 0 {
+			ids.Add(comment.LabelID)
+		}
 	}
 	return ids.Values()
 }
@@ -100,7 +96,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error {
 func (comments CommentList) getMilestoneIDs() []int64 {
 	ids := make(container.Set[int64], len(comments))
 	for _, comment := range comments {
-		ids.Add(comment.MilestoneID)
+		if comment.MilestoneID > 0 {
+			ids.Add(comment.MilestoneID)
+		}
 	}
 	return ids.Values()
 }
@@ -141,7 +139,9 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
 func (comments CommentList) getOldMilestoneIDs() []int64 {
 	ids := make(container.Set[int64], len(comments))
 	for _, comment := range comments {
-		ids.Add(comment.OldMilestoneID)
+		if comment.OldMilestoneID > 0 {
+			ids.Add(comment.OldMilestoneID)
+		}
 	}
 	return ids.Values()
 }
@@ -182,7 +182,9 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
 func (comments CommentList) getAssigneeIDs() []int64 {
 	ids := make(container.Set[int64], len(comments))
 	for _, comment := range comments {
-		ids.Add(comment.AssigneeID)
+		if comment.AssigneeID > 0 {
+			ids.Add(comment.AssigneeID)
+		}
 	}
 	return ids.Values()
 }
@@ -314,7 +316,9 @@ func (comments CommentList) getDependentIssueIDs() []int64 {
 		if comment.DependentIssue != nil {
 			continue
 		}
-		ids.Add(comment.DependentIssueID)
+		if comment.DependentIssueID > 0 {
+			ids.Add(comment.DependentIssueID)
+		}
 	}
 	return ids.Values()
 }
@@ -369,6 +373,41 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error {
 	return nil
 }
 
+// getAttachmentCommentIDs only return the comment ids which possibly has attachments
+func (comments CommentList) getAttachmentCommentIDs() []int64 {
+	ids := make(container.Set[int64], len(comments))
+	for _, comment := range comments {
+		if comment.Type == CommentTypeComment ||
+			comment.Type == CommentTypeReview ||
+			comment.Type == CommentTypeCode {
+			ids.Add(comment.ID)
+		}
+	}
+	return ids.Values()
+}
+
+// LoadAttachmentsByIssue loads attachments by issue id
+func (comments CommentList) LoadAttachmentsByIssue(ctx context.Context) error {
+	if len(comments) == 0 {
+		return nil
+	}
+
+	attachments := make([]*repo_model.Attachment, 0, len(comments)/2)
+	if err := db.GetEngine(ctx).Where("issue_id=? AND comment_id>0", comments[0].IssueID).Find(&attachments); err != nil {
+		return err
+	}
+
+	commentAttachmentsMap := make(map[int64][]*repo_model.Attachment, len(comments))
+	for _, attach := range attachments {
+		commentAttachmentsMap[attach.CommentID] = append(commentAttachmentsMap[attach.CommentID], attach)
+	}
+
+	for _, comment := range comments {
+		comment.Attachments = commentAttachmentsMap[comment.ID]
+	}
+	return nil
+}
+
 // LoadAttachments loads attachments
 func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
 	if len(comments) == 0 {
@@ -376,16 +415,15 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
 	}
 
 	attachments := make(map[int64][]*repo_model.Attachment, len(comments))
-	commentsIDs := comments.getCommentIDs()
+	commentsIDs := comments.getAttachmentCommentIDs()
 	left := len(commentsIDs)
 	for left > 0 {
 		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
-		rows, err := db.GetEngine(ctx).Table("attachment").
-			Join("INNER", "comment", "comment.id = attachment.comment_id").
-			In("comment.id", commentsIDs[:limit]).
+		rows, err := db.GetEngine(ctx).
+			In("comment_id", commentsIDs[:limit]).
 			Rows(new(repo_model.Attachment))
 		if err != nil {
 			return err
@@ -415,7 +453,9 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
 func (comments CommentList) getReviewIDs() []int64 {
 	ids := make(container.Set[int64], len(comments))
 	for _, comment := range comments {
-		ids.Add(comment.ReviewID)
+		if comment.ReviewID > 0 {
+			ids.Add(comment.ReviewID)
+		}
 	}
 	return ids.Values()
 }
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index 0fb8447ff7..41a90d133d 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -388,9 +388,8 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
 		if left < limit {
 			limit = left
 		}
-		rows, err := db.GetEngine(ctx).Table("attachment").
-			Join("INNER", "issue", "issue.id = attachment.issue_id").
-			In("issue.id", issuesIDs[:limit]).
+		rows, err := db.GetEngine(ctx).
+			In("issue_id", issuesIDs[:limit]).
 			Rows(new(repo_model.Attachment))
 		if err != nil {
 			return err
@@ -609,3 +608,23 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev
 
 	return approvalCountMap, nil
 }
+
+func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error {
+	issueIDs := issues.getIssueIDs()
+	issueUsers := make([]*IssueUser, 0, len(issueIDs))
+	if err := db.GetEngine(ctx).Where("uid =?", userID).
+		In("issue_id").
+		Find(&issueUsers); err != nil {
+		return err
+	}
+
+	for _, issueUser := range issueUsers {
+		for _, issue := range issues {
+			if issue.ID == issueUser.IssueID {
+				issue.IsRead = issueUser.IsRead
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index ce77432db4..87fddefb88 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -566,6 +566,8 @@ var migrations = []Migration{
 	NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch),
 	// v290 -> v291
 	NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
+	// v291 -> v292
+	NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v291.go b/models/migrations/v1_22/v291.go
new file mode 100644
index 0000000000..0bfffe5d05
--- /dev/null
+++ b/models/migrations/v1_22/v291.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import "xorm.io/xorm"
+
+func AddCommentIDIndexofAttachment(x *xorm.Engine) error {
+	type Attachment struct {
+		CommentID int64 `xorm:"INDEX"`
+	}
+
+	return x.Sync(&Attachment{})
+}
diff --git a/models/repo/attachment.go b/models/repo/attachment.go
index 1a588398c1..9b0de11fdc 100644
--- a/models/repo/attachment.go
+++ b/models/repo/attachment.go
@@ -24,7 +24,7 @@ type Attachment struct {
 	IssueID           int64  `xorm:"INDEX"`           // maybe zero when creating
 	ReleaseID         int64  `xorm:"INDEX"`           // maybe zero when creating
 	UploaderID        int64  `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
-	CommentID         int64
+	CommentID         int64  `xorm:"INDEX"`
 	Name              string
 	DownloadCount     int64              `xorm:"DEFAULT 0"`
 	Size              int64              `xorm:"DEFAULT 0"`
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 21aabadf3d..070571ba62 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -323,10 +323,6 @@ func ListRepoIssueComments(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
 		return
 	}
-	if err := comments.LoadPosters(ctx); err != nil {
-		ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
-		return
-	}
 	if err := comments.LoadAttachments(ctx); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
 		return
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 83a5b76bf1..8935cc80e2 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -324,15 +324,15 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
 		return
 	}
 
-	// Get posters.
-	for i := range issues {
-		// Check read status
-		if !ctx.IsSigned {
-			issues[i].IsRead = true
-		} else if err = issues[i].GetIsRead(ctx, ctx.Doer.ID); err != nil {
-			ctx.ServerError("GetIsRead", err)
+	if ctx.IsSigned {
+		if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil {
+			ctx.ServerError("LoadIsRead", err)
 			return
 		}
+	} else {
+		for i := range issues {
+			issues[i].IsRead = true
+		}
 	}
 
 	commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
@@ -1604,20 +1604,20 @@ func ViewIssue(ctx *context.Context) {
 
 	// Render comments and and fetch participants.
 	participants[0] = issue.Poster
+
+	if err := issue.Comments.LoadAttachmentsByIssue(ctx); err != nil {
+		ctx.ServerError("LoadAttachmentsByIssue", err)
+		return
+	}
+	if err := issue.Comments.LoadPosters(ctx); err != nil {
+		ctx.ServerError("LoadPosters", err)
+		return
+	}
+
 	for _, comment = range issue.Comments {
 		comment.Issue = issue
 
-		if err := comment.LoadPoster(ctx); err != nil {
-			ctx.ServerError("LoadPoster", err)
-			return
-		}
-
 		if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
-			if err := comment.LoadAttachments(ctx); err != nil {
-				ctx.ServerError("LoadAttachments", err)
-				return
-			}
-
 			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
 				Links: markup.Links{
 					Base: ctx.Repo.RepoLink,
@@ -1665,7 +1665,6 @@ func ViewIssue(ctx *context.Context) {
 				comment.Milestone = ghostMilestone
 			}
 		} else if comment.Type == issues_model.CommentTypeProject {
-
 			if err = comment.LoadProject(ctx); err != nil {
 				ctx.ServerError("LoadProject", err)
 				return
@@ -1731,10 +1730,6 @@ func ViewIssue(ctx *context.Context) {
 			for _, codeComments := range comment.Review.CodeComments {
 				for _, lineComments := range codeComments {
 					for _, c := range lineComments {
-						if err := c.LoadAttachments(ctx); err != nil {
-							ctx.ServerError("LoadAttachments", err)
-							return
-						}
 						// Check tag.
 						role, ok = marked[c.PosterID]
 						if ok {
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index bce807aacd..5385ebfc97 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -179,11 +179,9 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
 		return
 	}
 
-	for _, c := range comments {
-		if err := c.LoadAttachments(ctx); err != nil {
-			ctx.ServerError("LoadAttachments", err)
-			return
-		}
+	if err := comments.LoadAttachments(ctx); err != nil {
+		ctx.ServerError("LoadAttachments", err)
+		return
 	}
 
 	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled

From 171d3d9a3c891d107001094b9118d93b0b00c02c Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 12 Mar 2024 18:53:53 +0800
Subject: [PATCH 344/679] Use Get but not Post to get actions artifacts
 (#29734)

---
 routers/web/web.go                       | 2 +-
 web_src/js/components/RepoActionView.vue | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/routers/web/web.go b/routers/web/web.go
index 8710f6e3e5..fc1432873f 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1374,7 +1374,7 @@ func registerRoutes(m *web.Route) {
 				})
 				m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
 				m.Post("/approve", reqRepoActionsWriter, actions.Approve)
-				m.Post("/artifacts", actions.ArtifactsView)
+				m.Get("/artifacts", actions.ArtifactsView)
 				m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
 				m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
 				m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index de9625b143..9641431508 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -5,7 +5,7 @@ import {createApp} from 'vue';
 import {toggleElem} from '../utils/dom.js';
 import {formatDatetime} from '../utils/time.js';
 import {renderAnsi} from '../render/ansi.js';
-import {POST, DELETE} from '../modules/fetch.js';
+import {GET, POST, DELETE} from '../modules/fetch.js';
 
 const sfc = {
   name: 'RepoActionView',
@@ -196,7 +196,7 @@ const sfc = {
     },
 
     async fetchArtifacts() {
-      const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
+      const resp = await GET(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
       return await resp.json();
     },
 

From e5e2b2fcd7e8446f99e8eb61eef9efe2790220c8 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 12 Mar 2024 19:21:09 +0800
Subject: [PATCH 345/679] Add more stats tables (#29730)

Add `Tags`, `Branches` and `CommitStatus` to monitor/stats
---
 models/activities/statistic.go | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/models/activities/statistic.go b/models/activities/statistic.go
index fe5f7d0872..d1a459d1b2 100644
--- a/models/activities/statistic.go
+++ b/models/activities/statistic.go
@@ -9,6 +9,7 @@ import (
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -29,7 +30,8 @@ type Statistic struct {
 		Mirror, Release, AuthSource, Webhook,
 		Milestone, Label, HookTask,
 		Team, UpdateTask, Project,
-		ProjectBoard, Attachment int64
+		ProjectBoard, Attachment,
+		Branches, Tags, CommitStatus int64
 		IssueByLabel      []IssueByLabelCount
 		IssueByRepository []IssueByRepositoryCount
 	}
@@ -58,6 +60,9 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
 	stats.Counter.Watch, _ = e.Count(new(repo_model.Watch))
 	stats.Counter.Star, _ = e.Count(new(repo_model.Star))
 	stats.Counter.Access, _ = e.Count(new(access_model.Access))
+	stats.Counter.Branches, _ = e.Count(new(git_model.Branch))
+	stats.Counter.Tags, _ = e.Where("is_draft=?", false).Count(new(repo_model.Release))
+	stats.Counter.CommitStatus, _ = e.Count(new(git_model.CommitStatus))
 
 	type IssueCount struct {
 		Count    int64

From 36de5b299bb3e6e6cf28062c832ab8165e83f91a Mon Sep 17 00:00:00 2001
From: 6543 <m.huber@kithara.com>
Date: Tue, 12 Mar 2024 18:32:05 +0100
Subject: [PATCH 346/679] Highlight archived labels (#29680)

the issue is, that you can not distinguish between normal and archived
labels.

So this will make archived labels 80% **grayscale**. And prepend
"Archived: " to the tooltip info


![image](https://github.com/go-gitea/gitea/assets/24977596/fd77c4d2-eff5-4afd-9bfa-19cb9991c5e7)

![image](https://github.com/go-gitea/gitea/assets/24977596/2e0f30e5-f301-4c9c-8e9f-677298d90b27)

![image](https://github.com/go-gitea/gitea/assets/24977596/53d70abf-b306-453d-aa95-a3a035b19a33)

![image](https://github.com/go-gitea/gitea/assets/24977596/6020e5f5-2364-4807-979f-37dffa8735e5)


---
*Sponsored by Kithara Software GmbH*

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 modules/templates/util_render.go              | 29 +++++++++++++------
 templates/repo/issue/filter_actions.tmpl      |  2 +-
 templates/repo/issue/filter_list.tmpl         |  2 +-
 templates/repo/issue/labels/label.tmpl        |  2 +-
 templates/repo/issue/labels/label_list.tmpl   |  4 +--
 .../issue/labels/labels_selector_field.tmpl   |  4 +--
 .../repo/issue/view_content/comments.tmpl     |  6 ++--
 templates/shared/issuelist.tmpl               |  2 +-
 web_src/css/repo.css                          |  4 +++
 9 files changed, 35 insertions(+), 20 deletions(-)

diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index cdff31698c..ba9b050e02 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -118,10 +119,15 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
 }
 
 // RenderLabel renders a label
-func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
-	labelScope := label.ExclusiveScope()
+// locale is needed due to an import cycle with our context providing the `Tr` function
+func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
+	var (
+		archivedCSSClass string
+		textColor        = "#111"
+		isArchived       = !label.ArchivedUnix.IsZero()
+		labelScope       = label.ExclusiveScope()
+	)
 
-	textColor := "#111"
 	r, g, b := util.HexToRBGColor(label.Color)
 	// Determine if label text should be light or dark to be readable on background color
 	if util.UseLightTextOnBackground(r, g, b) {
@@ -130,10 +136,15 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
 
 	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
 
+	if isArchived {
+		archivedCSSClass = "archived-label"
+		description = fmt.Sprintf("(%s) %s", locale.TrString("archived"), description)
+	}
+
 	if labelScope == "" {
 		// Regular label
-		s := fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' data-tooltip-content title='%s'>%s</div>",
-			textColor, label.Color, description, RenderEmoji(ctx, label.Name))
+		s := fmt.Sprintf("<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>",
+			archivedCSSClass, textColor, label.Color, description, RenderEmoji(ctx, label.Name))
 		return template.HTML(s)
 	}
 
@@ -166,11 +177,11 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
 	itemColor := "#" + hex.EncodeToString(itemBytes)
 	scopeColor := "#" + hex.EncodeToString(scopeBytes)
 
-	s := fmt.Sprintf("<span class='ui label scope-parent' data-tooltip-content title='%s'>"+
+	s := fmt.Sprintf("<span class='ui label %s scope-parent' data-tooltip-content title='%s'>"+
 		"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
 		"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+
 		"</span>",
-		description,
+		archivedCSSClass, description,
 		textColor, scopeColor, scopeText,
 		textColor, itemColor, itemText)
 	return template.HTML(s)
@@ -211,7 +222,7 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n
 	return output
 }
 
-func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
+func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string) template.HTML {
 	htmlCode := `<span class="labels-list">`
 	for _, label := range labels {
 		// Protect against nil value in labels - shouldn't happen but would cause a panic if so
@@ -219,7 +230,7 @@ func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink st
 			continue
 		}
 		htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
-			repoLink, label.ID, RenderLabel(ctx, label))
+			repoLink, label.ID, RenderLabel(ctx, locale, label))
 	}
 	htmlCode += "</span>"
 	return template.HTML(htmlCode)
diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl
index a2296f6597..f573b8e09e 100644
--- a/templates/repo/issue/filter_actions.tmpl
+++ b/templates/repo/issue/filter_actions.tmpl
@@ -30,7 +30,7 @@
 					{{end}}
 					{{$previousExclusiveScope = $exclusiveScope}}
 					<div class="item issue-action gt-df gt-sb" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels">
-						{{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}}
+						{{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context ctx.Locale .}}
 						{{template "repo/issue/labels/label_archived" .}}
 					</div>
 				{{end}}
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index 9d3341cc81..d0086fdf8c 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -42,7 +42,7 @@
 						{{svg "octicon-check"}}
 					{{end}}
 				{{end}}
-				{{RenderLabel $.Context .}}
+				{{RenderLabel $.Context ctx.Locale .}}
 				<p class="gt-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
 			</a>
 		{{end}}
diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl
index 3ecae09373..d20d5e63d7 100644
--- a/templates/repo/issue/labels/label.tmpl
+++ b/templates/repo/issue/labels/label.tmpl
@@ -3,5 +3,5 @@
 	id="label_{{.label.ID}}"
 	href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}}
 >
-	{{- RenderLabel $.Context .label -}}
+	{{- RenderLabel $.Context ctx.Locale .label -}}
 </a>
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index 9b0061b60e..428a4919aa 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -32,7 +32,7 @@
 		{{range .Labels}}
 		<li class="item">
 			<div class="label-title">
-				{{RenderLabel $.Context .}}
+				{{RenderLabel $.Context ctx.Locale .}}
 				{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 			</div>
 			<div class="label-issues">
@@ -72,7 +72,7 @@
 			{{range .OrgLabels}}
 			<li class="item org-label">
 				<div class="label-title">
-					{{RenderLabel $.Context .}}
+					{{RenderLabel $.Context ctx.Locale .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 				</div>
 				<div class="label-issues">
diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl
index e42a1de895..a9c33bc4eb 100644
--- a/templates/repo/issue/labels/labels_selector_field.tmpl
+++ b/templates/repo/issue/labels/labels_selector_field.tmpl
@@ -21,7 +21,7 @@
 					<div class="divider"></div>
 				{{end}}
 				{{$previousExclusiveScope = $exclusiveScope}}
-				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
+				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context ctx.Locale .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 					<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
 				</a>
@@ -34,7 +34,7 @@
 					<div class="divider"></div>
 				{{end}}
 				{{$previousExclusiveScope = $exclusiveScope}}
-				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
+				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context ctx.Locale .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 					<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
 				</a>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 8bbcb1a54a..db63e2b951 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -173,11 +173,11 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
 						{{if and .AddedLabels (not .RemovedLabels)}}
-							{{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr}}
+							{{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) $createdStr}}
 						{{else if and (not .AddedLabels) .RemovedLabels}}
-							{{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}}
+							{{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}}
+							{{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}}
 						{{end}}
 					</span>
 				</div>
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index a90188297f..cffe579741 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -21,7 +21,7 @@
 						{{end}}
 						<span class="labels-list gt-ml-2">
 							{{range .Labels}}
-								<a href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context .}}</a>
+								<a href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context ctx.Locale .}}</a>
 							{{end}}
 						</span>
 					</div>
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 03d9664331..587a3152e5 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2417,6 +2417,10 @@
   gap: 0 !important;
 }
 
+.archived-label {
+  filter: grayscale(0.8);
+}
+
 .ui.label.scope-left {
   border-bottom-right-radius: 0;
   border-top-right-radius: 0;

From 3e7ae79f99ef0e5ba3d1201c38f491121ea2a156 Mon Sep 17 00:00:00 2001
From: JakobDev <jakobdev@gmx.de>
Date: Tue, 12 Mar 2024 22:40:43 +0100
Subject: [PATCH 347/679] Update Chroma to v2.13.0 (#29732)

This adds new lexers and includes some fixes. See
https://github.com/alecthomas/chroma/releases/tag/v2.13.0 for the full
changelog.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 go.mod |  4 ++--
 go.sum | 16 ++++++++--------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/go.mod b/go.mod
index 9b70a191ce..97f429aadb 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
 	github.com/NYTimes/gziphandler v1.1.1
 	github.com/PuerkitoBio/goquery v1.8.1
-	github.com/alecthomas/chroma/v2 v2.12.0
+	github.com/alecthomas/chroma/v2 v2.13.0
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
 	github.com/blevesearch/bleve/v2 v2.3.10
 	github.com/bufbuild/connect-go v1.10.0
@@ -172,7 +172,7 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/davidmz/go-pageant v1.0.2 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
-	github.com/dlclark/regexp2 v1.10.0 // indirect
+	github.com/dlclark/regexp2 v1.11.0 // indirect
 	github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
 	github.com/fatih/color v1.16.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
diff --git a/go.sum b/go.sum
index a44809dde5..916378d759 100644
--- a/go.sum
+++ b/go.sum
@@ -104,14 +104,14 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06
 github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
 github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
 github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
-github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
-github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
+github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
+github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
 github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
-github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
-github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
+github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
+github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
-github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
-github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
 github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
@@ -260,8 +260,8 @@ github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmW
 github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
-github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=

From 225fc405283a21c9ef966aa0bf8dabfe687804a8 Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Tue, 12 Mar 2024 23:09:02 +0100
Subject: [PATCH 348/679] Update to labeler v5 (#29721)

Updated to actions/labeler@v5

Updated labeler config accordingly, also improved the config and added
more labels.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 .github/labeler.yml                | 90 +++++++++++++++++++++++-------
 .github/workflows/pull-labeler.yml |  6 +-
 2 files changed, 72 insertions(+), 24 deletions(-)

diff --git a/.github/labeler.yml b/.github/labeler.yml
index 8a5ab26975..a1209c77b8 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,36 +1,84 @@
 modifies/docs:
-  - "**/*.md"
-  - "docs/**"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "**/*.md"
+          - "docs/**"
 
 modifies/frontend:
-  - "web_src/**/*"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "web_src/**"
+          - "tailwind.config.js"
+          - "webpack.config.js"
 
 modifies/templates:
-  - all: ["templates/**", "!templates/swagger/v1_json.tmpl"]
+  - changed-files:
+      - all-globs-to-any-file:
+          - "templates/**"
+          - "!templates/swagger/v1_json.tmpl"
 
 modifies/api:
-  - "routers/api/**"
-  - "templates/swagger/v1_json.tmpl"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "routers/api/**"
+          - "templates/swagger/v1_json.tmpl"
 
 modifies/cli:
-  - "cmd/**"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "cmd/**"
 
 modifies/translation:
-  - "options/locale/*.ini"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "options/locale/*.ini"
 
 modifies/migrations:
-  - "models/migrations/**/*"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "models/migrations/**"
 
 modifies/internal:
-  - "Makefile"
-  - "Dockerfile"
-  - "Dockerfile.rootless"
-  - "docker/**"
-  - "webpack.config.js"
-  - ".eslintrc.yaml"
-  - ".golangci.yml"
-  - ".markdownlint.yaml"
-  - ".spectral.yaml"
-  - ".stylelintrc.yaml"
-  - ".yamllint.yaml"
-  - ".github/**"
+  - changed-files:
+      - any-glob-to-any-file:
+          - ".air.toml"
+          - "Makefile"
+          - "Dockerfile"
+          - "Dockerfile.rootless"
+          - ".dockerignore"
+          - "docker/**"
+          - ".editorconfig"
+          - ".eslintrc.yaml"
+          - ".golangci.yml"
+          - ".gitpod.yml"
+          - ".markdownlint.yaml"
+          - ".spectral.yaml"
+          - ".stylelintrc.yaml"
+          - ".yamllint.yaml"
+          - ".github/**"
+          - ".gitea/"
+          - ".devcontainer/**"
+          - "build.go"
+          - "build/**"
+          - "contrib/**"
+
+modifies/dependencies:
+  - changed-files:
+      - any-glob-to-any-file:
+          - "package.json"
+          - "package-lock.json"
+          - "poetry.toml"
+          - "poetry.lock"
+          - "go.mod"
+          - "go.sum"
+          - "pyproject.toml"
+
+modifies/go:
+  - changed-files:
+      - any-glob-to-any-file:
+          - "**/*.go"
+
+modifies/js:
+  - changed-files:
+      - any-glob-to-any-file:
+          - "**/*.js"
diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml
index edd2f6d16e..812819b599 100644
--- a/.github/workflows/pull-labeler.yml
+++ b/.github/workflows/pull-labeler.yml
@@ -9,12 +9,12 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  label:
+  labeler:
     runs-on: ubuntu-latest
     permissions:
       contents: read
       pull-requests: write
     steps:
-      - uses: actions/labeler@v4
+      - uses: actions/labeler@v5
         with:
-          dot: true
+          sync-labels: true

From 857243bed7f9dccdc597c2941df41e821781cb0f Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 12 Mar 2024 23:37:02 +0100
Subject: [PATCH 349/679] Fix date rendering by adding `<gitea-absolute-date>`
 (#29725)

Alternative to: https://github.com/go-gitea/gitea/pull/29698
Fixes: https://github.com/go-gitea/gitea/issues/29034

<img width="278" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/12ecd967-2723-410d-8a28-a1b0f41b7bba">

It also fixes a secondary issue that we were showing timestamp tooltips
over date, which makes no sense, so these are now gone as well:

<img width="284" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/a70432f3-97b6-41e6-b202-b53b76924a66">
---
 modules/timeutil/datetime.go                  | 16 ++++----
 modules/timeutil/datetime_test.go             | 14 ++++---
 templates/devtest/gitea-ui.tmpl               | 10 +++++
 web_src/js/webcomponents/GiteaAbsoluteDate.js | 40 +++++++++++++++++++
 web_src/js/webcomponents/webcomponents.js     |  1 +
 5 files changed, 67 insertions(+), 14 deletions(-)
 create mode 100644 web_src/js/webcomponents/GiteaAbsoluteDate.js

diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go
index 62b94f7cf4..50c8d44f13 100644
--- a/modules/timeutil/datetime.go
+++ b/modules/timeutil/datetime.go
@@ -51,18 +51,16 @@ func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
 
 	attrs := make([]string, 0, 10+len(extraAttrs))
 	attrs = append(attrs, extraAttrs...)
-	attrs = append(attrs, `data-tooltip-content`, `data-tooltip-interactive="true"`)
-	attrs = append(attrs, `format="datetime"`, `weekday=""`, `year="numeric"`)
+	attrs = append(attrs, `weekday=""`, `year="numeric"`)
 
 	switch format {
-	case "short":
-		attrs = append(attrs, `month="short"`, `day="numeric"`)
-	case "long":
-		attrs = append(attrs, `month="long"`, `day="numeric"`)
-	case "full":
-		attrs = append(attrs, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`)
+	case "short", "long": // date only
+		attrs = append(attrs, `month="`+format+`"`, `day="numeric"`)
+		return template.HTML(fmt.Sprintf(`<gitea-absolute-date %s date="%s">%s</gitea-absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
+	case "full": // full date including time
+		attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
+		return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
 	default:
 		panic(fmt.Sprintf("Unsupported format %s", format))
 	}
-	return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
 }
diff --git a/modules/timeutil/datetime_test.go b/modules/timeutil/datetime_test.go
index 26494b8475..39aecbc43b 100644
--- a/modules/timeutil/datetime_test.go
+++ b/modules/timeutil/datetime_test.go
@@ -18,6 +18,7 @@ func TestDateTime(t *testing.T) {
 	defer test.MockVariableValue(&setting.DefaultUILocation, testTz)()
 
 	refTimeStr := "2018-01-01T00:00:00Z"
+	refDateStr := "2018-01-01"
 	refTime, _ := time.Parse(time.RFC3339, refTimeStr)
 	refTimeStamp := TimeStamp(refTime.Unix())
 
@@ -27,17 +28,20 @@ func TestDateTime(t *testing.T) {
 	assert.EqualValues(t, "-", DateTime("short", TimeStamp(0)))
 
 	actual := DateTime("short", "invalid")
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="invalid">invalid</relative-time>`, actual)
+	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="invalid">invalid</gitea-absolute-date>`, actual)
 
 	actual = DateTime("short", refTimeStr)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</relative-time>`, actual)
+	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</gitea-absolute-date>`, actual)
 
 	actual = DateTime("short", refTime)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01</relative-time>`, actual)
+	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</gitea-absolute-date>`, actual)
+
+	actual = DateTime("short", refDateStr)
+	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01">2018-01-01</gitea-absolute-date>`, actual)
 
 	actual = DateTime("short", refTimeStamp)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31</relative-time>`, actual)
+	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</gitea-absolute-date>`, actual)
 
 	actual = DateTime("full", refTimeStamp)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual)
+	assert.EqualValues(t, `<relative-time weekday="" year="numeric" format="datetime" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" data-tooltip-content data-tooltip-interactive="true" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual)
 }
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index ccf188609c..e551572b96 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -110,6 +110,16 @@
 		<div><gitea-origin-url data-url="/test/url"></gitea-origin-url></div>
 	</div>
 
+	<div>
+		<h1>GiteaAbsoluteDate</h1>
+		<div><gitea-absolute-date date="2024-03-11" year="numeric" day="numeric" month="short"></gitea-absolute-date></div>
+		<div><gitea-absolute-date date="2024-03-11" year="numeric" day="numeric" month="long"></gitea-absolute-date></div>
+		<div><gitea-absolute-date date="2024-03-11" year="" day="numeric" month="numeric"></gitea-absolute-date></div>
+		<div><gitea-absolute-date date="2024-03-11" year="" day="numeric" month="numeric" weekday="long"></gitea-absolute-date></div>
+		<div><gitea-absolute-date date="2024-03-11T19:00:00-05:00" year="" day="numeric" month="numeric" weekday="long"></gitea-absolute-date></div>
+		<div class="tw-text-text-light-2">relative-time: <relative-time format="datetime" datetime="2024-03-11" year="" day="numeric" month="numeric"></relative-time></div>
+	</div>
+
 	<div>
 		<h1>LocaleNumber</h1>
 		<div>{{ctx.Locale.PrettyNumber 1}}</div>
diff --git a/web_src/js/webcomponents/GiteaAbsoluteDate.js b/web_src/js/webcomponents/GiteaAbsoluteDate.js
new file mode 100644
index 0000000000..660aa99d07
--- /dev/null
+++ b/web_src/js/webcomponents/GiteaAbsoluteDate.js
@@ -0,0 +1,40 @@
+window.customElements.define('gitea-absolute-date', class extends HTMLElement {
+  static observedAttributes = ['date', 'year', 'month', 'weekday', 'day'];
+
+  update = () => {
+    const year = this.getAttribute('year') ?? '';
+    const month = this.getAttribute('month') ?? '';
+    const weekday = this.getAttribute('weekday') ?? '';
+    const day = this.getAttribute('day') ?? '';
+    const lang = this.closest('[lang]')?.getAttribute('lang') ||
+      this.ownerDocument.documentElement.getAttribute('lang') ||
+      '';
+
+    // only extract the `yyyy-mm-dd` part. When converting to Date, it will become midnight UTC and when rendered
+    // as localized date, will have a offset towards UTC, which we remove to shift the timestamp to midnight in the
+    // localized date. We should eventually use `Temporal.PlainDate` which will make the correction unnecessary.
+    // - https://stackoverflow.com/a/14569783/808699
+    // - https://tc39.es/proposal-temporal/docs/plaindate.html
+    const date = new Date(this.getAttribute('date').substring(0, 10));
+    const correctedDate = new Date(date.getTime() - date.getTimezoneOffset() * -60000);
+
+    if (!this.shadowRoot) this.attachShadow({mode: 'open'});
+    this.shadowRoot.textContent = correctedDate.toLocaleString(lang ?? [], {
+      ...(year && {year}),
+      ...(month && {month}),
+      ...(weekday && {weekday}),
+      ...(day && {day}),
+    });
+  };
+
+  attributeChangedCallback(_name, oldValue, newValue) {
+    if (!this.initialized || oldValue === newValue) return;
+    this.update();
+  }
+
+  connectedCallback() {
+    this.initialized = false;
+    this.update();
+    this.initialized = true;
+  }
+});
diff --git a/web_src/js/webcomponents/webcomponents.js b/web_src/js/webcomponents/webcomponents.js
index 916a588db6..03348d895f 100644
--- a/web_src/js/webcomponents/webcomponents.js
+++ b/web_src/js/webcomponents/webcomponents.js
@@ -3,3 +3,4 @@ import './polyfill.js';
 
 import '@github/relative-time-element';
 import './GiteaOriginUrl.js';
+import './GiteaAbsoluteDate.js';

From 9a93b1816e0bc65101e7ad7ca66786fb38a8e628 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 13 Mar 2024 07:04:07 +0100
Subject: [PATCH 350/679] Refactor label.IsArchived() (#29750)

just some missed nits
---
 models/issues/label.go           | 12 ++++++------
 modules/templates/util_render.go |  3 +--
 routers/web/repo/issue_label.go  | 12 +++++-------
 3 files changed, 12 insertions(+), 15 deletions(-)

diff --git a/models/issues/label.go b/models/issues/label.go
index f6ecc68cd1..2397a29e35 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -116,12 +116,17 @@ func (l *Label) CalOpenIssues() {
 func (l *Label) SetArchived(isArchived bool) {
 	if !isArchived {
 		l.ArchivedUnix = timeutil.TimeStamp(0)
-	} else if isArchived && l.ArchivedUnix.IsZero() {
+	} else if isArchived && !l.IsArchived() {
 		// Only change the date when it is newly archived.
 		l.ArchivedUnix = timeutil.TimeStampNow()
 	}
 }
 
+// IsArchived returns true if label is an archived
+func (l *Label) IsArchived() bool {
+	return !l.ArchivedUnix.IsZero()
+}
+
 // CalOpenOrgIssues calculates the open issues of a label for a specific repo
 func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
 	counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
@@ -166,11 +171,6 @@ func (l *Label) BelongsToOrg() bool {
 	return l.OrgID > 0
 }
 
-// IsArchived returns true if label is an archived
-func (l *Label) IsArchived() bool {
-	return l.ArchivedUnix > 0
-}
-
 // BelongsToRepo returns true if label is a repository label
 func (l *Label) BelongsToRepo() bool {
 	return l.RepoID > 0
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index ba9b050e02..7ed3a8b9b4 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -124,7 +124,6 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
 	var (
 		archivedCSSClass string
 		textColor        = "#111"
-		isArchived       = !label.ArchivedUnix.IsZero()
 		labelScope       = label.ExclusiveScope()
 	)
 
@@ -136,7 +135,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
 
 	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
 
-	if isArchived {
+	if label.IsArchived() {
 		archivedCSSClass = "archived-label"
 		description = fmt.Sprintf("(%s) %s", locale.TrString("archived"), description)
 	}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 9dedaefa4b..81bee4dbb5 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -13,7 +13,6 @@ import (
 	"code.gitea.io/gitea/modules/label"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
-	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
@@ -112,12 +111,11 @@ func NewLabel(ctx *context.Context) {
 	}
 
 	l := &issues_model.Label{
-		RepoID:       ctx.Repo.Repository.ID,
-		Name:         form.Title,
-		Exclusive:    form.Exclusive,
-		Description:  form.Description,
-		Color:        form.Color,
-		ArchivedUnix: timeutil.TimeStamp(0),
+		RepoID:      ctx.Repo.Repository.ID,
+		Name:        form.Title,
+		Exclusive:   form.Exclusive,
+		Description: form.Description,
+		Color:       form.Color,
 	}
 	if err := issues_model.NewLabel(ctx, l); err != nil {
 		ctx.ServerError("NewLabel", err)

From 67e9f0d49828f62a942ac6db04153207f88e82ee Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 13 Mar 2024 14:57:30 +0800
Subject: [PATCH 351/679] Fix user router possbile panic (#29751)

regression from #28023
---
 routers/web/user/home.go       | 7 +++++--
 tests/integration/user_test.go | 9 +++++++++
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index caa7115259..e731a2a9b7 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -714,12 +714,16 @@ func UsernameSubRoute(ctx *context.Context) {
 	reloadParam := func(suffix string) (success bool) {
 		ctx.SetParams("username", strings.TrimSuffix(username, suffix))
 		context.UserAssignmentWeb()(ctx)
+		if ctx.Written() {
+			return false
+		}
+
 		// check view permissions
 		if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
 			ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
 			return false
 		}
-		return !ctx.Written()
+		return true
 	}
 	switch {
 	case strings.HasSuffix(username, ".png"):
@@ -740,7 +744,6 @@ func UsernameSubRoute(ctx *context.Context) {
 			return
 		}
 		if reloadParam(".rss") {
-			context.UserAssignmentWeb()(ctx)
 			feed.ShowUserFeedRSS(ctx)
 		}
 	case strings.HasSuffix(username, ".atom"):
diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go
index c30733b1b0..c4544f37aa 100644
--- a/tests/integration/user_test.go
+++ b/tests/integration/user_test.go
@@ -243,6 +243,8 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) {
 }
 
 func TestGetUserRss(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
 	user34 := "the_34-user.with.all.allowedChars"
 	req := NewRequestf(t, "GET", "/%s.rss", user34)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -253,6 +255,13 @@ func TestGetUserRss(t *testing.T) {
 		description, _ := rssDoc.ChildrenFiltered("description").Html()
 		assert.EqualValues(t, "&lt;p dir=&#34;auto&#34;&gt;some &lt;a href=&#34;https://commonmark.org/&#34; rel=&#34;nofollow&#34;&gt;commonmark&lt;/a&gt;!&lt;/p&gt;\n", description)
 	}
+
+	req = NewRequestf(t, "GET", "/non-existent-user.rss")
+	MakeRequest(t, req, http.StatusNotFound)
+
+	session := loginUser(t, "user2")
+	req = NewRequestf(t, "GET", "/non-existent-user.rss")
+	session.MakeRequest(t, req, http.StatusNotFound)
 }
 
 func TestListStopWatches(t *testing.T) {

From 7fd0a5b276aadcf88dcc012fcd364fe160a58810 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 13 Mar 2024 09:25:53 +0100
Subject: [PATCH 352/679] Refactor to use optional.Option for issue index
 search option (#29739)

Signed-off-by: 6543 <6543@obermui.de>
---
 modules/indexer/internal/bleve/query.go       | 12 ++--
 modules/indexer/issues/bleve/bleve.go         | 39 ++++++-----
 modules/indexer/issues/db/options.go          | 32 ++++-----
 modules/indexer/issues/dboptions.go           | 12 ++--
 .../issues/elasticsearch/elasticsearch.go     | 42 ++++++------
 modules/indexer/issues/indexer_test.go        | 65 ++++++++-----------
 modules/indexer/issues/internal/model.go      | 20 +++---
 .../indexer/issues/internal/tests/tests.go    | 65 ++++---------------
 .../indexer/issues/meilisearch/meilisearch.go | 40 ++++++------
 routers/api/v1/repo/issue.go                  | 24 +++----
 routers/web/repo/issue.go                     | 32 ++++-----
 routers/web/user/home.go                      | 20 +++---
 12 files changed, 178 insertions(+), 225 deletions(-)

diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go
index 2a427c4020..b96875343e 100644
--- a/modules/indexer/internal/bleve/query.go
+++ b/modules/indexer/internal/bleve/query.go
@@ -4,6 +4,8 @@
 package bleve
 
 import (
+	"code.gitea.io/gitea/modules/optional"
+
 	"github.com/blevesearch/bleve/v2"
 	"github.com/blevesearch/bleve/v2/search/query"
 )
@@ -39,18 +41,18 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery {
 	return q
 }
 
-func NumericRangeInclusiveQuery(min, max *int64, field string) *query.NumericRangeQuery {
+func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery {
 	var minF, maxF *float64
 	var minI, maxI *bool
-	if min != nil {
+	if min.Has() {
 		minF = new(float64)
-		*minF = float64(*min)
+		*minF = float64(min.Value())
 		minI = new(bool)
 		*minI = true
 	}
-	if max != nil {
+	if max.Has() {
 		maxF = new(float64)
-		*maxF = float64(*max)
+		*maxF = float64(max.Value())
 		maxI = new(bool)
 		*maxI = true
 	}
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index aaea854efa..927ad58cd4 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -224,38 +224,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...))
 	}
 
-	if options.ProjectID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectID, "project_id"))
+	if options.ProjectID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
 	}
-	if options.ProjectBoardID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectBoardID, "project_board_id"))
+	if options.ProjectBoardID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id"))
 	}
 
-	if options.PosterID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.PosterID, "poster_id"))
+	if options.PosterID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id"))
 	}
 
-	if options.AssigneeID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.AssigneeID, "assignee_id"))
+	if options.AssigneeID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id"))
 	}
 
-	if options.MentionID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.MentionID, "mention_ids"))
+	if options.MentionID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.MentionID.Value(), "mention_ids"))
 	}
 
-	if options.ReviewedID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewedID, "reviewed_ids"))
+	if options.ReviewedID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewedID.Value(), "reviewed_ids"))
 	}
-	if options.ReviewRequestedID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewRequestedID, "review_requested_ids"))
+	if options.ReviewRequestedID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewRequestedID.Value(), "review_requested_ids"))
 	}
 
-	if options.SubscriberID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.SubscriberID, "subscriber_ids"))
+	if options.SubscriberID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.SubscriberID.Value(), "subscriber_ids"))
 	}
 
-	if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil {
-		queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(options.UpdatedAfterUnix, options.UpdatedBeforeUnix, "updated_unix"))
+	if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() {
+		queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(
+			options.UpdatedAfterUnix,
+			options.UpdatedBeforeUnix,
+			"updated_unix"))
 	}
 
 	var indexerQuery query.Query = bleve.NewConjunctionQuery(queries...)
diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go
index 69146573a8..eeaf1696ad 100644
--- a/modules/indexer/issues/db/options.go
+++ b/modules/indexer/issues/db/options.go
@@ -15,22 +15,6 @@ import (
 )
 
 func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) {
-	// See the comment of issues_model.SearchOptions for the reason why we need to convert
-	convertID := func(id *int64) int64 {
-		if id == nil {
-			return 0
-		}
-		if *id == 0 {
-			return db.NoConditionID
-		}
-		return *id
-	}
-	convertInt64 := func(i *int64) int64 {
-		if i == nil {
-			return 0
-		}
-		return *i
-	}
 	var sortType string
 	switch options.SortBy {
 	case internal.SortByCreatedAsc:
@@ -53,6 +37,18 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 		sortType = "newest"
 	}
 
+	// See the comment of issues_model.SearchOptions for the reason why we need to convert
+	convertID := func(id optional.Option[int64]) int64 {
+		if !id.Has() {
+			return 0
+		}
+		value := id.Value()
+		if value == 0 {
+			return db.NoConditionID
+		}
+		return value
+	}
+
 	opts := &issue_model.IssuesOptions{
 		Paginator:          options.Paginator,
 		RepoIDs:            options.RepoIDs,
@@ -73,8 +69,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 		IncludeMilestones:  nil,
 		SortType:           sortType,
 		IssueIDs:           nil,
-		UpdatedAfterUnix:   convertInt64(options.UpdatedAfterUnix),
-		UpdatedBeforeUnix:  convertInt64(options.UpdatedBeforeUnix),
+		UpdatedAfterUnix:   options.UpdatedAfterUnix.Value(),
+		UpdatedBeforeUnix:  options.UpdatedBeforeUnix.Value(),
 		PriorityRepoID:     0,
 		IsArchived:         optional.None[bool](),
 		Org:                nil,
diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go
index 80e233e29a..4a98b4588a 100644
--- a/modules/indexer/issues/dboptions.go
+++ b/modules/indexer/issues/dboptions.go
@@ -6,6 +6,7 @@ package issues
 import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions {
@@ -38,13 +39,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
 	}
 
 	// See the comment of issues_model.SearchOptions for the reason why we need to convert
-	convertID := func(id int64) *int64 {
+	convertID := func(id int64) optional.Option[int64] {
 		if id > 0 {
-			return &id
+			return optional.Some(id)
 		}
 		if id == db.NoConditionID {
-			var zero int64
-			return &zero
+			return optional.None[int64]()
 		}
 		return nil
 	}
@@ -59,10 +59,10 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
 	searchOpt.SubscriberID = convertID(opts.SubscriberID)
 
 	if opts.UpdatedAfterUnix > 0 {
-		searchOpt.UpdatedAfterUnix = &opts.UpdatedAfterUnix
+		searchOpt.UpdatedAfterUnix = optional.Some(opts.UpdatedAfterUnix)
 	}
 	if opts.UpdatedBeforeUnix > 0 {
-		searchOpt.UpdatedBeforeUnix = &opts.UpdatedBeforeUnix
+		searchOpt.UpdatedBeforeUnix = optional.Some(opts.UpdatedBeforeUnix)
 	}
 
 	searchOpt.Paginator = opts.Paginator
diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go
index 0077da263a..53b383c8d5 100644
--- a/modules/indexer/issues/elasticsearch/elasticsearch.go
+++ b/modules/indexer/issues/elasticsearch/elasticsearch.go
@@ -195,43 +195,43 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...))
 	}
 
-	if options.ProjectID != nil {
-		query.Must(elastic.NewTermQuery("project_id", *options.ProjectID))
+	if options.ProjectID.Has() {
+		query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
 	}
-	if options.ProjectBoardID != nil {
-		query.Must(elastic.NewTermQuery("project_board_id", *options.ProjectBoardID))
+	if options.ProjectBoardID.Has() {
+		query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value()))
 	}
 
-	if options.PosterID != nil {
-		query.Must(elastic.NewTermQuery("poster_id", *options.PosterID))
+	if options.PosterID.Has() {
+		query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value()))
 	}
 
-	if options.AssigneeID != nil {
-		query.Must(elastic.NewTermQuery("assignee_id", *options.AssigneeID))
+	if options.AssigneeID.Has() {
+		query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value()))
 	}
 
-	if options.MentionID != nil {
-		query.Must(elastic.NewTermQuery("mention_ids", *options.MentionID))
+	if options.MentionID.Has() {
+		query.Must(elastic.NewTermQuery("mention_ids", options.MentionID.Value()))
 	}
 
-	if options.ReviewedID != nil {
-		query.Must(elastic.NewTermQuery("reviewed_ids", *options.ReviewedID))
+	if options.ReviewedID.Has() {
+		query.Must(elastic.NewTermQuery("reviewed_ids", options.ReviewedID.Value()))
 	}
-	if options.ReviewRequestedID != nil {
-		query.Must(elastic.NewTermQuery("review_requested_ids", *options.ReviewRequestedID))
+	if options.ReviewRequestedID.Has() {
+		query.Must(elastic.NewTermQuery("review_requested_ids", options.ReviewRequestedID.Value()))
 	}
 
-	if options.SubscriberID != nil {
-		query.Must(elastic.NewTermQuery("subscriber_ids", *options.SubscriberID))
+	if options.SubscriberID.Has() {
+		query.Must(elastic.NewTermQuery("subscriber_ids", options.SubscriberID.Value()))
 	}
 
-	if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil {
+	if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() {
 		q := elastic.NewRangeQuery("updated_unix")
-		if options.UpdatedAfterUnix != nil {
-			q.Gte(*options.UpdatedAfterUnix)
+		if options.UpdatedAfterUnix.Has() {
+			q.Gte(options.UpdatedAfterUnix.Value())
 		}
-		if options.UpdatedBeforeUnix != nil {
-			q.Lte(*options.UpdatedBeforeUnix)
+		if options.UpdatedBeforeUnix.Has() {
+			q.Lte(options.UpdatedBeforeUnix.Value())
 		}
 		query.Must(q)
 	}
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index 10ffa7cbe6..0d0cfc8516 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -134,63 +134,60 @@ func searchIssueInRepo(t *testing.T) {
 }
 
 func searchIssueByID(t *testing.T) {
-	int64Pointer := func(x int64) *int64 {
-		return &x
-	}
 	tests := []struct {
 		opts        SearchOptions
 		expectedIDs []int64
 	}{
 		{
-			SearchOptions{
-				PosterID: int64Pointer(1),
+			opts: SearchOptions{
+				PosterID: optional.Some(int64(1)),
 			},
-			[]int64{11, 6, 3, 2, 1},
+			expectedIDs: []int64{11, 6, 3, 2, 1},
 		},
 		{
-			SearchOptions{
-				AssigneeID: int64Pointer(1),
+			opts: SearchOptions{
+				AssigneeID: optional.Some(int64(1)),
 			},
-			[]int64{6, 1},
+			expectedIDs: []int64{6, 1},
 		},
 		{
-			SearchOptions{
-				MentionID: int64Pointer(4),
+			opts: SearchOptions{
+				MentionID: optional.Some(int64(4)),
 			},
-			[]int64{1},
+			expectedIDs: []int64{1},
 		},
 		{
-			SearchOptions{
-				ReviewedID: int64Pointer(1),
+			opts: SearchOptions{
+				ReviewedID: optional.Some(int64(1)),
 			},
-			[]int64{},
+			expectedIDs: []int64{},
 		},
 		{
-			SearchOptions{
-				ReviewRequestedID: int64Pointer(1),
+			opts: SearchOptions{
+				ReviewRequestedID: optional.Some(int64(1)),
 			},
-			[]int64{12},
+			expectedIDs: []int64{12},
 		},
 		{
-			SearchOptions{
-				SubscriberID: int64Pointer(1),
+			opts: SearchOptions{
+				SubscriberID: optional.Some(int64(1)),
 			},
-			[]int64{11, 6, 5, 3, 2, 1},
+			expectedIDs: []int64{11, 6, 5, 3, 2, 1},
 		},
 		{
 			// issue 20 request user 15 and team 5 which user 15 belongs to
 			// the review request number of issue 20 should be 1
-			SearchOptions{
-				ReviewRequestedID: int64Pointer(15),
+			opts: SearchOptions{
+				ReviewRequestedID: optional.Some(int64(15)),
 			},
-			[]int64{12, 20},
+			expectedIDs: []int64{12, 20},
 		},
 		{
 			// user 20 approved the issue 20, so return nothing
-			SearchOptions{
-				ReviewRequestedID: int64Pointer(20),
+			opts: SearchOptions{
+				ReviewRequestedID: optional.Some(int64(20)),
 			},
-			[]int64{},
+			expectedIDs: []int64{},
 		},
 	}
 
@@ -318,16 +315,13 @@ func searchIssueByLabelID(t *testing.T) {
 }
 
 func searchIssueByTime(t *testing.T) {
-	int64Pointer := func(i int64) *int64 {
-		return &i
-	}
 	tests := []struct {
 		opts        SearchOptions
 		expectedIDs []int64
 	}{
 		{
 			SearchOptions{
-				UpdatedAfterUnix: int64Pointer(0),
+				UpdatedAfterUnix: optional.Some(int64(0)),
 			},
 			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
 		},
@@ -363,28 +357,25 @@ func searchIssueWithOrder(t *testing.T) {
 }
 
 func searchIssueInProject(t *testing.T) {
-	int64Pointer := func(i int64) *int64 {
-		return &i
-	}
 	tests := []struct {
 		opts        SearchOptions
 		expectedIDs []int64
 	}{
 		{
 			SearchOptions{
-				ProjectID: int64Pointer(1),
+				ProjectID: optional.Some(int64(1)),
 			},
 			[]int64{5, 3, 2, 1},
 		},
 		{
 			SearchOptions{
-				ProjectBoardID: int64Pointer(1),
+				ProjectBoardID: optional.Some(int64(1)),
 			},
 			[]int64{1},
 		},
 		{
 			SearchOptions{
-				ProjectBoardID: int64Pointer(0), // issue with in default board
+				ProjectBoardID: optional.Some(int64(0)), // issue with in default board
 			},
 			[]int64{2},
 		},
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index d41fec4aba..b7102c35af 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -89,22 +89,22 @@ type SearchOptions struct {
 
 	MilestoneIDs []int64 // milestones the issues have
 
-	ProjectID      *int64 // project the issues belong to
-	ProjectBoardID *int64 // project board the issues belong to
+	ProjectID      optional.Option[int64] // project the issues belong to
+	ProjectBoardID optional.Option[int64] // project board the issues belong to
 
-	PosterID *int64 // poster of the issues
+	PosterID optional.Option[int64] // poster of the issues
 
-	AssigneeID *int64 // assignee of the issues, zero means no assignee
+	AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee
 
-	MentionID *int64 // mentioned user of the issues
+	MentionID optional.Option[int64] // mentioned user of the issues
 
-	ReviewedID        *int64 // reviewer of the issues
-	ReviewRequestedID *int64 // requested reviewer of the issues
+	ReviewedID        optional.Option[int64] // reviewer of the issues
+	ReviewRequestedID optional.Option[int64] // requested reviewer of the issues
 
-	SubscriberID *int64 // subscriber of the issues
+	SubscriberID optional.Option[int64] // subscriber of the issues
 
-	UpdatedAfterUnix  *int64
-	UpdatedBeforeUnix *int64
+	UpdatedAfterUnix  optional.Option[int64]
+	UpdatedBeforeUnix optional.Option[int64]
 
 	db.Paginator
 
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 6724471539..91aafd589c 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -300,10 +300,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ProjectID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -321,10 +318,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectID: func() *int64 {
-				id := int64(0)
-				return &id
-			}(),
+			ProjectID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -342,10 +336,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectBoardID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ProjectBoardID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -363,10 +354,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectBoardID: func() *int64 {
-				id := int64(0)
-				return &id
-			}(),
+			ProjectBoardID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -384,10 +372,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			PosterID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			PosterID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -405,10 +390,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			AssigneeID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			AssigneeID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -426,10 +408,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			AssigneeID: func() *int64 {
-				id := int64(0)
-				return &id
-			}(),
+			AssigneeID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -447,10 +426,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			MentionID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			MentionID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -468,10 +444,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ReviewedID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ReviewedID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -489,10 +462,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ReviewRequestedID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ReviewRequestedID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -510,10 +480,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			SubscriberID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			SubscriberID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -531,14 +498,8 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			UpdatedAfterUnix: func() *int64 {
-				var t int64 = 20
-				return &t
-			}(),
-			UpdatedBeforeUnix: func() *int64 {
-				var t int64 = 30
-				return &t
-			}(),
+			UpdatedAfterUnix:  optional.Some(int64(20)),
+			UpdatedBeforeUnix: optional.Some(int64(30)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index c429920065..34066bf559 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -170,41 +170,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...))
 	}
 
-	if options.ProjectID != nil {
-		query.And(inner_meilisearch.NewFilterEq("project_id", *options.ProjectID))
+	if options.ProjectID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
 	}
-	if options.ProjectBoardID != nil {
-		query.And(inner_meilisearch.NewFilterEq("project_board_id", *options.ProjectBoardID))
+	if options.ProjectBoardID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value()))
 	}
 
-	if options.PosterID != nil {
-		query.And(inner_meilisearch.NewFilterEq("poster_id", *options.PosterID))
+	if options.PosterID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value()))
 	}
 
-	if options.AssigneeID != nil {
-		query.And(inner_meilisearch.NewFilterEq("assignee_id", *options.AssigneeID))
+	if options.AssigneeID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value()))
 	}
 
-	if options.MentionID != nil {
-		query.And(inner_meilisearch.NewFilterEq("mention_ids", *options.MentionID))
+	if options.MentionID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("mention_ids", options.MentionID.Value()))
 	}
 
-	if options.ReviewedID != nil {
-		query.And(inner_meilisearch.NewFilterEq("reviewed_ids", *options.ReviewedID))
+	if options.ReviewedID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("reviewed_ids", options.ReviewedID.Value()))
 	}
-	if options.ReviewRequestedID != nil {
-		query.And(inner_meilisearch.NewFilterEq("review_requested_ids", *options.ReviewRequestedID))
+	if options.ReviewRequestedID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("review_requested_ids", options.ReviewRequestedID.Value()))
 	}
 
-	if options.SubscriberID != nil {
-		query.And(inner_meilisearch.NewFilterEq("subscriber_ids", *options.SubscriberID))
+	if options.SubscriberID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("subscriber_ids", options.SubscriberID.Value()))
 	}
 
-	if options.UpdatedAfterUnix != nil {
-		query.And(inner_meilisearch.NewFilterGte("updated_unix", *options.UpdatedAfterUnix))
+	if options.UpdatedAfterUnix.Has() {
+		query.And(inner_meilisearch.NewFilterGte("updated_unix", options.UpdatedAfterUnix.Value()))
 	}
-	if options.UpdatedBeforeUnix != nil {
-		query.And(inner_meilisearch.NewFilterLte("updated_unix", *options.UpdatedBeforeUnix))
+	if options.UpdatedBeforeUnix.Has() {
+		query.And(inner_meilisearch.NewFilterLte("updated_unix", options.UpdatedBeforeUnix.Value()))
 	}
 
 	if options.SortBy == "" {
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 09175fe0ac..61a318baab 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -269,28 +269,28 @@ func SearchIssues(ctx *context.APIContext) {
 	}
 
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 
 	if ctx.IsSigned {
 		ctxUserID := ctx.Doer.ID
 		if ctx.FormBool("created") {
-			searchOpt.PosterID = &ctxUserID
+			searchOpt.PosterID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("assigned") {
-			searchOpt.AssigneeID = &ctxUserID
+			searchOpt.AssigneeID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("mentioned") {
-			searchOpt.MentionID = &ctxUserID
+			searchOpt.MentionID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("review_requested") {
-			searchOpt.ReviewRequestedID = &ctxUserID
+			searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("reviewed") {
-			searchOpt.ReviewedID = &ctxUserID
+			searchOpt.ReviewedID = optional.Some(ctxUserID)
 		}
 	}
 
@@ -502,10 +502,10 @@ func ListIssues(ctx *context.APIContext) {
 		SortBy:    issue_indexer.SortByCreatedDesc,
 	}
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 	if len(labelIDs) == 1 && labelIDs[0] == 0 {
 		searchOpt.NoLabelOnly = true
@@ -526,13 +526,13 @@ func ListIssues(ctx *context.APIContext) {
 	}
 
 	if createdByID > 0 {
-		searchOpt.PosterID = &createdByID
+		searchOpt.PosterID = optional.Some(createdByID)
 	}
 	if assignedByID > 0 {
-		searchOpt.AssigneeID = &assignedByID
+		searchOpt.AssigneeID = optional.Some(assignedByID)
 	}
 	if mentionedByID > 0 {
-		searchOpt.MentionID = &mentionedByID
+		searchOpt.MentionID = optional.Some(mentionedByID)
 	}
 
 	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 8935cc80e2..d5fd8439f3 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -2634,9 +2634,9 @@ func SearchIssues(ctx *context.Context) {
 		}
 	}
 
-	var projectID *int64
+	projectID := optional.None[int64]()
 	if v := ctx.FormInt64("project"); v > 0 {
-		projectID = &v
+		projectID = optional.Some(v)
 	}
 
 	// this api is also used in UI,
@@ -2665,28 +2665,28 @@ func SearchIssues(ctx *context.Context) {
 	}
 
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 
 	if ctx.IsSigned {
 		ctxUserID := ctx.Doer.ID
 		if ctx.FormBool("created") {
-			searchOpt.PosterID = &ctxUserID
+			searchOpt.PosterID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("assigned") {
-			searchOpt.AssigneeID = &ctxUserID
+			searchOpt.AssigneeID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("mentioned") {
-			searchOpt.MentionID = &ctxUserID
+			searchOpt.MentionID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("review_requested") {
-			searchOpt.ReviewRequestedID = &ctxUserID
+			searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("reviewed") {
-			searchOpt.ReviewedID = &ctxUserID
+			searchOpt.ReviewedID = optional.Some(ctxUserID)
 		}
 	}
 
@@ -2791,9 +2791,9 @@ func ListIssues(ctx *context.Context) {
 		}
 	}
 
-	var projectID *int64
+	projectID := optional.None[int64]()
 	if v := ctx.FormInt64("project"); v > 0 {
-		projectID = &v
+		projectID = optional.Some(v)
 	}
 
 	isPull := optional.None[bool]()
@@ -2831,10 +2831,10 @@ func ListIssues(ctx *context.Context) {
 		SortBy:         issue_indexer.SortByCreatedDesc,
 	}
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 	if len(labelIDs) == 1 && labelIDs[0] == 0 {
 		searchOpt.NoLabelOnly = true
@@ -2855,13 +2855,13 @@ func ListIssues(ctx *context.Context) {
 	}
 
 	if createdByID > 0 {
-		searchOpt.PosterID = &createdByID
+		searchOpt.PosterID = optional.Some(createdByID)
 	}
 	if assignedByID > 0 {
-		searchOpt.AssigneeID = &assignedByID
+		searchOpt.AssigneeID = optional.Some(assignedByID)
 	}
 	if mentionedByID > 0 {
-		searchOpt.MentionID = &mentionedByID
+		searchOpt.MentionID = optional.Some(mentionedByID)
 	}
 
 	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index e731a2a9b7..4ec6f6be3f 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -792,15 +792,15 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
 		case issues_model.FilterModeYourRepositories:
 			openClosedOpts.AllPublic = false
 		case issues_model.FilterModeAssign:
-			openClosedOpts.AssigneeID = &doerID
+			openClosedOpts.AssigneeID = optional.Some(doerID)
 		case issues_model.FilterModeCreate:
-			openClosedOpts.PosterID = &doerID
+			openClosedOpts.PosterID = optional.Some(doerID)
 		case issues_model.FilterModeMention:
-			openClosedOpts.MentionID = &doerID
+			openClosedOpts.MentionID = optional.Some(doerID)
 		case issues_model.FilterModeReviewRequested:
-			openClosedOpts.ReviewRequestedID = &doerID
+			openClosedOpts.ReviewRequestedID = optional.Some(doerID)
 		case issues_model.FilterModeReviewed:
-			openClosedOpts.ReviewedID = &doerID
+			openClosedOpts.ReviewedID = optional.Some(doerID)
 		}
 		openClosedOpts.IsClosed = optional.Some(false)
 		ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
@@ -818,23 +818,23 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
 	if err != nil {
 		return nil, err
 	}
-	ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID }))
+	ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID }))
+	ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID }))
+	ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID }))
+	ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID }))
+	ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}

From 9b1a8888fa754676073bc851b783b2b8f1adecfb Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 13 Mar 2024 09:43:58 +0100
Subject: [PATCH 353/679] Configure pinned JS dependencies via
 updates.config.js (#29696)

Split out from https://github.com/go-gitea/gitea/pull/29684. This
configures the [`updates`](https://github.com/silverwind/updates) module
to exclude these modules for reasons stated in the comments.
---
 updates.config.js | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 updates.config.js

diff --git a/updates.config.js b/updates.config.js
new file mode 100644
index 0000000000..11908dea8e
--- /dev/null
+++ b/updates.config.js
@@ -0,0 +1,6 @@
+export default {
+  exclude: [
+    '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
+    'eslint-plugin-array-func', // need to migrate to eslint flat config first
+  ],
+};

From 66edc888ee8b2f77a6f11139acd2d03c561ad5ef Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 13 Mar 2024 18:07:53 +0800
Subject: [PATCH 354/679] Move fork router functions to a standalone file
 (#29756)

To reduce the pull.go file's size.
---
 routers/web/repo/fork.go | 238 +++++++++++++++++++++++++++++++++++++++
 routers/web/repo/pull.go | 214 -----------------------------------
 2 files changed, 238 insertions(+), 214 deletions(-)
 create mode 100644 routers/web/repo/fork.go

diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
new file mode 100644
index 0000000000..60e37476ee
--- /dev/null
+++ b/routers/web/repo/fork.go
@@ -0,0 +1,238 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+	"net/url"
+
+	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	"code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/forms"
+	repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplFork base.TplName = "repo/pulls/fork"
+)
+
+func getForkRepository(ctx *context.Context) *repo_model.Repository {
+	forkRepo := ctx.Repo.Repository
+	if ctx.Written() {
+		return nil
+	}
+
+	if forkRepo.IsEmpty {
+		log.Trace("Empty repository %-v", forkRepo)
+		ctx.NotFound("getForkRepository", nil)
+		return nil
+	}
+
+	if err := forkRepo.LoadOwner(ctx); err != nil {
+		ctx.ServerError("LoadOwner", err)
+		return nil
+	}
+
+	ctx.Data["repo_name"] = forkRepo.Name
+	ctx.Data["description"] = forkRepo.Description
+	ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
+	canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
+
+	ctx.Data["ForkRepo"] = forkRepo
+
+	ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
+	if err != nil {
+		ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
+		return nil
+	}
+	var orgs []*organization.Organization
+	for _, org := range ownedOrgs {
+		if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
+			orgs = append(orgs, org)
+		}
+	}
+
+	traverseParentRepo := forkRepo
+	for {
+		if ctx.Doer.ID == traverseParentRepo.OwnerID {
+			canForkToUser = false
+		} else {
+			for i, org := range orgs {
+				if org.ID == traverseParentRepo.OwnerID {
+					orgs = append(orgs[:i], orgs[i+1:]...)
+					break
+				}
+			}
+		}
+
+		if !traverseParentRepo.IsFork {
+			break
+		}
+		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+		if err != nil {
+			ctx.ServerError("GetRepositoryByID", err)
+			return nil
+		}
+	}
+
+	ctx.Data["CanForkToUser"] = canForkToUser
+	ctx.Data["Orgs"] = orgs
+
+	if canForkToUser {
+		ctx.Data["ContextUser"] = ctx.Doer
+	} else if len(orgs) > 0 {
+		ctx.Data["ContextUser"] = orgs[0]
+	} else {
+		ctx.Data["CanForkRepo"] = false
+		ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
+		return nil
+	}
+
+	branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
+		RepoID: ctx.Repo.Repository.ID,
+		ListOptions: db.ListOptions{
+			ListAll: true,
+		},
+		IsDeletedBranch: optional.Some(false),
+		// Add it as the first option
+		ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
+	})
+	if err != nil {
+		ctx.ServerError("FindBranchNames", err)
+		return nil
+	}
+	ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
+
+	return forkRepo
+}
+
+// Fork render repository fork page
+func Fork(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("new_fork")
+
+	if ctx.Doer.CanForkRepo() {
+		ctx.Data["CanForkRepo"] = true
+	} else {
+		maxCreationLimit := ctx.Doer.MaxCreationLimit()
+		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+		ctx.Flash.Error(msg, true)
+	}
+
+	getForkRepository(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.HTML(http.StatusOK, tplFork)
+}
+
+// ForkPost response for forking a repository
+func ForkPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.CreateRepoForm)
+	ctx.Data["Title"] = ctx.Tr("new_fork")
+	ctx.Data["CanForkRepo"] = true
+
+	ctxUser := checkContextUser(ctx, form.UID)
+	if ctx.Written() {
+		return
+	}
+
+	forkRepo := getForkRepository(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.Data["ContextUser"] = ctxUser
+
+	if ctx.HasError() {
+		ctx.HTML(http.StatusOK, tplFork)
+		return
+	}
+
+	var err error
+	traverseParentRepo := forkRepo
+	for {
+		if ctxUser.ID == traverseParentRepo.OwnerID {
+			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+			return
+		}
+		repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
+		if repo != nil {
+			ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+			return
+		}
+		if !traverseParentRepo.IsFork {
+			break
+		}
+		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+		if err != nil {
+			ctx.ServerError("GetRepositoryByID", err)
+			return
+		}
+	}
+
+	// Check if user is allowed to create repo's on the organization.
+	if ctxUser.IsOrganization() {
+		isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
+		if err != nil {
+			ctx.ServerError("CanCreateOrgRepo", err)
+			return
+		} else if !isAllowedToFork {
+			ctx.Error(http.StatusForbidden)
+			return
+		}
+	}
+
+	repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
+		BaseRepo:     forkRepo,
+		Name:         form.RepoName,
+		Description:  form.Description,
+		SingleBranch: form.ForkSingleBranch,
+	})
+	if err != nil {
+		ctx.Data["Err_RepoName"] = true
+		switch {
+		case repo_model.IsErrReachLimitOfRepo(err):
+			maxCreationLimit := ctxUser.MaxCreationLimit()
+			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+			ctx.RenderWithErr(msg, tplFork, &form)
+		case repo_model.IsErrRepoAlreadyExist(err):
+			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+		case repo_model.IsErrRepoFilesAlreadyExist(err):
+			switch {
+			case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
+			case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
+			case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
+			default:
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
+			}
+		case db.IsErrNameReserved(err):
+			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
+		case db.IsErrNamePatternNotAllowed(err):
+			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+		case errors.Is(err, user_model.ErrBlockedUser):
+			ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
+		default:
+			ctx.ServerError("ForkPost", err)
+		}
+		return
+	}
+
+	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
+	ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index ed063715e5..447781602d 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -10,7 +10,6 @@ import (
 	"fmt"
 	"html"
 	"net/http"
-	"net/url"
 	"strconv"
 	"strings"
 	"time"
@@ -20,7 +19,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	pull_model "code.gitea.io/gitea/models/pull"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -32,9 +30,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	issue_template "code.gitea.io/gitea/modules/issue/template"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
@@ -53,7 +49,6 @@ import (
 )
 
 const (
-	tplFork        base.TplName = "repo/pulls/fork"
 	tplCompareDiff base.TplName = "repo/diff/compare"
 	tplPullCommits base.TplName = "repo/pulls/commits"
 	tplPullFiles   base.TplName = "repo/pulls/files"
@@ -112,215 +107,6 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
 	return repo
 }
 
-func getForkRepository(ctx *context.Context) *repo_model.Repository {
-	forkRepo := ctx.Repo.Repository
-	if ctx.Written() {
-		return nil
-	}
-
-	if forkRepo.IsEmpty {
-		log.Trace("Empty repository %-v", forkRepo)
-		ctx.NotFound("getForkRepository", nil)
-		return nil
-	}
-
-	if err := forkRepo.LoadOwner(ctx); err != nil {
-		ctx.ServerError("LoadOwner", err)
-		return nil
-	}
-
-	ctx.Data["repo_name"] = forkRepo.Name
-	ctx.Data["description"] = forkRepo.Description
-	ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
-	canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
-
-	ctx.Data["ForkRepo"] = forkRepo
-
-	ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
-	if err != nil {
-		ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
-		return nil
-	}
-	var orgs []*organization.Organization
-	for _, org := range ownedOrgs {
-		if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
-			orgs = append(orgs, org)
-		}
-	}
-
-	traverseParentRepo := forkRepo
-	for {
-		if ctx.Doer.ID == traverseParentRepo.OwnerID {
-			canForkToUser = false
-		} else {
-			for i, org := range orgs {
-				if org.ID == traverseParentRepo.OwnerID {
-					orgs = append(orgs[:i], orgs[i+1:]...)
-					break
-				}
-			}
-		}
-
-		if !traverseParentRepo.IsFork {
-			break
-		}
-		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
-		if err != nil {
-			ctx.ServerError("GetRepositoryByID", err)
-			return nil
-		}
-	}
-
-	ctx.Data["CanForkToUser"] = canForkToUser
-	ctx.Data["Orgs"] = orgs
-
-	if canForkToUser {
-		ctx.Data["ContextUser"] = ctx.Doer
-	} else if len(orgs) > 0 {
-		ctx.Data["ContextUser"] = orgs[0]
-	} else {
-		ctx.Data["CanForkRepo"] = false
-		ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
-		return nil
-	}
-
-	branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: ctx.Repo.Repository.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
-		IsDeletedBranch: optional.Some(false),
-		// Add it as the first option
-		ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
-	})
-	if err != nil {
-		ctx.ServerError("FindBranchNames", err)
-		return nil
-	}
-	ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
-
-	return forkRepo
-}
-
-// Fork render repository fork page
-func Fork(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("new_fork")
-
-	if ctx.Doer.CanForkRepo() {
-		ctx.Data["CanForkRepo"] = true
-	} else {
-		maxCreationLimit := ctx.Doer.MaxCreationLimit()
-		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
-		ctx.Flash.Error(msg, true)
-	}
-
-	getForkRepository(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	ctx.HTML(http.StatusOK, tplFork)
-}
-
-// ForkPost response for forking a repository
-func ForkPost(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.CreateRepoForm)
-	ctx.Data["Title"] = ctx.Tr("new_fork")
-	ctx.Data["CanForkRepo"] = true
-
-	ctxUser := checkContextUser(ctx, form.UID)
-	if ctx.Written() {
-		return
-	}
-
-	forkRepo := getForkRepository(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	ctx.Data["ContextUser"] = ctxUser
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, tplFork)
-		return
-	}
-
-	var err error
-	traverseParentRepo := forkRepo
-	for {
-		if ctxUser.ID == traverseParentRepo.OwnerID {
-			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
-			return
-		}
-		repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
-		if repo != nil {
-			ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
-			return
-		}
-		if !traverseParentRepo.IsFork {
-			break
-		}
-		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
-		if err != nil {
-			ctx.ServerError("GetRepositoryByID", err)
-			return
-		}
-	}
-
-	// Check if user is allowed to create repo's on the organization.
-	if ctxUser.IsOrganization() {
-		isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
-		if err != nil {
-			ctx.ServerError("CanCreateOrgRepo", err)
-			return
-		} else if !isAllowedToFork {
-			ctx.Error(http.StatusForbidden)
-			return
-		}
-	}
-
-	repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
-		BaseRepo:     forkRepo,
-		Name:         form.RepoName,
-		Description:  form.Description,
-		SingleBranch: form.ForkSingleBranch,
-	})
-	if err != nil {
-		ctx.Data["Err_RepoName"] = true
-		switch {
-		case repo_model.IsErrReachLimitOfRepo(err):
-			maxCreationLimit := ctxUser.MaxCreationLimit()
-			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
-			ctx.RenderWithErr(msg, tplFork, &form)
-		case repo_model.IsErrRepoAlreadyExist(err):
-			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
-		case repo_model.IsErrRepoFilesAlreadyExist(err):
-			switch {
-			case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
-			case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
-			case setting.Repository.AllowDeleteOfUnadoptedRepositories:
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
-			default:
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
-			}
-		case db.IsErrNameReserved(err):
-			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
-		case db.IsErrNamePatternNotAllowed(err):
-			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
-		case errors.Is(err, user_model.ErrBlockedUser):
-			ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
-		default:
-			ctx.ServerError("ForkPost", err)
-		}
-		return
-	}
-
-	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
-	ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
-}
-
 func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {

From 85c59d6c21e10ef9d3ccf11713548f50e47e920f Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Wed, 13 Mar 2024 11:34:58 +0100
Subject: [PATCH 355/679] Use relative links for commits, mentions, and issues
 in markdown (#29427)

Fixes #29404

Use relative links for
- commits
- mentions
- issues

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 modules/markup/html.go                   | 12 +++++------
 modules/markup/html_internal_test.go     |  1 +
 modules/markup/html_test.go              |  9 +++++---
 modules/markup/markdown/markdown_test.go |  6 +++---
 modules/markup/renderer.go               | 14 ++++++++++---
 modules/templates/util_render_test.go    | 14 ++++++-------
 routers/common/markup.go                 |  6 ++++--
 services/mailer/mail.go                  |  3 ++-
 services/mailer/mail_test.go             | 26 +++++++++++++++++++++++-
 9 files changed, 65 insertions(+), 26 deletions(-)

diff --git a/modules/markup/html.go b/modules/markup/html.go
index 56e1a1c54e..21bd6206e0 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -609,7 +609,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
 		if ok && strings.Contains(mention, "/") {
 			mentionOrgAndTeam := strings.Split(mention, "/")
 			if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
-				replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
+				replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
 				node = node.NextSibling.NextSibling
 				start = 0
 				continue
@@ -620,7 +620,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
 		mentionedUsername := mention[1:]
 
 		if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
-			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention"))
+			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention"))
 			node = node.NextSibling.NextSibling
 		} else {
 			node = node.NextSibling
@@ -898,9 +898,9 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 				path = "pulls"
 			}
 			if ref.Owner == "" {
-				link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue")
+				link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue")
 			} else {
-				link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
+				link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
 			}
 		}
 
@@ -939,7 +939,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
 		}
 
 		reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
-		link := createLink(util.URLJoin(setting.AppSubURL, ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
+		link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
 
 		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
 		node = node.NextSibling.NextSibling
@@ -1166,7 +1166,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
 			continue
 		}
 
-		link := util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
+		link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
 		replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
 		start = 0
 		node = node.NextSibling.NextSibling
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 93ba9d7667..e313be7040 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -287,6 +287,7 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
 }
 
 func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
+	ctx.Links.AbsolutePrefix = true
 	if ctx.Links.Base == "" {
 		ctx.Links.Base = TestRepoURL
 	}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index ccb63c6bab..55de65d196 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -43,7 +43,8 @@ func TestRender_Commits(t *testing.T) {
 			Ctx:          git.DefaultContext,
 			RelativePath: ".md",
 			Links: markup.Links{
-				Base: markup.TestRepoURL,
+				AbsolutePrefix: true,
+				Base:           markup.TestRepoURL,
 			},
 			Metas: localMetas,
 		}, input)
@@ -96,7 +97,8 @@ func TestRender_CrossReferences(t *testing.T) {
 			Ctx:          git.DefaultContext,
 			RelativePath: "a.md",
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				AbsolutePrefix: true,
+				Base:           setting.AppSubURL,
 			},
 			Metas: localMetas,
 		}, input)
@@ -588,7 +590,8 @@ func TestPostProcess_RenderDocument(t *testing.T) {
 		err := markup.PostProcess(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: "https://example.com",
+				AbsolutePrefix: true,
+				Base:           "https://example.com",
 			},
 			Metas: localMetas,
 		}, strings.NewReader(input), &res)
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index dbf95e5e62..a12bd4f9e7 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -130,11 +130,11 @@ func testAnswers(baseURLContent, baseURLImages string) []string {
 <li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
 <li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li>
 </ul>
-<p>See commit <a href="http://localhost:3000/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
+<p>See commit <a href="/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
 <p>Ideas and codes</p>
 <ul>
-<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
-<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li>
+<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
+<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li>
 <li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li>
 <li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
 <li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 5a7adcc553..0f0bf55740 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -82,9 +82,17 @@ type RenderContext struct {
 }
 
 type Links struct {
-	Base       string
-	BranchPath string
-	TreePath   string
+	AbsolutePrefix bool
+	Base           string
+	BranchPath     string
+	TreePath       string
+}
+
+func (l *Links) Prefix() string {
+	if l.AbsolutePrefix {
+		return setting.AppURL
+	}
+	return setting.AppSubURL
 }
 
 func (l *Links) HasBranchInfo() bool {
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index 8648967d38..15aee8912d 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -117,21 +117,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
 com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 <span class="emoji" aria-label="thumbs up">👍</span>
 <a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
-<a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> test
-<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
+<a href="/mention-user" class="mention">@mention-user</a> test
+<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
   space`
 
 	assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
 }
 
 func TestRenderCommitMessage(t *testing.T) {
-	expected := `space <a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>  `
+	expected := `space <a href="/mention-user" class="mention">@mention-user</a>  `
 
 	assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas))
 }
 
 func TestRenderCommitMessageLinkSubject(t *testing.T) {
-	expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>`
+	expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" class="mention">@mention-user</a>`
 
 	assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas))
 }
@@ -155,14 +155,14 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 <span class="emoji" aria-label="thumbs up">👍</span>
 mail@domain.com
 @mention-user test
-<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
+<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
   space  
 `
 	assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
 }
 
 func TestRenderMarkdownToHtml(t *testing.T) {
-	expected := `<p>space <a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a><br/>
+	expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
 /just/a/path.bin
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
 <a href="/file.bin" rel="nofollow">local link</a>
@@ -179,7 +179,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
 com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 <span class="emoji" aria-label="thumbs up">👍</span>
 <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
-<a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a> test
+<a href="/mention-user" rel="nofollow">@mention-user</a> test
 #123
 space</p>
 `
diff --git a/routers/common/markup.go b/routers/common/markup.go
index 7819ee7227..2d5638ef61 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -34,7 +34,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 		if err := markdown.RenderRaw(&markup.RenderContext{
 			Ctx: ctx,
 			Links: markup.Links{
-				Base: urlPrefix,
+				AbsolutePrefix: true,
+				Base:           urlPrefix,
 			},
 		}, strings.NewReader(text), ctx.Resp); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
@@ -79,7 +80,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 	if err := markup.Render(&markup.RenderContext{
 		Ctx: ctx,
 		Links: markup.Links{
-			Base: urlPrefix,
+			AbsolutePrefix: true,
+			Base:           urlPrefix,
 		},
 		Metas:        meta,
 		IsWiki:       wiki,
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 38973ea935..a63ba7a52a 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -222,7 +222,8 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 	body, err := markdown.RenderString(&markup.RenderContext{
 		Ctx: ctx,
 		Links: markup.Links{
-			Base: ctx.Issue.Repo.HTMLURL(),
+			AbsolutePrefix: true,
+			Base:           ctx.Issue.Repo.HTMLURL(),
 		},
 		Metas: ctx.Issue.Repo.ComposeMetas(ctx),
 	}, ctx.Content)
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index e300aeccb0..d87c57ffe7 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -8,6 +8,8 @@ import (
 	"context"
 	"fmt"
 	"html/template"
+	"io"
+	"mime/quotedprintable"
 	"regexp"
 	"strings"
 	"testing"
@@ -19,6 +21,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/stretchr/testify/assert"
@@ -67,6 +70,12 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
 func TestComposeIssueCommentMessage(t *testing.T) {
 	doer, _, issue, comment := prepareMailerTest(t)
 
+	markup.Init(&markup.ProcessorHelper{
+		IsUsernameMentionable: func(ctx context.Context, username string) bool {
+			return username == doer.Name
+		},
+	})
+
 	setting.IncomingEmail.Enabled = true
 	defer func() { setting.IncomingEmail.Enabled = false }()
 
@@ -77,7 +86,8 @@ func TestComposeIssueCommentMessage(t *testing.T) {
 	msgs, err := composeIssueCommentMessages(&mailCommentContext{
 		Context: context.TODO(), // TODO: use a correct context
 		Issue:   issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
-		Content: "test body", Comment: comment,
+		Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
+		Comment: comment,
 	}, "en-US", recipients, false, "issue comment")
 	assert.NoError(t, err)
 	assert.Len(t, msgs, 2)
@@ -96,6 +106,20 @@ func TestComposeIssueCommentMessage(t *testing.T) {
 	assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match")
 	assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetHeader("List-Post")[0])
 	assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto
+
+	var buf bytes.Buffer
+	gomailMsg.WriteTo(&buf)
+
+	b, err := io.ReadAll(quotedprintable.NewReader(&buf))
+	assert.NoError(t, err)
+
+	// text/plain
+	assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL()))
+	assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL()))
+
+	// text/html
+	assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL()))
+	assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL()))
 }
 
 func TestComposeIssueMessage(t *testing.T) {

From 3e94ac5c7c6751919453fdb66ba3472e2793759e Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 13 Mar 2024 21:32:30 +0800
Subject: [PATCH 356/679] Improve QueryEscape helper function (#29768)

Make it return "template.URL" to follow Golang template's context
auto-escaping.
---
 modules/templates/helper.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 0997239a55..2452064749 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap {
 		"SafeHTML":     SafeHTML,
 		"HTMLFormat":   HTMLFormat,
 		"HTMLEscape":   HTMLEscape,
-		"QueryEscape":  url.QueryEscape,
+		"QueryEscape":  QueryEscape,
 		"JSEscape":     JSEscapeSafe,
 		"SanitizeHTML": SanitizeHTML,
 		"URLJoin":      util.URLJoin,
@@ -226,6 +226,10 @@ func JSEscapeSafe(s string) template.HTML {
 	return template.HTML(template.JSEscapeString(s))
 }
 
+func QueryEscape(s string) template.URL {
+	return template.URL(url.QueryEscape(s))
+}
+
 // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
 func DotEscape(raw string) string {
 	return strings.ReplaceAll(raw, ".", "\u200d.\u200d")

From e01b0014de5b732181ac42c03a77c21219f88c6a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 13 Mar 2024 21:44:46 +0800
Subject: [PATCH 357/679] Improve a11y document and dropdown item (#29753)

Co-authored-by: silverwind <me@silverwind.io>
---
 docs/content/contributing/guidelines-frontend.zh-cn.md |  2 +-
 web_src/js/modules/fomantic/aria.md                    | 10 +++++-----
 web_src/js/modules/fomantic/dropdown.js                |  2 +-
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index ace0d97f49..b5fb8964b3 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -53,7 +53,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 ### 可访问性 / ARIA
 
 在历史上,Gitea大量使用了可访问性不友好的框架 Fomantic UI。
-Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`aria.md`),
+Gitea 使用一些补丁使 Fomantic UI 更具可访问性(参见 `aria.md`),
 但仍然存在许多问题需要大量的工作和时间来修复。
 
 ### 框架使用
diff --git a/web_src/js/modules/fomantic/aria.md b/web_src/js/modules/fomantic/aria.md
index a32d15f46f..f639233346 100644
--- a/web_src/js/modules/fomantic/aria.md
+++ b/web_src/js/modules/fomantic/aria.md
@@ -2,10 +2,10 @@
 
 This document is used as aria/accessibility(a11y) reference for future developers.
 
-There are a lot of a11y problems in the Fomantic UI library. This `aria.js` is used
-as a workaround to make the UI more accessible.
+There are a lot of a11y problems in the Fomantic UI library. Files in 
+`web_src/js/modules/fomantic/` are used as a workaround to make the UI more accessible.
 
-The `aria.js` is designed to avoid touching the official Fomantic UI library,
+The aria-related code is designed to avoid touching the official Fomantic UI library,
 and to be as independent as possible, so it can be easily modified/removed in the future.
 
 To test the aria/accessibility with screen readers, developers can use the following steps:
@@ -14,7 +14,7 @@ To test the aria/accessibility with screen readers, developers can use the follo
   * Press `Command + F5` to turn on VoiceOver.
   * Try to operate the UI with keyboard-only.
   * Use Tab/Shift+Tab to switch focus between elements.
-  * Arrow keys to navigate between menu/combobox items (only aria-active, not really focused).
+  * Arrow keys (Option+Up/Down) to navigate between menu/combobox items (only aria-active, not really focused).
   * Press Enter to trigger the aria-active element.
 * On Android, you can use TalkBack.
   * Go to Settings -> Accessibility -> TalkBack, turn it on.
@@ -75,7 +75,7 @@ Fomantic Dropdown is designed to be used for many purposes:
 Fomantic Dropdown requires that the focus must be on its primary element.
 If the focus changes, it hides or panics.
 
-At the moment, `aria.js` only tries to partially resolve the a11y problems for dropdowns with items.
+At the moment, the aria-related code only tries to partially resolve the a11y problems for dropdowns with items.
 
 There are different solutions:
 
diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
index c053256dd5..caba8a2f28 100644
--- a/web_src/js/modules/fomantic/dropdown.js
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -38,7 +38,7 @@ function updateMenuItem(dropdown, item) {
   if (!item.id) item.id = generateAriaId();
   item.setAttribute('role', dropdown[ariaPatchKey].listItemRole);
   item.setAttribute('tabindex', '-1');
-  for (const a of item.querySelectorAll('a')) a.setAttribute('tabindex', '-1');
+  for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1');
 }
 
 // make the label item and its "delete icon" has correct aria attributes

From df60dbfb9918081962614d063485337fb42e0ee7 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 14 Mar 2024 00:24:34 +0800
Subject: [PATCH 358/679] Fix incorrect locale Tr for gpg command (#29754)

---
 options/locale/locale_en-US.ini       | 1 -
 templates/user/settings/keys_gpg.tmpl | 4 ++--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index afd613af59..836703a480 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -804,7 +804,6 @@ gpg_invalid_token_signature = The provided GPG key, signature and token do not m
 gpg_token_required = You must provide a signature for the below token
 gpg_token = Token
 gpg_token_help = You can generate a signature using:
-gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature = Armored GPG signature
 key_signature_gpg_placeholder = Begins with '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success = GPG key "%s" has been verified.
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index 0dd0059511..e57658b197 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -22,7 +22,7 @@
 					<input readonly="" value="{{.TokenToSign}}">
 					<div class="help">
 						<p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p>
-						<p><code>{{ctx.Locale.Tr "settings.gpg_token_code" .TokenToSign .PaddedKeyID}}</code></p>
+						<p><code>{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}</code></p>
 					</div>
 				</div>
 				<div class="field">
@@ -90,7 +90,7 @@
 							<input readonly="" value="{{$.TokenToSign}}">
 							<div class="help">
 								<p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p>
-								<p><code>{{ctx.Locale.Tr "settings.gpg_token_code" $.TokenToSign .PaddedKeyID}}</code></p>
+								<p><code>{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}</code></p>
 							</div>
 							<br>
 						</div>

From 712e19fa6fbf2f1a5b0a471782d38a7d91e538ae Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 13 Mar 2024 19:00:38 +0100
Subject: [PATCH 359/679] fix missed RenderLabel change in card template
 (#29772)

regression of #29680
close  #29770

PS: it would be nice to have a linter that is able to check template
helpers ...
---
 templates/repo/issue/card.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index ff635c736a..b461c5fc98 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -61,7 +61,7 @@
 	{{if or .Labels .Assignees}}
 	<div class="extra content labels-list gt-p-0 gt-pt-2">
 		{{range .Labels}}
-			<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx .}}</a>
+			<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a>
 		{{end}}
 		<div class="right floated">
 			{{range .Assignees}}

From 83ba882bab7e1545fe02cd41f554ae41b83a6040 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 14 Mar 2024 03:46:15 +0800
Subject: [PATCH 360/679] Fix possible NPE in ToPullReviewList (#29759)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/convert/pull_review.go      |  2 +-
 services/convert/pull_review_test.go | 52 ++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 1 deletion(-)
 create mode 100644 services/convert/pull_review_test.go

diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go
index aa7ad68a47..29a5ab7466 100644
--- a/services/convert/pull_review.go
+++ b/services/convert/pull_review.go
@@ -66,7 +66,7 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user
 	result := make([]*api.PullReview, 0, len(rl))
 	for i := range rl {
 		// show pending reviews only for the user who created them
-		if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) {
+		if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || (!doer.IsAdmin && doer.ID != rl[i].ReviewerID)) {
 			continue
 		}
 		r, err := ToPullReview(ctx, rl[i], doer)
diff --git a/services/convert/pull_review_test.go b/services/convert/pull_review_test.go
new file mode 100644
index 0000000000..6886950280
--- /dev/null
+++ b/services/convert/pull_review_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_ToPullReview(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 6})
+	assert.EqualValues(t, reviewer.ID, review.ReviewerID)
+	assert.EqualValues(t, issues_model.ReviewTypePending, review.Type)
+
+	reviewList := []*issues_model.Review{review}
+
+	t.Run("Anonymous User", func(t *testing.T) {
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, nil)
+		assert.NoError(t, err)
+		assert.Empty(t, prList)
+	})
+
+	t.Run("Reviewer Himself", func(t *testing.T) {
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, reviewer)
+		assert.NoError(t, err)
+		assert.Len(t, prList, 1)
+	})
+
+	t.Run("Other User", func(t *testing.T) {
+		user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, user4)
+		assert.NoError(t, err)
+		assert.Len(t, prList, 0)
+	})
+
+	t.Run("Admin User", func(t *testing.T) {
+		adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, adminUser)
+		assert.NoError(t, err)
+		assert.Len(t, prList, 1)
+	})
+}

From 43de021ac1ca017212ec75fd88a8a80a9db27c4c Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 14 Mar 2024 09:10:51 +0800
Subject: [PATCH 361/679] Add test for webhook (#29755)

Follow #29690
---
 modules/util/util.go             |  9 ++++
 services/webhook/deliver_test.go | 78 ++++++++++++--------------------
 2 files changed, 38 insertions(+), 49 deletions(-)

diff --git a/modules/util/util.go b/modules/util/util.go
index 5c75158196..c94fb91047 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -212,3 +212,12 @@ func ToFloat64(number any) (float64, error) {
 func ToPointer[T any](val T) *T {
 	return &val
 }
+
+// IfZero returns "def" if "v" is a zero value, otherwise "v"
+func IfZero[T comparable](v, def T) T {
+	var zero T
+	if v == zero {
+		return def
+	}
+	return v
+}
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index 24924ab214..85de1f9904 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -18,6 +18,7 @@ import (
 	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/hostmatcher"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/stretchr/testify/assert"
@@ -226,49 +227,29 @@ func TestWebhookDeliverSpecificTypes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	type hookCase struct {
-		gotBody chan []byte
+		gotBody    chan []byte
+		httpMethod string // default to POST
 	}
 
-	cases := map[string]hookCase{
-		webhook_module.SLACK: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.DISCORD: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.DINGTALK: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.TELEGRAM: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.MSTEAMS: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.FEISHU: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.MATRIX: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.WECHATWORK: {
-			gotBody: make(chan []byte, 1),
-		},
-		webhook_module.PACKAGIST: {
-			gotBody: make(chan []byte, 1),
-		},
+	cases := map[string]*hookCase{
+		webhook_module.SLACK:      {},
+		webhook_module.DISCORD:    {},
+		webhook_module.DINGTALK:   {},
+		webhook_module.TELEGRAM:   {},
+		webhook_module.MSTEAMS:    {},
+		webhook_module.FEISHU:     {},
+		webhook_module.MATRIX:     {httpMethod: "PUT"},
+		webhook_module.WECHATWORK: {},
+		webhook_module.PACKAGIST:  {},
 	}
 
 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		typ := strings.Split(r.URL.Path, "/")[1] // URL: "/{webhook_type}/other-path"
 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)
-
-		typ := strings.Split(r.URL.Path, "/")[1] // take first segment (after skipping leading slash)
-		hc := cases[typ]
-		require.NotNil(t, hc.gotBody, r.URL.Path)
-		body, err := io.ReadAll(r.Body)
-		assert.NoError(t, err)
-		w.WriteHeader(200)
-		hc.gotBody <- body
+		assert.Equal(t, util.IfZero(cases[typ].httpMethod, "POST"), r.Method, "webhook test request %q", r.URL.Path)
+		body, _ := io.ReadAll(r.Body) // read request and send it back to the test by testcase's chan
+		cases[typ].gotBody <- body
+		w.WriteHeader(http.StatusNoContent)
 	}))
 	t.Cleanup(s.Close)
 
@@ -276,19 +257,17 @@ func TestWebhookDeliverSpecificTypes(t *testing.T) {
 	data, err := p.JSONPayload()
 	assert.NoError(t, err)
 
-	for typ, hc := range cases {
-		typ := typ
-		hc := hc
+	for typ := range cases {
+		cases[typ].gotBody = make(chan []byte, 1)
+		typ := typ // TODO: remove this workaround when Go >= 1.22
 		t.Run(typ, func(t *testing.T) {
 			t.Parallel()
 			hook := &webhook_model.Webhook{
-				RepoID:      3,
-				IsActive:    true,
-				Type:        typ,
-				URL:         s.URL + "/" + typ,
-				HTTPMethod:  "POST",
-				ContentType: 0, // set to 0 so that falling back to default request fails with "invalid content type"
-				Meta:        "{}",
+				RepoID:   3,
+				IsActive: true,
+				Type:     typ,
+				URL:      s.URL + "/" + typ,
+				Meta:     "{}",
 			}
 			assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
 
@@ -304,10 +283,11 @@ func TestWebhookDeliverSpecificTypes(t *testing.T) {
 			assert.NotNil(t, hookTask)
 
 			assert.NoError(t, Deliver(context.Background(), hookTask))
+
 			select {
-			case gotBody := <-hc.gotBody:
+			case gotBody := <-cases[typ].gotBody:
 				assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload")
-				assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "request body was not saved")
+				assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "delivered webhook payload doesn't match saved request")
 			case <-time.After(5 * time.Second):
 				t.Fatal("waited to long for request to happen")
 			}

From 2da13675c0cfdc531044553636c3b74f2fda3eb4 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Thu, 14 Mar 2024 10:37:15 +0900
Subject: [PATCH 362/679] Fix incorrect menu/link on webhook edit page (#29709)

Fix #29699

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 routers/web/repo/setting/webhook.go    |  1 +
 tests/integration/repo_webhook_test.go | 41 ++++++++++++++++++++++++++
 2 files changed, 42 insertions(+)
 create mode 100644 tests/integration/repo_webhook_test.go

diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index c8e621fac8..1a3549fea4 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -588,6 +588,7 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
 		return nil, nil
 	}
 	ctx.Data["BaseLink"] = orCtx.Link
+	ctx.Data["BaseLinkNew"] = orCtx.LinkNew
 
 	var w *webhook.Webhook
 	if orCtx.RepoID > 0 {
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
new file mode 100644
index 0000000000..ef44a9e2d0
--- /dev/null
+++ b/tests/integration/repo_webhook_test.go
@@ -0,0 +1,41 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"strings"
+	"testing"
+
+	"code.gitea.io/gitea/tests"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewWebHookLink(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	session := loginUser(t, "user2")
+
+	baseurl := "/user2/repo1/settings/hooks"
+	tests := []string{
+		// webhook list page
+		baseurl,
+		// new webhook page
+		baseurl + "/gitea/new",
+		// edit webhook page
+		baseurl + "/1",
+	}
+
+	for _, url := range tests {
+		resp := session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK)
+		htmlDoc := NewHTMLParser(t, resp.Body)
+		menus := htmlDoc.doc.Find(".ui.top.attached.header .ui.dropdown .menu a")
+		menus.Each(func(i int, menu *goquery.Selection) {
+			url, exist := menu.Attr("href")
+			assert.True(t, exist)
+			assert.True(t, strings.HasPrefix(url, baseurl))
+		})
+	}
+}

From bbef5fc5c304d8e9cef110bffa44a0e4bcf028fb Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 14 Mar 2024 03:23:58 +0100
Subject: [PATCH 363/679] Fix `make generate-swagger` in go 1.22 (#29780)

Fixes: https://github.com/go-gitea/gitea/issues/29664. No release
available for https://github.com/go-swagger/go-swagger/issues/3070 so
let's depend on latest commit hash. Output is the same as before for me.
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 5ab8655c2f..cedc4b4198 100644
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,7 @@ GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1
 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1
-SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5
+SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285
 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0
 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3

From e79a807a8461a73bd66146d816f635b66e198c89 Mon Sep 17 00:00:00 2001
From: coldWater <254244460@qq.com>
Date: Thu, 14 Mar 2024 10:51:55 +0800
Subject: [PATCH 364/679] Refactor markup/csv: don't read all to memory
 (#29760)

---
 modules/markup/csv/csv.go      | 63 ++++++++++++++++++++++++++--------
 modules/markup/csv/csv_test.go | 10 ++++++
 2 files changed, 58 insertions(+), 15 deletions(-)

diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 12458e954a..570c4f4704 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -77,29 +77,62 @@ func writeField(w io.Writer, element, class, field string) error {
 }
 
 // Render implements markup.Renderer
-func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
+func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 	tmpBlock := bufio.NewWriter(output)
+	maxSize := setting.UI.CSV.MaxFileSize
 
-	// FIXME: don't read all to memory
-	rawBytes, err := io.ReadAll(input)
+	if maxSize == 0 {
+		return r.tableRender(ctx, input, tmpBlock)
+	}
+
+	rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
 	if err != nil {
 		return err
 	}
 
-	if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < int64(len(rawBytes)) {
-		if _, err := tmpBlock.WriteString("<pre>"); err != nil {
-			return err
-		}
-		if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil {
-			return err
-		}
-		if _, err := tmpBlock.WriteString("</pre>"); err != nil {
-			return err
-		}
-		return tmpBlock.Flush()
+	if int64(len(rawBytes)) <= maxSize {
+		return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock)
+	}
+	return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock)
+}
+
+func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
+	_, err := tmpBlock.WriteString("<pre>")
+	if err != nil {
+		return err
 	}
 
-	rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, bytes.NewReader(rawBytes))
+	scan := bufio.NewScanner(input)
+	scan.Split(bufio.ScanRunes)
+	for scan.Scan() {
+		switch scan.Text() {
+		case `&`:
+			_, err = tmpBlock.WriteString("&amp;")
+		case `'`:
+			_, err = tmpBlock.WriteString("&#39;") // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
+		case `<`:
+			_, err = tmpBlock.WriteString("&lt;")
+		case `>`:
+			_, err = tmpBlock.WriteString("&gt;")
+		case `"`:
+			_, err = tmpBlock.WriteString("&#34;") // "&#34;" is shorter than "&quot;".
+		default:
+			_, err = tmpBlock.Write(scan.Bytes())
+		}
+		if err != nil {
+			return err
+		}
+	}
+
+	_, err = tmpBlock.WriteString("</pre>")
+	if err != nil {
+		return err
+	}
+	return tmpBlock.Flush()
+}
+
+func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error {
+	rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
 	if err != nil {
 		return err
 	}
diff --git a/modules/markup/csv/csv_test.go b/modules/markup/csv/csv_test.go
index 8c07184b21..3d12be477c 100644
--- a/modules/markup/csv/csv_test.go
+++ b/modules/markup/csv/csv_test.go
@@ -4,6 +4,8 @@
 package markup
 
 import (
+	"bufio"
+	"bytes"
 	"strings"
 	"testing"
 
@@ -29,4 +31,12 @@ func TestRenderCSV(t *testing.T) {
 		assert.NoError(t, err)
 		assert.EqualValues(t, v, buf.String())
 	}
+
+	t.Run("fallbackRender", func(t *testing.T) {
+		var buf bytes.Buffer
+		err := render.fallbackRender(strings.NewReader("1,<a>\n2,<b>"), bufio.NewWriter(&buf))
+		assert.NoError(t, err)
+		want := "<pre>1,&lt;a&gt;\n2,&lt;b&gt;</pre>"
+		assert.Equal(t, want, buf.String())
+	})
 }

From 7a90e5954f8515329f20ff0e391130e1ee7b8864 Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Thu, 14 Mar 2024 04:18:04 +0100
Subject: [PATCH 365/679] add skip ci support for pull request title (#29774)

Extends #28075 to support [skip ci] inside PR titles.

Close #29265
---
 custom/conf/app.example.ini                   |  2 +-
 .../config-cheat-sheet.en-us.md               |  2 +-
 services/actions/notifier_helper.go           | 10 ++--
 tests/integration/actions_trigger_test.go     | 51 ++++++++++++++++---
 4 files changed, 54 insertions(+), 11 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 17d6cd3a35..b4b4f3a8a2 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2608,7 +2608,7 @@ LEVEL = Info
 ;ENDLESS_TASK_TIMEOUT = 3h
 ;; Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time
 ;ABANDONED_JOB_TIMEOUT = 24h
-;; Strings committers can place inside a commit message to skip executing the corresponding actions workflow
+;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
 ;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 43ec470ad0..04923acdcb 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -1406,7 +1406,7 @@ PROXY_HOSTS = *.github.com
 - `ZOMBIE_TASK_TIMEOUT`: **10m**: Timeout to stop the task which have running status, but haven't been updated for a long time
 - `ENDLESS_TASK_TIMEOUT`: **3h**: Timeout to stop the tasks which have running status and continuous updates, but don't end for a long time
 - `ABANDONED_JOB_TIMEOUT`: **24h**: Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time
-- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message to skip executing the corresponding actions workflow
+- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
 
 `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path.
 For example, `uses: actions/checkout@v4` means `https://github.com/actions/checkout@v4` since the value of `DEFAULT_ACTIONS_URL` is `github`.
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index d84191dca2..fafb6ab40e 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -157,7 +157,7 @@ func notify(ctx context.Context, input *notifyInput) error {
 		return fmt.Errorf("gitRepo.GetCommit: %w", err)
 	}
 
-	if skipWorkflowsForCommit(input, commit) {
+	if skipWorkflows(input, commit) {
 		return nil
 	}
 
@@ -223,8 +223,8 @@ func notify(ctx context.Context, input *notifyInput) error {
 	return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
 }
 
-func skipWorkflowsForCommit(input *notifyInput, commit *git.Commit) bool {
-	// skip workflow runs with a configured skip-ci string in commit message if the event is push or pull_request(_sync)
+func skipWorkflows(input *notifyInput, commit *git.Commit) bool {
+	// skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync)
 	// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
 	skipWorkflowEvents := []webhook_module.HookEventType{
 		webhook_module.HookEventPush,
@@ -233,6 +233,10 @@ func skipWorkflowsForCommit(input *notifyInput, commit *git.Commit) bool {
 	}
 	if slices.Contains(skipWorkflowEvents, input.Event) {
 		for _, s := range setting.Actions.SkipWorkflowStrings {
+			if input.PullRequest != nil && strings.Contains(input.PullRequest.Issue.Title, s) {
+				log.Debug("repo %s: skipped run for pr %v because of %s string", input.Repo.RepoPath(), input.PullRequest.Issue.ID, s)
+				return true
+			}
 			if strings.Contains(commit.CommitMessage, s) {
 				log.Debug("repo %s with commit %s: skipped run because of %s string", input.Repo.RepoPath(), commit.ID, s)
 				return true
diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go
index 7744f33e57..9a8bfc5db6 100644
--- a/tests/integration/actions_trigger_test.go
+++ b/tests/integration/actions_trigger_test.go
@@ -20,6 +20,7 @@ import (
 	actions_module "code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
 	files_service "code.gitea.io/gitea/services/repository/files"
@@ -199,6 +200,7 @@ func TestPullRequestTargetEvent(t *testing.T) {
 
 func TestSkipCI(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		session := loginUser(t, "user2")
 		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
 		// create the repo
@@ -209,7 +211,7 @@ func TestSkipCI(t *testing.T) {
 			Gitignores:    "Go",
 			License:       "MIT",
 			Readme:        "Default",
-			DefaultBranch: "main",
+			DefaultBranch: "master",
 			IsPrivate:     false,
 		})
 		assert.NoError(t, err)
@@ -228,12 +230,12 @@ func TestSkipCI(t *testing.T) {
 				{
 					Operation:     "create",
 					TreePath:      ".gitea/workflows/pr.yml",
-					ContentReader: strings.NewReader("name: test\non:\n  push:\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo helloworld\n"),
+					ContentReader: strings.NewReader("name: test\non:\n  push:\n    branches: [master]\n  pull_request:\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo helloworld\n"),
 				},
 			},
 			Message:   "add workflow",
-			OldBranch: "main",
-			NewBranch: "main",
+			OldBranch: "master",
+			NewBranch: "master",
 			Author: &files_service.IdentityOptions{
 				Name:  user2.Name,
 				Email: user2.Email,
@@ -263,8 +265,8 @@ func TestSkipCI(t *testing.T) {
 				},
 			},
 			Message:   fmt.Sprintf("%s add bar", setting.Actions.SkipWorkflowStrings[0]),
-			OldBranch: "main",
-			NewBranch: "main",
+			OldBranch: "master",
+			NewBranch: "master",
 			Author: &files_service.IdentityOptions{
 				Name:  user2.Name,
 				Email: user2.Email,
@@ -283,5 +285,42 @@ func TestSkipCI(t *testing.T) {
 
 		// the commit message contains a configured skip-ci string, so there is still only 1 record
 		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
+
+		// add file to new branch
+		addFileToBranchResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      "test-skip-ci",
+					ContentReader: strings.NewReader("test-skip-ci"),
+				},
+			},
+			Message:   "add test file",
+			OldBranch: "master",
+			NewBranch: "test-skip-ci",
+			Author: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Committer: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Dates: &files_service.CommitDateOptions{
+				Author:    time.Now(),
+				Committer: time.Now(),
+			},
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, addFileToBranchResp)
+
+		resp := testPullCreate(t, session, "user2", "skip-ci", true, "master", "test-skip-ci", "[skip ci] test-skip-ci")
+
+		// check the redirected URL
+		url := test.RedirectURL(resp)
+		assert.Regexp(t, "^/user2/skip-ci/pulls/[0-9]*$", url)
+
+		// the pr title contains a configured skip-ci string, so there is still only 1 record
+		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
 	})
 }

From eb8c34fc367f324226625d39d0487f945269cd73 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 14 Mar 2024 05:30:10 +0100
Subject: [PATCH 366/679] Tweak actions view sticky (#29781)

Add some space when the left side items are sticky due to scrolling the
right side.

<img width="419" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/292e1b03-a071-4744-bb79-e50d109056c8">
---
 web_src/js/components/RepoActionView.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 9641431508..484a3677c5 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -524,7 +524,7 @@ export function initRepositoryActionView() {
   width: 30%;
   max-width: 400px;
   position: sticky;
-  top: 0;
+  top: 12px;
   max-height: 100vh;
   overflow-y: auto;
 }

From 2033eb7c1138a06ea052e6c6e6e5799187519e16 Mon Sep 17 00:00:00 2001
From: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Date: Thu, 14 Mar 2024 12:59:52 +0800
Subject: [PATCH 367/679] Fix lint-swagger warning (#29787)

Caused by: #23106
Fix:
https://github.com/go-gitea/gitea/actions/runs/8274650046/job/22640335697

1. Delete `UserBadgeList` in `options.go`, because it wasn't used. (The
struct defined in `options.go` is the struct used to parse the request
body)
2. Move `BadgeList` struct under `routers/api/v1/swagger` folder which
response should be defined in.
---
 modules/structs/user.go           |  7 -------
 routers/api/v1/swagger/options.go |  3 ---
 routers/api/v1/swagger/user.go    |  7 +++++++
 templates/swagger/v1_json.tmpl    | 17 +----------------
 4 files changed, 8 insertions(+), 26 deletions(-)

diff --git a/modules/structs/user.go b/modules/structs/user.go
index c43558be5d..21ecc1479e 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -132,10 +132,3 @@ type UserBadgeOption struct {
 	// example: ["badge1","badge2"]
 	BadgeSlugs []string `json:"badge_slugs" binding:"Required"`
 }
-
-// BadgeList
-// swagger:response BadgeList
-type BadgeList struct {
-	// in:body
-	Body []Badge `json:"body"`
-}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index e03862d7b9..471e7d9c4e 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -193,7 +193,4 @@ type swaggerParameterBodies struct {
 
 	// in:body
 	UserBadgeOption api.UserBadgeOption
-
-	// in:body
-	UserBadgeList api.BadgeList
 }
diff --git a/routers/api/v1/swagger/user.go b/routers/api/v1/swagger/user.go
index fb6d185ee7..e2ad511d2b 100644
--- a/routers/api/v1/swagger/user.go
+++ b/routers/api/v1/swagger/user.go
@@ -48,3 +48,10 @@ type swaggerResponseUserSettings struct {
 	// in:body
 	Body []api.UserSettings `json:"body"`
 }
+
+// BadgeList
+// swagger:response BadgeList
+type swaggerResponseBadgeList struct {
+	// in:body
+	Body []api.Badge `json:"body"`
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 221b34b7f8..f835df084d 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -17413,21 +17413,6 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
-    "BadgeList": {
-      "description": "BadgeList",
-      "type": "object",
-      "properties": {
-        "body": {
-          "description": "in:body",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/Badge"
-          },
-          "x-go-name": "Body"
-        }
-      },
-      "x-go-package": "code.gitea.io/gitea/modules/structs"
-    },
     "Branch": {
       "description": "Branch represents a repository branch",
       "type": "object",
@@ -24722,7 +24707,7 @@
     "parameterBodies": {
       "description": "parameterBodies",
       "schema": {
-        "$ref": "#/definitions/BadgeList"
+        "$ref": "#/definitions/UserBadgeOption"
       }
     },
     "redirect": {

From 8d979f16928b11779c04c4b547dee3d748cadd51 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 14 Mar 2024 15:40:52 +0800
Subject: [PATCH 368/679] Fix missing translation on milestons (#29785)

Caused by #26569
Fix #29778
---
 templates/user/dashboard/milestones.tmpl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 7cde02291b..fd684fcabf 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -62,8 +62,8 @@
 						</span>
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						<div class="menu">
-							<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.closest_due_date"}}</a>
-							<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.furthest_due_date"}}</a>
+							<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
+							<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
 							<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
 							<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
 							<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>

From 487ac9bf6c239ce897f1a2f6c4321d6f1769a22f Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Thu, 14 Mar 2024 16:44:49 +0800
Subject: [PATCH 369/679] Support GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT env
 (#29788)

It is convenient to skip by setting environment, since it's OK
to use root user in job containers.

It's not a bug, but I want to backport it to v1.21 since it doesn't
break anything.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/setting/setting.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 6e7ce7e67f..13821da44d 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -13,6 +13,7 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/user"
+	"code.gitea.io/gitea/modules/util"
 )
 
 // settings
@@ -158,9 +159,11 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
 func loadRunModeFrom(rootCfg ConfigProvider) {
 	rootSec := rootCfg.Section("")
 	RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
+
 	// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
 	// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
 	unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
+	unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || util.OptionalBoolParse(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
 	RunMode = os.Getenv("GITEA_RUN_MODE")
 	if RunMode == "" {
 		RunMode = rootSec.Key("RUN_MODE").MustString("prod")

From 03753cbc0ff68cc4eee0a65b556e6d86a8f1c63f Mon Sep 17 00:00:00 2001
From: Rafael Heard <rafael.heard@gmail.com>
Date: Thu, 14 Mar 2024 14:20:54 -0400
Subject: [PATCH 370/679] enable tailwind nesting (#29746)

Currently, if you implement native CSS nesting within a Vue component a
warning will appear in the terminal. It states
`Nested CSS was detected, but CSS nesting has not been configured
correctly.
Please enable a CSS nesting plugin *before* Tailwind in your
configuration.` To fix this error we need to enable the built-in
[tailwinds nesting
config](https://tailwindcss.com/docs/using-with-preprocessors#nesting).

Example code to trigger the warning within a vue component:

```CSS
<style>
.example {
  &:hover,
  &:focus-visible {
    color: var(--color-text);
  }

  & svg {
    margin-right: 0.78rem;
  }
}
</style>
```

---------

Co-authored-by: rafh <rafaelheard@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 package-lock.json | 49 ++++++++++++++++++++++++++++++++++++++++++++++-
 package.json      |  1 +
 webpack.config.js |  8 +++++++-
 3 files changed, 56 insertions(+), 2 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 6a6eb4b947..f75e3068b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,7 @@
         "pdfobject": "2.3.0",
         "postcss": "8.4.35",
         "postcss-loader": "8.1.1",
+        "postcss-nesting": "12.1.0",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
         "swagger-ui-dist": "5.11.8",
@@ -528,11 +529,31 @@
         "@csstools/css-tokenizer": "^2.2.3"
       }
     },
+    "node_modules/@csstools/selector-resolve-nested": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz",
+      "integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      },
+      "peerDependencies": {
+        "postcss-selector-parser": "^6.0.13"
+      }
+    },
     "node_modules/@csstools/selector-specificity": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz",
       "integrity": "sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -9816,6 +9837,32 @@
         "postcss": "^8.2.14"
       }
     },
+    "node_modules/postcss-nesting": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.0.tgz",
+      "integrity": "sha512-QOYnosaZ+mlP6plQrAxFw09UUp2Sgtxj1BVHN+rSVbtV0Yx48zRt9/9F/ZOoxOKBBEsaJk2MYhhVRjeRRw5yuw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "dependencies": {
+        "@csstools/selector-resolve-nested": "^1.1.0",
+        "@csstools/selector-specificity": "^3.0.2",
+        "postcss-selector-parser": "^6.0.13"
+      },
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
     "node_modules/postcss-resolve-nested-selector": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
diff --git a/package.json b/package.json
index d5e8170228..26632480e8 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
     "pdfobject": "2.3.0",
     "postcss": "8.4.35",
     "postcss-loader": "8.1.1",
+    "postcss-nesting": "12.1.0",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
     "swagger-ui-dist": "5.11.8",
diff --git a/webpack.config.js b/webpack.config.js
index 3973d85344..d073a3e9f1 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -13,6 +13,8 @@ import {readFileSync} from 'node:fs';
 import {env} from 'node:process';
 import tailwindcss from 'tailwindcss';
 import tailwindConfig from './tailwind.config.js';
+import tailwindcssNesting from 'tailwindcss/nesting/index.js';
+import postcssNesting from 'postcss-nesting';
 
 const {EsbuildPlugin} = EsBuildLoader;
 const {SourceMapDevToolPlugin, DefinePlugin} = webpack;
@@ -145,6 +147,7 @@ export default {
               sourceMap: sourceMaps === 'true',
               url: {filter: filterCssImport},
               import: {filter: filterCssImport},
+              importLoaders: 1,
             },
           },
           {
@@ -152,7 +155,10 @@ export default {
             options: {
               postcssOptions: {
                 map: false, // https://github.com/postcss/postcss/issues/1914
-                plugins: [tailwindcss(tailwindConfig)],
+                plugins: [
+                  tailwindcssNesting(postcssNesting({edition: '2024-02'})),
+                  tailwindcss(tailwindConfig),
+                ],
               },
             },
           }

From ce085b26fc5076b36c55e6a0a30ba8f11105c0bf Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Fri, 15 Mar 2024 04:01:16 +0900
Subject: [PATCH 371/679] Improve commit record's ui in comment list (#26619)

Before:

![image](https://github.com/go-gitea/gitea/assets/18380374/795f9941-9989-4045-b0fc-d6dd0262269b)

![image](https://github.com/go-gitea/gitea/assets/18380374/f6505f5e-4248-456e-a98d-e714c6484b2f)

After:

![image](https://github.com/go-gitea/gitea/assets/18380374/321dda1e-6999-4851-afff-2e6c8d20367b)

![image](https://github.com/go-gitea/gitea/assets/18380374/182f18d1-2295-4004-852b-c0ebb498b411)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 templates/repo/issue/view_content/comments.tmpl | 16 ++++++++--------
 web_src/css/repo.css                            |  6 +-----
 2 files changed, 9 insertions(+), 13 deletions(-)

diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index db63e2b951..a9c6bbe318 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -148,7 +148,7 @@
 				</span>
 				{{if eq .RefAction 3}}</del>{{end}}
 
-				<div class="detail">
+				<div class="detail flex-text-block">
 					<span class="text grey muted-links"><a href="{{.RefIssueLink ctx}}"><b>{{.RefIssueTitle ctx}}</b> {{.RefIssueIdent ctx}}</a></span>
 				</div>
 			</div>
@@ -160,7 +160,7 @@
 					{{template "shared/user/authorlink" .Poster}}
 					{{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr}}
 				</span>
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-git-commit"}}
 					<span class="text grey muted-links">{{.Content | SanitizeHTML}}</span>
 				</div>
@@ -252,7 +252,7 @@
 					{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}}
 				</span>
 				{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-clock"}}
 					{{if .RenderedContent}}
 						{{/* compatibility with time comments made before v1.21 */}}
@@ -271,7 +271,7 @@
 					{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}}
 				</span>
 				{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-clock"}}
 					{{if .RenderedContent}}
 						{{/* compatibility with time comments made before v1.21 */}}
@@ -331,7 +331,7 @@
 					{{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr}}
 				</span>
 				{{if .DependentIssue}}
-					<div class="detail">
+					<div class="detail flex-text-block">
 						{{svg "octicon-plus"}}
 						<span class="text grey muted-links">
 							<a href="{{.DependentIssue.Link}}">
@@ -354,8 +354,8 @@
 					{{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr}}
 				</span>
 				{{if .DependentIssue}}
-					<div class="detail">
-						<span class="text grey muted-links">{{svg "octicon-trash"}}</span>
+					<div class="detail flex-text-block">
+						{{svg "octicon-trash"}}
 						<span class="text grey muted-links">
 							<a href="{{.DependentIssue.Link}}">
 								{{if eq .DependentIssue.RepoID .Issue.RepoID}}
@@ -506,7 +506,7 @@
 
 					{{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr}}
 				</span>
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-clock"}}
 					{{if .RenderedContent}}
 						{{/* compatibility with time comments made before v1.21 */}}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 587a3152e5..c9c27acf34 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1065,11 +1065,7 @@
 
 .repository.view.issue .comment-list .event .detail {
   margin-top: 4px;
-  margin-left: 14px;
-}
-
-.repository.view.issue .comment-list .event .detail .svg {
-  margin-right: 2px;
+  margin-left: 15px;
 }
 
 .repository.view.issue .comment-list .event .segments {

From 0679e60c776cd45f32acc12f52fe41b627da57e9 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Thu, 14 Mar 2024 23:36:17 +0200
Subject: [PATCH 372/679] Remove jQuery AJAX from the `repo-issue.js` file
 (#29776)

Removed all jQuery AJAX calls and replaced with our fetch wrapper.

Tested the following functionalities and they work as before:
- due-date update
- comment deletion
- branch update by merge or rebase
- allow edits from maintainers button
- reviewer addition or deletion
- WIP toggle button
- new diff code comment button
- issue title edit button

# Demo using `fetch` instead of jQuery AJAX
## Updating the due-date of an issue

![due_date](https://github.com/go-gitea/gitea/assets/20454870/7de395d3-63e8-49e8-9a13-8d14fc26810d)

## Deleting a comment

![comment_delete](https://github.com/go-gitea/gitea/assets/20454870/2814e695-44e3-4548-9ee7-7b437bef4b01)

## Updating a branch in a pull request

![branch_update](https://github.com/go-gitea/gitea/assets/20454870/137da77e-acc4-4984-a1bc-be58583bf52a)

## Checking and unchecking the "Allow edits from maintainers" checkbox

![allow_edits](https://github.com/go-gitea/gitea/assets/20454870/8d4829af-5813-432d-90ef-da057f8cdafc)

## Requesting review and removing review request

![reviewer_addition](https://github.com/go-gitea/gitea/assets/20454870/08f210e0-be3f-41af-b271-214a1dd2d0ba)

## Toggling the WIP status of a pull request

![wip](https://github.com/go-gitea/gitea/assets/20454870/dea5e668-1c89-4f3d-a5d6-4c26aefc4814)

## Clicking the new code comment button on the diff page

![code_comment](https://github.com/go-gitea/gitea/assets/20454870/1d17174e-3bba-4cf8-81fe-c3a2c21f80b9)

## Editing the issue title and target branch

![issue_title](https://github.com/go-gitea/gitea/assets/20454870/7099888e-81c0-47d4-9371-8e4469e9e519)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-issue.js | 196 +++++++++++++++++-------------
 1 file changed, 113 insertions(+), 83 deletions(-)

diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 6fb13b0dda..01c30a9e53 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -6,8 +6,9 @@ import {setFileFolding} from './file-fold.js';
 import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {toAbsoluteUrl} from '../utils.js';
 import {initDropzone} from './common-global.js';
+import {POST, GET} from '../modules/fetch.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 
 export function initRepoIssueTimeTracking() {
   $(document).on('click', '.issue-add-time', () => {
@@ -40,7 +41,7 @@ export function initRepoIssueTimeTracking() {
   });
 }
 
-function updateDeadline(deadlineString) {
+async function updateDeadline(deadlineString) {
   hideElem($('#deadline-err-invalid-date'));
   $('#deadline-loader').addClass('loading');
 
@@ -56,23 +57,21 @@ function updateDeadline(deadlineString) {
     realDeadline = new Date(newDate);
   }
 
-  $.ajax(`${$('#update-issue-deadline-form').attr('action')}`, {
-    data: JSON.stringify({
-      due_date: realDeadline,
-    }),
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-    contentType: 'application/json',
-    type: 'POST',
-    success() {
+  try {
+    const response = await POST($('#update-issue-deadline-form').attr('action'), {
+      data: {due_date: realDeadline}
+    });
+
+    if (response.ok) {
       window.location.reload();
-    },
-    error() {
-      $('#deadline-loader').removeClass('loading');
-      showElem($('#deadline-err-invalid-date'));
-    },
-  });
+    } else {
+      throw new Error('Invalid response');
+    }
+  } catch (error) {
+    console.error(error);
+    $('#deadline-loader').removeClass('loading');
+    showElem($('#deadline-err-invalid-date'));
+  }
 }
 
 export function initRepoIssueDue() {
@@ -156,12 +155,12 @@ export function initRepoIssueSidebarList() {
 
 export function initRepoIssueCommentDelete() {
   // Delete comment
-  $(document).on('click', '.delete-comment', function () {
+  $(document).on('click', '.delete-comment', async function () {
     const $this = $(this);
     if (window.confirm($this.data('locale'))) {
-      $.post($this.data('url'), {
-        _csrf: csrfToken,
-      }).done(() => {
+      try {
+        const response = await POST($this.data('url'));
+        if (!response.ok) throw new Error('Failed to delete comment');
         const $conversationHolder = $this.closest('.conversation-holder');
 
         // Check if this was a pending comment.
@@ -186,7 +185,9 @@ export function initRepoIssueCommentDelete() {
           }
           $conversationHolder.remove();
         }
-      });
+      } catch (error) {
+        console.error(error);
+      }
     }
     return false;
   });
@@ -226,22 +227,32 @@ export function initRepoIssueCodeCommentCancel() {
 export function initRepoPullRequestUpdate() {
   // Pull Request update button
   const $pullUpdateButton = $('.update-button > button');
-  $pullUpdateButton.on('click', function (e) {
+  $pullUpdateButton.on('click', async function (e) {
     e.preventDefault();
     const $this = $(this);
     const redirect = $this.data('redirect');
     $this.addClass('loading');
-    $.post($this.data('do'), {
-      _csrf: csrfToken
-    }).done((data) => {
-      if (data.redirect) {
-        window.location.href = data.redirect;
-      } else if (redirect) {
-        window.location.href = redirect;
-      } else {
-        window.location.reload();
-      }
-    });
+    let response;
+    try {
+      response = await POST($this.data('do'));
+    } catch (error) {
+      console.error(error);
+    } finally {
+      $this.removeClass('loading');
+    }
+    let data;
+    try {
+      data = await response?.json(); // the response is probably not a JSON
+    } catch (error) {
+      console.error(error);
+    }
+    if (data?.redirect) {
+      window.location.href = data.redirect;
+    } else if (redirect) {
+      window.location.href = redirect;
+    } else {
+      window.location.reload();
+    }
   });
 
   $('.update-button > .dropdown').dropdown({
@@ -267,20 +278,24 @@ export function initRepoPullRequestAllowMaintainerEdit() {
 
   const promptError = $checkbox.attr('data-prompt-error');
   $checkbox.checkbox({
-    'onChange': () => {
+    'onChange': async () => {
       const checked = $checkbox.checkbox('is checked');
       let url = $checkbox.attr('data-url');
       url += '/set_allow_maintainer_edit';
       $checkbox.checkbox('set disabled');
-      $.ajax({url, type: 'POST',
-        data: {_csrf: csrfToken, allow_maintainer_edit: checked},
-        error: () => {
-          showTemporaryTooltip($checkbox[0], promptError);
-        },
-        complete: () => {
-          $checkbox.checkbox('set enabled');
-        },
-      });
+      try {
+        const response = await POST(url, {
+          data: {allow_maintainer_edit: checked},
+        });
+        if (!response.ok) {
+          throw new Error('Failed to update maintainer edit permission');
+        }
+      } catch (error) {
+        console.error(error);
+        showTemporaryTooltip($checkbox[0], promptError);
+      } finally {
+        $checkbox.checkbox('set enabled');
+      }
     },
   });
 }
@@ -329,17 +344,15 @@ export function initRepoIssueWipTitle() {
   });
 }
 
-export async function updateIssuesMeta(url, action, issueIds, elementId) {
-  return $.ajax({
-    type: 'POST',
-    url,
-    data: {
-      _csrf: csrfToken,
-      action,
-      issue_ids: issueIds,
-      id: elementId,
-    },
-  });
+export async function updateIssuesMeta(url, action, issue_ids, id) {
+  try {
+    const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
+    if (!response.ok) {
+      throw new Error('Failed to update issues meta');
+    }
+  } catch (error) {
+    console.error(error);
+  }
 }
 
 export function initRepoIssueComments() {
@@ -511,15 +524,20 @@ export function initRepoPullRequestReview() {
     const td = ntr.find(`.add-comment-${side}`);
     const commentCloud = td.find('.comment-code-cloud');
     if (commentCloud.length === 0 && !ntr.find('button[name="pending_review"]').length) {
-      const html = await $.get($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
-      td.html(html);
-      td.find("input[name='line']").val(idx);
-      td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
-      td.find("input[name='path']").val(path);
+      try {
+        const response = await GET($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
+        const html = await response.text();
+        td.html(html);
+        td.find("input[name='line']").val(idx);
+        td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
+        td.find("input[name='path']").val(path);
 
-      initDropzone(td.find('.dropzone')[0]);
-      const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor'));
-      editor.focus();
+        initDropzone(td.find('.dropzone')[0]);
+        const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor'));
+        editor.focus();
+      } catch (error) {
+        console.error(error);
+      }
     }
   });
 }
@@ -547,11 +565,19 @@ export function initRepoIssueWipToggle() {
     const title = toggleWip.getAttribute('data-title');
     const wipPrefix = toggleWip.getAttribute('data-wip-prefix');
     const updateUrl = toggleWip.getAttribute('data-update-url');
-    await $.post(updateUrl, {
-      _csrf: csrfToken,
-      title: title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`,
-    });
-    window.location.reload();
+
+    try {
+      const params = new URLSearchParams();
+      params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`);
+
+      const response = await POST(updateUrl, {data: params});
+      if (!response.ok) {
+        throw new Error('Failed to toggle WIP status');
+      }
+      window.location.reload();
+    } catch (error) {
+      console.error(error);
+    }
   });
 }
 
@@ -576,39 +602,43 @@ export function initRepoIssueTitleEdit() {
 
   $('#edit-title').on('click', editTitleToggle);
   $('#cancel-edit-title').on('click', editTitleToggle);
-  $('#save-edit-title').on('click', editTitleToggle).on('click', function () {
-    const pullrequest_targetbranch_change = function (update_url) {
+  $('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
+    const pullrequest_targetbranch_change = async function (update_url) {
       const targetBranch = $('#pull-target-branch').data('branch');
       const $branchTarget = $('#branch_target');
       if (targetBranch === $branchTarget.text()) {
         window.location.reload();
         return false;
       }
-      $.post(update_url, {
-        _csrf: csrfToken,
-        target_branch: targetBranch
-      }).always(() => {
+      try {
+        await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
+      } catch (error) {
+        console.error(error);
+      } finally {
         window.location.reload();
-      });
+      }
     };
 
     const pullrequest_target_update_url = $(this).attr('data-target-update-url');
     if ($editInput.val().length === 0 || $editInput.val() === $issueTitle.text()) {
       $editInput.val($issueTitle.text());
-      pullrequest_targetbranch_change(pullrequest_target_update_url);
+      await pullrequest_targetbranch_change(pullrequest_target_update_url);
     } else {
-      $.post($(this).attr('data-update-url'), {
-        _csrf: csrfToken,
-        title: $editInput.val()
-      }, (data) => {
+      try {
+        const params = new URLSearchParams();
+        params.append('title', $editInput.val());
+        const response = await POST($(this).attr('data-update-url'), {data: params});
+        const data = await response.json();
         $editInput.val(data.title);
         $issueTitle.text(data.title);
         if (pullrequest_target_update_url) {
-          pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
+          await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
         } else {
           window.location.reload();
         }
-      });
+      } catch (error) {
+        console.error(error);
+      }
     }
     return false;
   });

From 35def319fdb8c73aa5e2c52fad5230d287e2bd93 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 14 Mar 2024 23:04:33 +0100
Subject: [PATCH 373/679] Fix Safari spinner rendering (#29801)

Fixes: https://github.com/go-gitea/gitea/issues/29041
Fixes: https://github.com/go-gitea/gitea/pull/29713

Any of the `width: *-content` properties seem to workaround this Webkit
bug, this one seemed most suitable.
---
 web_src/css/modules/animations.css | 1 +
 1 file changed, 1 insertion(+)

diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index 87eb6a75cf..d5ddc772f6 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -20,6 +20,7 @@
   left: 50%;
   top: 50%;
   height: min(4em, 66.6%);
+  width: fit-content; /* compat: safari - https://bugs.webkit.org/show_bug.cgi?id=267625 */
   aspect-ratio: 1;
   transform: translate(-50%, -50%);
   animation: isloadingspin 1000ms infinite linear;

From 70e077036f8d3026cecddb746a1de69e02ab9b9a Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 15 Mar 2024 00:21:14 +0200
Subject: [PATCH 374/679] Remove jQuery AJAX from the diff functions (#29743)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the review conversation comment, resolve, unresolve, show more
files, and load diff functionality and it works as before

# Demo using `fetch` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/cc0bed59-f11f-4e48-bfa3-59ab52d9889e)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-diff.js | 67 +++++++++++++++++++-------------
 1 file changed, 40 insertions(+), 27 deletions(-)

diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 77691e15e6..b341583c3e 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -8,8 +8,9 @@ import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndC
 import {initImageDiff} from './imagediff.js';
 import {showErrorToast} from '../modules/toast.js';
 import {submitEventSubmitter} from '../utils/dom.js';
+import {POST, GET} from '../modules/fetch.js';
 
-const {csrfToken, pageData, i18n} = window.config;
+const {pageData, i18n} = window.config;
 
 function initRepoDiffReviewButton() {
   const $reviewBox = $('#review-box');
@@ -63,8 +64,9 @@ function initRepoDiffConversationForm() {
       if (isSubmittedByButton && submitter.name) {
         formData.append(submitter.name, submitter.value);
       }
-      const formDataString = String(new URLSearchParams(formData));
-      const $newConversationHolder = $(await $.post($form.attr('action'), formDataString));
+
+      const response = await POST($form.attr('action'), {data: formData});
+      const $newConversationHolder = $(await response.text());
       const {path, side, idx} = $newConversationHolder.data();
 
       $form.closest('.conversation-holder').replaceWith($newConversationHolder);
@@ -75,7 +77,8 @@ function initRepoDiffConversationForm() {
       }
       $newConversationHolder.find('.dropdown').dropdown();
       initCompReactionSelector($newConversationHolder);
-    } catch { // here the caught error might be a jQuery AJAX error (thrown by await $.post), which is not good to use for error message handling
+    } catch (error) {
+      console.error('Error:', error);
       showErrorToast(i18n.network_error);
     } finally {
       $form.removeClass('is-loading');
@@ -89,15 +92,20 @@ function initRepoDiffConversationForm() {
     const action = $(this).data('action');
     const url = $(this).data('update-url');
 
-    const data = await $.post(url, {_csrf: csrfToken, origin, action, comment_id});
+    try {
+      const response = await POST(url, {data: new URLSearchParams({origin, action, comment_id})});
+      const data = await response.text();
 
-    if ($(this).closest('.conversation-holder').length) {
-      const conversation = $(data);
-      $(this).closest('.conversation-holder').replaceWith(conversation);
-      conversation.find('.dropdown').dropdown();
-      initCompReactionSelector(conversation);
-    } else {
-      window.location.reload();
+      if ($(this).closest('.conversation-holder').length) {
+        const conversation = $(data);
+        $(this).closest('.conversation-holder').replaceWith(conversation);
+        conversation.find('.dropdown').dropdown();
+        initCompReactionSelector(conversation);
+      } else {
+        window.location.reload();
+      }
+    } catch (error) {
+      console.error('Error:', error);
     }
   });
 }
@@ -132,7 +140,7 @@ function onShowMoreFiles() {
   initImageDiff();
 }
 
-export function loadMoreFiles(url) {
+export async function loadMoreFiles(url) {
   const $target = $('a#diff-show-more-files');
   if ($target.hasClass('disabled') || pageData.diffFileInfo.isLoadingNewData) {
     return;
@@ -140,10 +148,10 @@ export function loadMoreFiles(url) {
 
   pageData.diffFileInfo.isLoadingNewData = true;
   $target.addClass('disabled');
-  $.ajax({
-    type: 'GET',
-    url,
-  }).done((resp) => {
+
+  try {
+    const response = await GET(url);
+    const resp = await response.text();
     const $resp = $(resp);
     // the response is a full HTML page, we need to extract the relevant contents:
     // 1. append the newly loaded file list items to the existing list
@@ -152,10 +160,13 @@ export function loadMoreFiles(url) {
     $('body').append($resp.find('script#diff-data-script'));
 
     onShowMoreFiles();
-  }).always(() => {
+  } catch (error) {
+    console.error('Error:', error);
+    showErrorToast('An error occurred while loading more files.');
+  } finally {
     $target.removeClass('disabled');
     pageData.diffFileInfo.isLoadingNewData = false;
-  });
+  }
 }
 
 function initRepoDiffShowMore() {
@@ -167,7 +178,7 @@ function initRepoDiffShowMore() {
     loadMoreFiles(linkLoadMore);
   });
 
-  $(document).on('click', 'a.diff-load-button', (e) => {
+  $(document).on('click', 'a.diff-load-button', async (e) => {
     e.preventDefault();
     const $target = $(e.target);
 
@@ -178,19 +189,21 @@ function initRepoDiffShowMore() {
     $target.addClass('disabled');
 
     const url = $target.data('href');
-    $.ajax({
-      type: 'GET',
-      url,
-    }).done((resp) => {
+
+    try {
+      const response = await GET(url);
+      const resp = await response.text();
+
       if (!resp) {
-        $target.removeClass('disabled');
         return;
       }
       $target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children());
       onShowMoreFiles();
-    }).fail(() => {
+    } catch (error) {
+      console.error('Error:', error);
+    } finally {
       $target.removeClass('disabled');
-    });
+    }
   });
 }
 

From 607ed27b4fb8ead346f89b379d9788f5c76fb799 Mon Sep 17 00:00:00 2001
From: Daniel YC Lin <dlin.tw@gmail.com>
Date: Fri, 15 Mar 2024 06:54:11 +0800
Subject: [PATCH 375/679] Fix document error about 'make trans-copy' (#29710)

Change document to 'make docs'

---------

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
---
 Makefile                                      |  4 ---
 .../development/hacking-on-gitea.en-us.md     |  7 +---
 .../development/hacking-on-gitea.zh-cn.md     |  6 +---
 docs/scripts/trans-copy.sh                    | 34 -------------------
 4 files changed, 2 insertions(+), 49 deletions(-)
 delete mode 100755 docs/scripts/trans-copy.sh

diff --git a/Makefile b/Makefile
index cedc4b4198..88bcf0e17c 100644
--- a/Makefile
+++ b/Makefile
@@ -839,10 +839,6 @@ release-sources: | $(DIST_DIRS)
 release-docs: | $(DIST_DIRS) docs
 	tar -czf $(DIST)/release/gitea-docs-$(VERSION).tar.gz -C ./docs .
 
-.PHONY: docs
-docs:
-	cd docs; bash scripts/trans-copy.sh;
-
 .PHONY: deps
 deps: deps-frontend deps-backend deps-tools deps-py
 
diff --git a/docs/content/development/hacking-on-gitea.en-us.md b/docs/content/development/hacking-on-gitea.en-us.md
index df8a9047d6..982dbcf6ea 100644
--- a/docs/content/development/hacking-on-gitea.en-us.md
+++ b/docs/content/development/hacking-on-gitea.en-us.md
@@ -333,14 +333,9 @@ Documentation for the website is found in `docs/`. If you change this you
 can test your changes to ensure that they pass continuous integration using:
 
 ```bash
-# from the docs directory within Gitea
-make trans-copy clean build
+make lint-md
 ```
 
-You will require a copy of [Hugo](https://gohugo.io/) to run this task. Please
-note: this may generate a number of untracked Git objects, which will need to
-be cleaned up.
-
 ## Visual Studio Code
 
 A `launch.json` and `tasks.json` are provided within `contrib/ide/vscode` for
diff --git a/docs/content/development/hacking-on-gitea.zh-cn.md b/docs/content/development/hacking-on-gitea.zh-cn.md
index 2dba3c92b6..a31e1dc511 100644
--- a/docs/content/development/hacking-on-gitea.zh-cn.md
+++ b/docs/content/development/hacking-on-gitea.zh-cn.md
@@ -307,13 +307,9 @@ TAGS="bindata sqlite sqlite_unlock_notify" make build test-sqlite
 该网站的文档位于 `docs/` 中。如果你改变了文档内容,你可以使用以下测试方法进行持续集成:
 
 ```bash
-# 来自 Gitea 中的 docs 目录
-make trans-copy clean build
+make lint-md
 ```
 
-运行此任务依赖于 [Hugo](https://gohugo.io/)。请注意:这可能会生成一些未跟踪的 Git 对象,
-需要被清理干净。
-
 ## Visual Studio Code
 
 `contrib/ide/vscode` 中为 Visual Studio Code 提供了 `launch.json` 和 `tasks.json`。查看
diff --git a/docs/scripts/trans-copy.sh b/docs/scripts/trans-copy.sh
deleted file mode 100755
index 7374ab9e73..0000000000
--- a/docs/scripts/trans-copy.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-#
-# This script is used to copy the en-US content to our available locales as a
-# fallback to always show all pages when displaying a specific locale that is
-# missing some documents to be translated.
-#
-# Just execute the script without any argument and you will get the missing
-# files copied into the content folder. We are calling this script within the CI
-# server simply by `make trans-copy`.
-#
-
-declare -a LOCALES=(
-  "fr-fr"
-  "nl-nl"
-  "pt-br"
-  "zh-cn"
-  "zh-tw"
-)
-
-ROOT=$(realpath $(dirname $0)/..)
-
-for SOURCE in $(find ${ROOT}/content -type f -iname *.en-us.md); do
-  for LOCALE in "${LOCALES[@]}"; do
-    DEST="${SOURCE%.en-us.md}.${LOCALE}.md"
-
-    if [[ ! -f ${DEST} ]]; then
-      cp ${SOURCE} ${DEST}
-      sed -i.bak "s/en\-us/${LOCALE}/g" ${DEST}
-      rm ${DEST}.bak
-    fi
-  done
-done

From e0b002a4a8fbdb539108ae619772607bef9be23b Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Fri, 15 Mar 2024 00:24:59 +0100
Subject: [PATCH 376/679] Unify search boxes (#29530)

Unify all but a few search boxes to use uniform style, uniform
translations and shared templates where possible.
Remove a few duplicated search templates, e. g. code search.

<details><summary>Example after screenshots:</summary>


![grafik](https://github.com/go-gitea/gitea/assets/47871822/e20e7d6b-c6be-4a47-b132-672766f41421)


![grafik](https://github.com/go-gitea/gitea/assets/47871822/d5b11b9c-c12f-4a29-8fb0-24e5aa511d18)


![grafik](https://github.com/go-gitea/gitea/assets/47871822/d86bb444-36c7-426d-9cf1-c634963dffb1)


![grafik](https://github.com/go-gitea/gitea/assets/47871822/a76c0319-0518-484a-a840-563d02b61198)

</details>


Also includes #29700

Co-authored-by: 6543 <6543@obermui.de>

---------

Co-authored-by: 6543 <m.huber@kithara.com>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 options/locale/locale_en-US.ini               |  55 ++--
 routers/web/explore/code.go                   |   5 +-
 routers/web/repo/commit.go                    |   2 +-
 routers/web/repo/search.go                    |   7 +-
 routers/web/user/code.go                      |   5 +-
 services/context/pagination.go                |   2 +-
 templates/admin/base/search.tmpl              |  23 --
 templates/admin/emails/list.tmpl              |   5 +-
 templates/admin/org/list.tmpl                 |  21 +-
 templates/admin/packages/list.tmpl            |   8 +-
 templates/admin/repo/unadopted.tmpl           |   8 +-
 templates/admin/user/list.tmpl                |   6 +-
 templates/code/searchcombo.tmpl               |  17 --
 templates/code/searchform.tmpl                |  14 -
 templates/explore/code.tmpl                   |   2 +-
 templates/explore/repo_list.tmpl              |   2 +-
 templates/explore/search.tmpl                 |  13 +-
 templates/explore/user_list.tmpl              |   4 +-
 templates/explore/users.tmpl                  |   2 -
 templates/org/team/members.tmpl               |   2 +-
 templates/org/team/repositories.tmpl          |   2 +-
 templates/package/shared/list.tmpl            |   8 +-
 templates/package/shared/versionlist.tmpl     |  10 +-
 templates/projects/list.tmpl                  |   9 +-
 templates/repo/branch/list.tmpl               | 262 +++++++++---------
 templates/repo/commits_search_dropdown.tmpl   |   8 +
 templates/repo/commits_table.tmpl             |  32 +--
 templates/repo/home.tmpl                      |   8 +-
 templates/repo/issue/search.tmpl              |   4 +-
 templates/repo/search.tmpl                    |  54 +---
 templates/repo/settings/collaboration.tmpl    |   4 +-
 templates/repo/settings/protected_branch.tmpl |  12 +-
 templates/repo/settings/tags.tmpl             |   4 +-
 templates/shared/actions/runner_list.tmpl     |   6 +-
 templates/shared/issuelist.tmpl               |   2 +-
 templates/shared/repo_search.tmpl             |  15 +-
 templates/shared/search/button.tmpl           |   3 +
 .../search/code/results.tmpl}                 |  18 +-
 templates/shared/search/code/search.tmpl      |  15 +
 templates/shared/search/combo.tmpl            |   8 +
 templates/shared/search/combo_fuzzy.tmpl      |  10 +
 templates/shared/search/fuzzy.tmpl            |  10 +
 templates/shared/search/input.tmpl            |   4 +
 templates/shared/searchinput.tmpl             |   1 -
 templates/user/code.tmpl                      |   4 +-
 templates/user/dashboard/issues.tmpl          |   4 +-
 templates/user/dashboard/milestones.tmpl      |   7 +-
 templates/user/dashboard/repolist.tmpl        |   2 +-
 web_src/css/base.css                          |  26 +-
 web_src/css/form.css                          |   5 +
 web_src/js/components/DashboardRepoList.vue   |   2 +-
 51 files changed, 356 insertions(+), 406 deletions(-)
 delete mode 100644 templates/admin/base/search.tmpl
 delete mode 100644 templates/code/searchcombo.tmpl
 delete mode 100644 templates/code/searchform.tmpl
 create mode 100644 templates/repo/commits_search_dropdown.tmpl
 create mode 100644 templates/shared/search/button.tmpl
 rename templates/{code/searchresults.tmpl => shared/search/code/results.tmpl} (70%)
 create mode 100644 templates/shared/search/code/search.tmpl
 create mode 100644 templates/shared/search/combo.tmpl
 create mode 100644 templates/shared/search/combo_fuzzy.tmpl
 create mode 100644 templates/shared/search/fuzzy.tmpl
 create mode 100644 templates/shared/search/input.tmpl
 delete mode 100644 templates/shared/searchinput.tmpl

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 836703a480..8c014955d0 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -155,6 +155,27 @@ filter.not_template = Not Template
 filter.public = Public
 filter.private = Private
 
+[search]
+search = Search...
+type_tooltip = Search type
+fuzzy = Fuzzy
+fuzzy_tooltip = Include results that also match the search term closely
+match = Match
+match_tooltip = Include only results that match the exact search term
+repo_kind = Search repos...
+user_kind = Search users...
+org_kind = Search orgs...
+team_kind = Search teams...
+code_kind = Search code...
+code_search_unavailable = Code search is currently not available. Please contact the site administrator.
+package_kind = Search packages...
+project_kind = Search projects...
+branch_kind = Search branches...
+commit_kind = Search commits...
+runner_kind = Search runners...
+no_results = No matching results found.
+keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator.
+
 [aria]
 navbar = Navigation Bar
 footer = Footer
@@ -330,7 +351,6 @@ collaborative_repos = Collaborative Repositories
 my_orgs = My Organizations
 my_mirrors = My Mirrors
 view_home = View %s
-search_repos = Find a repository…
 filter = Other Filters
 filter_by_team_repositories = Filter by team repositories
 feed_of = Feed of "%s"
@@ -351,20 +371,8 @@ issues.in_your_repos = In your repositories
 repos = Repositories
 users = Users
 organizations = Organizations
-search = Search
 go_to = Go to
 code = Code
-search.type.tooltip = Search type
-search.fuzzy = Fuzzy
-search.fuzzy.tooltip = Include results that also matches the search term closely
-search.match = Match
-search.match.tooltip = Include only results that matches the exact search term
-code_search_unavailable = Currently code search is not available. Please contact your site administrator.
-repo_no_results = No matching repositories found.
-user_no_results = No matching users found.
-org_no_results = No matching organizations found.
-code_no_results = No source code matching your search term found.
-code_search_results = Search results for "%s"
 code_last_indexed_at = Last indexed %s
 relevant_repositories_tooltip = Repositories that are forks or that have no topic, no icon, and no description are hidden.
 relevant_repositories = Only relevant repositories are being shown, <a href="%s">show unfiltered results</a>.
@@ -1324,9 +1332,8 @@ commits.desc = Browse source code change history.
 commits.commits = Commits
 commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
 commits.nothing_to_compare = These branches are equal.
-commits.search = Search commits…
 commits.search.tooltip = You can prefix keywords with "author:", "committer:", "after:", or "before:", e.g. "revert author:Alice before:2019-01-13".
-commits.find = Search
+commits.search_branch = This Branch
 commits.search_all = All Branches
 commits.author = Author
 commits.message = Message
@@ -1502,7 +1509,6 @@ issues.filter_sort.moststars = Most stars
 issues.filter_sort.feweststars = Fewest stars
 issues.filter_sort.mostforks = Most forks
 issues.filter_sort.fewestforks = Fewest forks
-issues.keyword_search_unavailable = Searching by keyword is currently not available. Please contact your site administrator.
 issues.action_open = Open
 issues.action_close = Close
 issues.action_label = Label
@@ -2035,17 +2041,6 @@ contributors.contribution_type.commits = Commits
 contributors.contribution_type.additions = Additions
 contributors.contribution_type.deletions = Deletions
 
-search = Search
-search.search_repo = Search repository
-search.type.tooltip = Search type
-search.fuzzy = Fuzzy
-search.fuzzy.tooltip = Include results that also matches the search term closely
-search.match = Match
-search.match.tooltip = Include only results that matches the exact search term
-search.results = Search results for "%s" in <a href="%s">%s</a>
-search.code_no_results = No source code matching your search term found.
-search.code_search_unavailable = Currently code search is not available. Please contact your site administrator.
-
 settings = Settings
 settings.desc = Settings is where you can manage the settings for the repository
 settings.options = Repository
@@ -2206,7 +2201,6 @@ settings.delete_collaborator = Remove
 settings.collaborator_deletion = Remove Collaborator
 settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
 settings.remove_collaborator_success = The collaborator has been removed.
-settings.search_user_placeholder = Search user…
 settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator.
 settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner
 settings.team_not_in_organization = The team is not in the same organization as the repository
@@ -2214,7 +2208,6 @@ settings.teams = Teams
 settings.add_team = Add Team
 settings.add_team_duplicate = Team already has the repository
 settings.add_team_success = The team now have access to the repository.
-settings.search_team = Search Team…
 settings.change_team_permission_tip = Team's permission is set on the team setting page and can't be changed per repository
 settings.delete_team_tip = This team has access to all repositories and can't be removed
 settings.remove_team_success = The team's access to the repository has been removed.
@@ -2367,9 +2360,7 @@ settings.protect_whitelist_committers = Whitelist Restricted Push
 settings.protect_whitelist_committers_desc = Only whitelisted users or teams will be allowed to push to this branch (but not force push).
 settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push.
 settings.protect_whitelist_users = Whitelisted users for pushing:
-settings.protect_whitelist_search_users = Search users…
 settings.protect_whitelist_teams = Whitelisted teams for pushing:
-settings.protect_whitelist_search_teams = Search teams…
 settings.protect_merge_whitelist_committers = Enable Merge Whitelist
 settings.protect_merge_whitelist_committers_desc = Allow only whitelisted users or teams to merge pull requests into this branch.
 settings.protect_merge_whitelist_users = Whitelisted users for merging:
@@ -2614,7 +2605,6 @@ branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be
 branch.restore = Restore Branch "%s"
 branch.download = Download Branch "%s"
 branch.rename = Rename Branch "%s"
-branch.search = Search Branch
 branch.included_desc = This branch is part of the default branch
 branch.included = Included
 branch.create_new_branch = Create branch from branch:
@@ -2760,7 +2750,6 @@ teams.write_permission_desc = This team grants <strong>Write</strong> access: me
 teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to and add collaborators to team repositories.
 teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
 teams.repositories = Team Repositories
-teams.search_repo_placeholder = Search repository…
 teams.remove_all_repos_title = Remove all team repositories
 teams.remove_all_repos_desc = This will remove all repositories from the team.
 teams.add_all_repos_title = Add all repositories
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index a6bc71ac9c..7ba5984002 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -34,12 +34,11 @@ func Code(ctx *context.Context) {
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	queryType := ctx.FormTrim("t")
-	isFuzzy := queryType != "match"
+	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
+	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["PageIsViewCode"] = true
 
 	if keyword == "" {
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 16da917d22..1b99f4183c 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -203,7 +203,7 @@ func SearchCommits(ctx *context.Context) {
 
 	ctx.Data["Keyword"] = query
 	if all {
-		ctx.Data["All"] = "checked"
+		ctx.Data["All"] = true
 	}
 	ctx.Data["Username"] = ctx.Repo.Owner.Name
 	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 766dd5726a..16e620f57d 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -24,12 +24,11 @@ func Search(ctx *context.Context) {
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	queryType := ctx.FormTrim("t")
-	isFuzzy := queryType != "match"
+	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
+	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["PageIsViewCode"] = true
 
 	if keyword == "" {
@@ -54,7 +53,7 @@ func Search(ctx *context.Context) {
 		ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
 	}
 
-	ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
+	ctx.Data["Repo"] = ctx.Repo.Repository
 	ctx.Data["SearchResults"] = searchResults
 	ctx.Data["SearchResultLanguages"] = searchResultLanguages
 
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index 8613d38b65..92911edfe9 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -39,12 +39,11 @@ func CodeSearch(ctx *context.Context) {
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	queryType := ctx.FormTrim("t")
-	isFuzzy := queryType != "match"
+	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
+	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["IsCodePage"] = true
 
 	if keyword == "" {
diff --git a/services/context/pagination.go b/services/context/pagination.go
index 68237c630c..655a278f9f 100644
--- a/services/context/pagination.go
+++ b/services/context/pagination.go
@@ -53,5 +53,5 @@ func (p *Pagination) SetDefaultParams(ctx *Context) {
 	p.AddParam(ctx, "sort", "SortType")
 	p.AddParam(ctx, "q", "Keyword")
 	// do not add any more uncommon params here!
-	p.AddParam(ctx, "t", "queryType")
+	p.AddParam(ctx, "fuzzy", "IsFuzzy")
 }
diff --git a/templates/admin/base/search.tmpl b/templates/admin/base/search.tmpl
deleted file mode 100644
index 0fecb61d9e..0000000000
--- a/templates/admin/base/search.tmpl
+++ /dev/null
@@ -1,23 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-		</div>
-	</div>
-</div>
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 29fbb5f039..1e552fba88 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -6,10 +6,7 @@
 		<div class="ui attached segment">
 			<div class="ui secondary filter menu gt-ac gt-mx-0">
 				<form class="ui form ignore-dirty gt-f1">
-					<div class="ui fluid action input">
-						{{template "shared/searchinput" dict "Value" .Keyword}}
-						<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-					</div>
+					{{template "shared/search/combo" dict "Value" .Keyword}}
 				</form>
 				<!-- Sort -->
 				<div class="ui dropdown type jump item gt-mr-0">
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index 0d79456b47..efb0a8847e 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -7,7 +7,26 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			{{template "admin/base/search" .}}
+			<div class="ui secondary filter menu gt-ac gt-mx-0">
+				<form class="ui form ignore-dirty gt-f1">
+					{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}}
+				</form>
+				<!-- Sort -->
+				<div class="ui dropdown type jump item gt-mr-0">
+					<span class="text">
+						{{ctx.Locale.Tr "repo.issues.filter_sort"}}
+					</span>
+					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+					<div class="menu">
+						<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+						<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+						<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+						<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
+						<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+						<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+					</div>
+				</div>
+			</div>
 		</div>
 		<div class="ui attached table segment">
 			<table class="ui very basic striped table unstackable">
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index aef4815424..863f11da25 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -13,16 +13,16 @@
 		</h4>
 		<div class="ui attached segment">
 			<form class="ui form ignore-dirty">
-				<div class="ui fluid action input">
-					{{template "shared/searchinput" dict "Value" .Query}}
-					<select class="ui dropdown" name="type">
+				<div class="ui small fluid action input">
+					{{template "shared/search/input" dict "Value" .Query}}
+					<select class="ui small dropdown" name="type">
 						<option value="">{{ctx.Locale.Tr "packages.filter.type"}}</option>
 						<option value="all">{{ctx.Locale.Tr "packages.filter.type.all"}}</option>
 						{{range $type := .AvailableTypes}}
 						<option{{if eq $.PackageType $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option>
 						{{end}}
 					</select>
-					<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+					{{template "shared/search/button"}}
 				</div>
 			</form>
 		</div>
diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl
index fb4f16791d..c65cfd9db4 100644
--- a/templates/admin/repo/unadopted.tmpl
+++ b/templates/admin/repo/unadopted.tmpl
@@ -8,10 +8,10 @@
 		</h4>
 		<div class="ui attached segment">
 			<form class="ui form ignore-dirty">
-				<div class="ui fluid action input">
-				<input name="search" value="true" type="hidden">
-				<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.adopt_search"}}" autofocus>
-				<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+				<div class="ui small fluid action input">
+					<input name="search" value="true" type="hidden">
+					<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.adopt_search"}}" autofocus>
+					{{template "shared/search/button"}}
 				</div>
 			</form>
 		</div>
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index e9ce17ac90..11c2fa5940 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -52,11 +52,7 @@
 					</div>
 				</div>
 
-				<!-- Search Text -->
-				<div class="ui fluid action input">
-					{{template "shared/searchinput" dict "Value" .Keyword}}
-					<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-				</div>
+				{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
 			</form>
 		</div>
 		<div class="ui attached table segment">
diff --git a/templates/code/searchcombo.tmpl b/templates/code/searchcombo.tmpl
deleted file mode 100644
index d451bc0ad8..0000000000
--- a/templates/code/searchcombo.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-{{template "code/searchform" .}}
-<div class="divider"></div>
-<div class="ui user list">
-	{{if .CodeIndexerUnavailable}}
-		<div class="ui error message">
-			<p>{{ctx.Locale.Tr "explore.code_search_unavailable"}}</p>
-		</div>
-	{{else if .SearchResults}}
-		<h3>
-			{{ctx.Locale.Tr "explore.code_search_results" .Keyword}}
-		</h3>
-		{{template "code/searchresults" .}}
-	{{else if .Keyword}}
-		<div>{{ctx.Locale.Tr "explore.code_no_results"}}</div>
-	{{end}}
-</div>
-{{template "base/paginate" .}}
diff --git a/templates/code/searchform.tmpl b/templates/code/searchform.tmpl
deleted file mode 100644
index fae1340046..0000000000
--- a/templates/code/searchform.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-<form class="ui form ignore-dirty">
-	<div class="ui fluid action input">
-		{{template "shared/searchinput" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable}}
-		<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "explore.search.type.tooltip"}}">
-			<input name="t" type="hidden" value="{{.queryType}}"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-			<div class="text">{{ctx.Locale.Tr (printf "explore.search.%s" (or .queryType "fuzzy"))}}</div>
-			<div class="menu">
-				<div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "explore.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "explore.search.fuzzy"}}</div>
-				<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "explore.search.match.tooltip"}}">{{ctx.Locale.Tr "explore.search.match"}}</div>
-			</div>
-		</div>
-		<button class="ui primary button"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{ctx.Locale.Tr "explore.search"}}</button>
-	</div>
-</form>
diff --git a/templates/explore/code.tmpl b/templates/explore/code.tmpl
index 2298575887..039933fa2d 100644
--- a/templates/explore/code.tmpl
+++ b/templates/explore/code.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content explore users">
 	{{template "explore/navbar" .}}
 	<div class="ui container">
-		{{template "code/searchcombo" .}}
+		{{template "shared/search/code/search" .}}
 	</div>
 </div>
 {{template "base/footer" .}}
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index c51dcaa3ff..17c8bcb6a3 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -58,7 +58,7 @@
 		</div>
 	{{else}}
 	<div>
-		{{ctx.Locale.Tr "explore.repo_no_results"}}
+		{{ctx.Locale.Tr "search.no_results"}}
 	</div>
 	{{end}}
 </div>
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index 2bb5f319d1..54d995989a 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -1,12 +1,13 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
+<div class="ui small secondary filter menu gt-ac gt-mx-0">
 	<form class="ui form ignore-dirty gt-f1">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
+		{{if .PageIsExploreUsers}}
+			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
+		{{else}}
+			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}}
+		{{end}}
 	</form>
 	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
+	<div class="ui small dropdown type jump item gt-mr-0">
 		<span class="text">
 			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 		</span>
diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl
index 0d661d53cb..fb86fbbea2 100644
--- a/templates/explore/user_list.tmpl
+++ b/templates/explore/user_list.tmpl
@@ -26,6 +26,8 @@
 			</div>
 		</div>
 	{{else}}
-		<div class="flex-item">{{ctx.Locale.Tr "explore.user_no_results"}}</div>
+		<div class="flex-item">
+			{{ctx.Locale.Tr "search.no_results"}}
+		</div>
 	{{end}}
 </div>
diff --git a/templates/explore/users.tmpl b/templates/explore/users.tmpl
index 7e15ae3d47..e9046125c6 100644
--- a/templates/explore/users.tmpl
+++ b/templates/explore/users.tmpl
@@ -3,9 +3,7 @@
 	{{template "explore/navbar" .}}
 	<div class="ui container">
 		{{template "explore/search" .}}
-
 		{{template "explore/user_list" .}}
-
 		{{template "base/paginate" .}}
 	</div>
 </div>
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index adaf83ae15..02220a917a 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -14,7 +14,7 @@
 							<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
 							<div id="search-user-box" class="ui search gt-mr-3"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
 								<div class="ui input">
-									<input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
+									<input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
 								</div>
 							</div>
 							<button class="ui primary button">{{ctx.Locale.Tr "org.teams.add_team_member"}}</button>
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl
index 5a32eea64f..bd38cda6d1 100644
--- a/templates/org/team/repositories.tmpl
+++ b/templates/org/team/repositories.tmpl
@@ -14,7 +14,7 @@
 							{{.CsrfTokenHtml}}
 							<div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search">
 								<div class="ui input">
-									<input class="prompt" name="repo_name" placeholder="{{ctx.Locale.Tr "org.teams.search_repo_placeholder"}}" autocomplete="off" required>
+									<input class="prompt" name="repo_name" placeholder="{{ctx.Locale.Tr "search.repo_kind"}}" autocomplete="off" required>
 								</div>
 							</div>
 							<button class="ui primary button gt-ml-3">{{ctx.Locale.Tr "add"}}</button>
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index 09205b19a5..a9ee023061 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -1,16 +1,16 @@
 {{template "base/alert" .}}
 {{if .HasPackages}}
 <form class="ui form ignore-dirty">
-	<div class="ui fluid action input">
-		{{template "shared/searchinput" dict "Value" .Query}}
-		<select class="ui dropdown" name="type">
+	<div class="ui small fluid action input">
+		{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
+		<select class="ui small dropdown" name="type">
 			<option value="">{{ctx.Locale.Tr "packages.filter.type"}}</option>
 			<option value="all">{{ctx.Locale.Tr "packages.filter.type.all"}}</option>
 			{{range $type := .AvailableTypes}}
 			<option{{if eq $.PackageType $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option>
 			{{end}}
 		</select>
-		<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+		{{template "shared/search/button"}}
 	</div>
 </form>
 {{end}}
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl
index 59d6d89b53..fc34ccc938 100644
--- a/templates/package/shared/versionlist.tmpl
+++ b/templates/package/shared/versionlist.tmpl
@@ -1,21 +1,21 @@
 <p><a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a> / <strong>{{ctx.Locale.Tr "packages.versions"}}</strong></p>
 <form class="ui form ignore-dirty">
-	<div class="ui fluid action input">
-		{{template "shared/searchinput" dict "Value" .Query}}
-		<select class="ui dropdown" name="sort">
+	<div class="ui small fluid action input">
+		{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
+		<select class="ui small dropdown" name="sort">
 			<option value="version_asc"{{if eq .Sort "version_asc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "filter.string.asc"}}</option>
 			<option value="version_desc"{{if eq .Sort "version_desc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "filter.string.desc"}}</option>
 			<option value="created_asc"{{if eq .Sort "created_asc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</option>
 			<option value="created_desc"{{if or (eq .Sort "") (eq .Sort "created_desc")}} selected="selected"{{end}}>{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</option>
 		</select>
 		{{if eq .PackageDescriptor.Package.Type "container"}}
-		<select class="ui dropdown" name="tagged">
+		<select class="ui small dropdown" name="tagged">
 			{{$isTagged := or (eq .Tagged "") (eq .Tagged "tagged")}}
 			<option value="tagged"{{if $isTagged}} selected="selected"{{end}}>{{ctx.Locale.Tr "packages.filter.container.tagged"}}</option>
 			<option value="untagged"{{if not $isTagged}} selected="selected"{{end}}>{{ctx.Locale.Tr "packages.filter.container.untagged"}}</option>
 		</select>
 		{{end}}
-		<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+		{{template "shared/search/button"}}
 	</div>
 </form>
 <div>
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index 54a41221bf..414c9dca2e 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -21,13 +21,8 @@
 <div class="list-header">
 	<!-- Search -->
 	<form class="list-header-search ui form ignore-dirty">
-		<div class="ui small search fluid action input">
-			<input type="hidden" name="state" value="{{$.State}}">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui small icon button" type="submit" aria-label="{{ctx.Locale.Tr "explore.search"}}">
-				{{svg "octicon-search"}}
-			</button>
-		</div>
+		<input type="hidden" name="state" value="{{$.State}}">
+		{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}}
 	</form>
 	<!-- Sort -->
 	<div class="list-header-sort ui small dropdown type jump item">
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 916111faca..48c14cf343 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -67,140 +67,136 @@
 			</div>
 		{{end}}
 
-		{{if .Branches}}
-			<h4 class="ui top attached header gt-df gt-ac gt-sb">
-				<div class="gt-df gt-ac">
-					{{ctx.Locale.Tr "repo.branches"}}
-				</div>
-				<div class="tw-whitespace-nowrap">
-					<form class="ignore-dirty" method="get">
-						<div class="ui tiny search input">
-							<input name="q" placeholder="{{ctx.Locale.Tr "repo.branch.search"}}" value="{{.Keyword}}" autofocus>
-						</div>
-						<button class="ui primary tiny button gt-mr-0" data-tooltip-content={{ctx.Locale.Tr "repo.commits.search.tooltip"}}>{{ctx.Locale.Tr "repo.commits.find"}}</button>
-					</form>
-				</div>
-			</h4>
-
-			<div class="ui attached table segment">
-				<table class="ui very basic striped fixed table single line">
-					<tbody>
-						{{range .Branches}}
-							<tr>
-								<td class="eight wide">
-								{{if .DBBranch.IsDeleted}}
-									<div class="flex-text-block">
-										<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
-										<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
-									</div>
-									<p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix ctx.Locale}}</p>
-								{{else}}
-									<div class="flex-text-block">
-										{{if .IsProtected}}{{svg "octicon-shield-lock"}}{{end}}
-										<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
-										<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
-										{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
-									</div>
-									<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
-								{{end}}
-								</td>
-								<td class="two wide ui">
-									{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
-									<div class="commit-divergence">
-										<div class="bar-group">
-											<div class="count count-behind">{{.CommitsBehind}}</div>
-											{{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}}
-											<div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
-										</div>
-										<div class="bar-group">
-											<div class="count count-ahead">{{.CommitsAhead}}</div>
-											<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
-										</div>
-									</div>
-									{{end}}
-								</td>
-								<td class="two wide right aligned">
-									{{if not .LatestPullRequest}}
-										{{if .IsIncluded}}
-											<span class="ui orange large label" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.included_desc"}}">
-												{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}}
-											</span>
-										{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
-										<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
-											<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
-										</a>
-										{{end}}
-									{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}}
-										{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
-										<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
-											<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
-										</a>
-										{{end}}
-									{{else}}
-										<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
-										{{if .LatestPullRequest.HasMerged}}
-											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
-										{{else if .LatestPullRequest.Issue.IsClosed}}
-											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
-										{{else}}
-											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
-										{{end}}
-									{{end}}
-								</td>
-								<td class="three wide right aligned overflow-visible">
-									{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
-										<button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal"
-											data-branch-from="{{.DBBranch.Name}}"
-											data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}"
-											data-tooltip-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}"
-											data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}"
-										>
-											{{svg "octicon-git-branch"}}
-										</button>
-									{{end}}
-									{{if $.EnableFeed}}
-										<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a>
-									{{end}}
-									{{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}}
-										<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}">
-											{{svg "octicon-download"}}
-											<div class="menu">
-												<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
-												<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a>
-											</div>
-										</div>
-									{{end}}
-									{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}}
-										<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
-											data-is-default-branch="false"
-											data-old-branch-name="{{.DBBranch.Name}}"
-											data-modal="#rename-branch-modal"
-											data-tooltip-content="{{ctx.Locale.Tr "repo.branch.rename" (.DBBranch.Name)}}"
-										>
-											{{svg "octicon-pencil"}}
-										</button>
-									{{end}}
-									{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}}
-										{{if .DBBranch.IsDeleted}}
-											<button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}">
-												<span class="text blue">
-													{{svg "octicon-reply"}}
-												</span>
-											</button>
-										{{else}}
-											<button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}">
-												{{svg "octicon-trash"}}
-											</button>
-										{{end}}
-									{{end}}
-								</td>
-							</tr>
-						{{end}}
-					</tbody>
-				</table>
+		<h4 class="ui top attached header gt-df gt-ac gt-sb">
+			<div class="gt-df gt-ac">
+				{{ctx.Locale.Tr "repo.branches"}}
 			</div>
-			{{template "base/paginate" .}}
-		{{end}}
+		</h4>
+
+		<div class="ui attached segment">
+			<form class="ignore-dirty" method="get">
+				{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.branch_kind")}}
+			</form>
+		</div>
+
+		<div class="ui attached table segment">
+			<table class="ui very basic striped fixed table single line">
+				<tbody>
+					{{range .Branches}}
+						<tr>
+							<td class="eight wide">
+							{{if .DBBranch.IsDeleted}}
+								<div class="flex-text-block">
+									<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
+									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+								</div>
+								<p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix ctx.Locale}}</p>
+							{{else}}
+								<div class="flex-text-block">
+									{{if .IsProtected}}{{svg "octicon-shield-lock"}}{{end}}
+									<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
+									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
+								</div>
+								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
+							{{end}}
+							</td>
+							<td class="two wide ui">
+								{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
+								<div class="commit-divergence">
+									<div class="bar-group">
+										<div class="count count-behind">{{.CommitsBehind}}</div>
+										{{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}}
+										<div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
+									</div>
+									<div class="bar-group">
+										<div class="count count-ahead">{{.CommitsAhead}}</div>
+										<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
+									</div>
+								</div>
+								{{end}}
+							</td>
+							<td class="two wide right aligned">
+								{{if not .LatestPullRequest}}
+									{{if .IsIncluded}}
+										<span class="ui orange large label" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.included_desc"}}">
+											{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}}
+										</span>
+									{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
+									<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
+										<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
+									</a>
+									{{end}}
+								{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}}
+									{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
+									<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
+										<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
+									</a>
+									{{end}}
+								{{else}}
+									<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
+									{{if .LatestPullRequest.HasMerged}}
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
+									{{else if .LatestPullRequest.Issue.IsClosed}}
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
+									{{else}}
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
+									{{end}}
+								{{end}}
+							</td>
+							<td class="three wide right aligned overflow-visible">
+								{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
+									<button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal"
+										data-branch-from="{{.DBBranch.Name}}"
+										data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}"
+										data-tooltip-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}"
+										data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}"
+									>
+										{{svg "octicon-git-branch"}}
+									</button>
+								{{end}}
+								{{if $.EnableFeed}}
+									<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a>
+								{{end}}
+								{{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}}
+									<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}">
+										{{svg "octicon-download"}}
+										<div class="menu">
+											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
+											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a>
+										</div>
+									</div>
+								{{end}}
+								{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}}
+									<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
+										data-is-default-branch="false"
+										data-old-branch-name="{{.DBBranch.Name}}"
+										data-modal="#rename-branch-modal"
+										data-tooltip-content="{{ctx.Locale.Tr "repo.branch.rename" (.DBBranch.Name)}}"
+									>
+										{{svg "octicon-pencil"}}
+									</button>
+								{{end}}
+								{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}}
+									{{if .DBBranch.IsDeleted}}
+										<button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}">
+											<span class="text blue">
+												{{svg "octicon-reply"}}
+											</span>
+										</button>
+									{{else}}
+										<button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}">
+											{{svg "octicon-trash"}}
+										</button>
+									{{end}}
+								{{end}}
+							</td>
+						</tr>
+					{{end}}
+				</tbody>
+			</table>
+		</div>
+		{{template "base/paginate" .}}
 	</div>
 </div>
 
diff --git a/templates/repo/commits_search_dropdown.tmpl b/templates/repo/commits_search_dropdown.tmpl
new file mode 100644
index 0000000000..5aa3f4f320
--- /dev/null
+++ b/templates/repo/commits_search_dropdown.tmpl
@@ -0,0 +1,8 @@
+<div class="ui small dropdown selection">
+	<input name="all" type="hidden" value="{{.All}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+	<div class="text">{{if .All}}{{ctx.Locale.Tr "repo.commits.search_all"}}{{else}}{{ctx.Locale.Tr "repo.commits.search_branch"}}{{end}}</div>
+	<div class="menu">
+		<div class="item" data-value="false">{{ctx.Locale.Tr "repo.commits.search_branch"}}</div>
+		<div class="item" data-value="true">{{ctx.Locale.Tr "repo.commits.search_all"}}</div>
+	</div>
+</div>
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index 70f673e27e..221ee8d99b 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -8,27 +8,27 @@
 			{{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}}
 		{{end}}
 	</div>
-	<div class="commits-table-right tw-whitespace-nowrap">
-		{{if .PageIsCommits}}
-			<form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search">
-				<div class="ui tiny search input">
-					<input name="q" placeholder="{{ctx.Locale.Tr "repo.commits.search"}}" value="{{.Keyword}}" autofocus>
-				</div>
-
-				<div class="ui tiny checkbox">
-					<input type="checkbox" name="all" value="true" {{.All}}>
-					<label>{{ctx.Locale.Tr "repo.commits.search_all"}}</label>
-				</div>
-				<button class="ui primary tiny button gt-mr-0" data-panel="#add-deploy-key-panel" data-tooltip-content={{ctx.Locale.Tr "repo.commits.search.tooltip"}}>{{ctx.Locale.Tr "repo.commits.find"}}</button>
-			</form>
-		{{else if .IsDiffCompare}}
+	{{if .IsDiffCompare}}
+		<div class="commits-table-right tw-whitespace-nowrap">
 			<a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a>
 			...
 			<a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a>
-		{{end}}
-	</div>
+		</div>
+	{{end}}
 </h4>
 
+{{if .PageIsCommits}}
+	<div class="ui attached segment">
+		<form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search">
+			<div class="ui small fluid action input">
+				{{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.commit_kind")}}
+				{{template "repo/commits_search_dropdown" .}}
+				{{template "shared/search/button" dict "Tooltip" (ctx.Locale.Tr "repo.commits.search.tooltip")}}
+			</div>
+		</form>
+	</div>
+{{end}}
+
 {{if and .Commits (gt .CommitCount 0)}}
 	{{template "repo/commits_list" .}}
 {{end}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index a98dcb8c28..ef10904bcc 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -15,14 +15,12 @@
 				<div class="ui repo-search">
 					<form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get">
 						<div class="field">
-							<div class="ui small action input{{if .CodeIndexerUnavailable}} disabled left icon{{end}}"{{if .CodeIndexerUnavailable}} data-tooltip-content="{{ctx.Locale.Tr "repo.search.code_search_unavailable"}}"{{end}}>
-								<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
+							<div class="ui small action input{{if .CodeIndexerUnavailable}} disabled left icon{{end}}"{{if .CodeIndexerUnavailable}} data-tooltip-content="{{ctx.Locale.Tr "search.code_search_unavailable"}}"{{end}}>
+								<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
 								{{if .CodeIndexerUnavailable}}
 									<i class="icon">{{svg "octicon-alert"}}</i>
 								{{end}}
-								<button class="ui small icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">
-									{{svg "octicon-search"}}
-								</button>
+								{{template "shared/search/button" dict "Disabled" .CodeIndexerUnavailable}}
 							</div>
 						</div>
 					</form>
diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl
index 361f16fd3b..4727b26154 100644
--- a/templates/repo/issue/search.tmpl
+++ b/templates/repo/issue/search.tmpl
@@ -9,10 +9,10 @@
 			<input type="hidden" name="assignee" value="{{$.AssigneeID}}">
 			<input type="hidden" name="poster" value="{{$.PosterID}}">
 		{{end}}
-		{{template "shared/searchinput" dict "Value" .Keyword}}
+		{{template "shared/search/input" dict "Value" .Keyword}}
 		{{if .PageIsIssueList}}
 			<button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}" data-repo-link="{{.RepoLink}}">{{svg "octicon-hash"}}</button>
 		{{end}}
-		<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button>
+		{{template "shared/search/button"}}
 	</div>
 </form>
diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl
index 7513d444cc..3f5b22b0ce 100644
--- a/templates/repo/search.tmpl
+++ b/templates/repo/search.tmpl
@@ -2,59 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository file list">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="ui repo-search">
-			<form class="ui form ignore-dirty" method="get">
-				<div class="ui fluid action input">
-					<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
-					<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.search.type.tooltip"}}">
-						<input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-						<div class="text">{{ctx.Locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div>
-						<div class="menu">
-							<div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "repo.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "repo.search.fuzzy"}}</div>
-							<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "repo.search.match.tooltip"}}">{{ctx.Locale.Tr "repo.search.match"}}</div>
-						</div>
-					</div>
-					<button class="ui icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">{{svg "octicon-search" 16}}</button>
-				</div>
-			</form>
-		</div>
-		{{if .CodeIndexerUnavailable}}
-			<div class="ui error message">
-				<p>{{ctx.Locale.Tr "repo.search.code_search_unavailable"}}</p>
-			</div>
-		{{else if .Keyword}}
-			<h3>
-				{{ctx.Locale.Tr "repo.search.results" .Keyword .RepoLink .RepoName}}
-			</h3>
-			{{if .SearchResults}}
-				<div class="flex-text-block gt-fw">
-					{{range $term := .SearchResultLanguages}}
-					<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0" href="{{$.SourcePath}}/search?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}">
-						<i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i>
-						{{$term.Language}}
-						<div class="detail">{{$term.Count}}</div>
-					</a>
-					{{end}}
-				</div>
-				<div class="repository search">
-					{{range $result := .SearchResults}}
-						<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
-							<h4 class="ui top attached normal header gt-df gt-fw">
-								<span class="file gt-f1">{{.Filename}}</span>
-								<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments .Filename}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
-							</h4>
-							<div class="ui attached table segment">
-								{{template "shared/searchfile" dict "RepoLink" $.SourcePath "SearchResult" .}}
-							</div>
-							{{template "shared/searchbottom" dict "root" $ "result" .}}
-						</div>
-					{{end}}
-				</div>
-				{{template "base/paginate" .}}
-			{{else}}
-				<div>{{ctx.Locale.Tr "repo.search.code_no_results"}}</div>
-			{{end}}
-		{{end}}
+		{{template "shared/search/code/search" .}}
 	</div>
 </div>
 {{template "base/footer" .}}
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index 19abf1bee8..d7b5c96bab 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -42,7 +42,7 @@
 			<form class="ui form" id="repo-collab-form" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<div id="search-user-box" class="ui search input gt-vm">
-					<input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" autofocus required>
+					<input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
 				</div>
 				<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_collaborator"}}</button>
 			</form>
@@ -90,7 +90,7 @@
 				<form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post">
 					{{.CsrfTokenHtml}}
 					<div id="search-team-box" class="ui search input gt-vm" data-org-name="{{.OrgName}}">
-						<input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "repo.settings.search_team"}}" autocomplete="off" autofocus required>
+						<input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
 					</div>
 					<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_team"}}</button>
 				</form>
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index 6bbcc9f6ec..e95dd831c9 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -52,7 +52,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_users"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="whitelist_users" value="{{.whitelist_users}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 								<div class="menu">
 									{{range .Users}}
 										<div class="item" data-value="{{.ID}}">
@@ -67,7 +67,7 @@
 								<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_teams"}}</label>
 								<div class="ui multiple search selection dropdown">
 									<input type="hidden" name="whitelist_teams" value="{{.whitelist_teams}}">
-									<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+									<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 									<div class="menu">
 										{{range .Teams}}
 											<div class="item" data-value="{{.ID}}">
@@ -113,7 +113,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_approvals_whitelist_users"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="approvals_whitelist_users" value="{{.approvals_whitelist_users}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 								<div class="menu">
 								{{range .Users}}
 									<div class="item" data-value="{{.ID}}">
@@ -128,7 +128,7 @@
 								<label>{{ctx.Locale.Tr "repo.settings.protect_approvals_whitelist_teams"}}</label>
 								<div class="ui multiple search selection dropdown">
 									<input type="hidden" name="approvals_whitelist_teams" value="{{.approvals_whitelist_teams}}">
-									<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+									<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 									<div class="menu">
 									{{range .Teams}}
 										<div class="item" data-value="{{.ID}}">
@@ -210,7 +210,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_users"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="merge_whitelist_users" value="{{.merge_whitelist_users}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 								<div class="menu">
 								{{range .Users}}
 									<div class="item" data-value="{{.ID}}">
@@ -225,7 +225,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_teams"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="merge_whitelist_teams" value="{{.merge_whitelist_teams}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 								<div class="menu">
 								{{range .Teams}}
 									<div class="item" data-value="{{.ID}}">
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl
index 31fb59e5e3..4c196f0f99 100644
--- a/templates/repo/settings/tags.tmpl
+++ b/templates/repo/settings/tags.tmpl
@@ -28,7 +28,7 @@
 									<label>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.users"}}</label>
 									<div class="ui multiple search selection dropdown">
 										<input type="hidden" name="allowlist_users" value="{{.allowlist_users}}">
-										<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+										<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 										<div class="menu">
 											{{range .Users}}
 												<div class="item" data-value="{{.ID}}">
@@ -43,7 +43,7 @@
 										<label>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.teams"}}</label>
 										<div class="ui multiple search selection dropdown">
 											<input type="hidden" name="allowlist_teams" value="{{.allowlist_teams}}">
-											<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+											<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 											<div class="menu">
 												{{range .Teams}}
 													<div class="item" data-value="{{.ID}}">
diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl
index 443c455faf..8163007993 100644
--- a/templates/shared/actions/runner_list.tmpl
+++ b/templates/shared/actions/runner_list.tmpl
@@ -33,11 +33,7 @@
 	</h4>
 	<div class="ui attached segment">
 		<form class="ui form ignore-dirty" id="user-list-search-form" action="{{$.Link}}">
-			<!-- Search Text -->
-			<div class="ui fluid action input">
-				{{template "shared/searchinput" dict "Value" .Keyword}}
-				<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-			</div>
+			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.runner_kind")}}
 		</form>
 	</div>
 	<div class="ui attached table segment">
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index cffe579741..fb502413fa 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -153,7 +153,7 @@
 	{{end}}
 	{{if .IssueIndexerUnavailable}}
 		<div class="ui error message">
-			<p>{{ctx.Locale.Tr "repo.issues.keyword_search_unavailable"}}</p>
+			<p>{{ctx.Locale.Tr "search.keyword_search_unavailable"}}</p>
 		</div>
 	{{end}}
 </div>
diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo_search.tmpl
index 2ea4bfaad7..7ba0070863 100644
--- a/templates/shared/repo_search.tmpl
+++ b/templates/shared/repo_search.tmpl
@@ -1,17 +1,17 @@
-<div class="ui secondary filter menu">
-	<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-flex-row tw-gap-x-2">
+<div class="ui small secondary filter menu">
+	<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-flex-row tw-gap-x-2 gt-ac">
 		{{if .Language}}<input hidden name="language" value="{{.Language}}">{{end}}
-		<div class="ui fluid action input tw-flex-1">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
+		<div class="ui small fluid action input tw-flex-1">
+			{{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.repo_kind")}}
 			{{if .PageIsExploreRepositories}}
 				<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">
 			{{else if .TabName}}
 				<input type="hidden" name="tab" value="{{.TabName}}">
 			{{end}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+			{{template "shared/search/button"}}
 		</div>
 		<!-- Filter -->
-		<div class="ui dropdown type jump item tw-mr-0">
+		<div class="ui small dropdown type jump item tw-mr-0">
 			<span class="text">
 				{{ctx.Locale.Tr "filter"}}
 			</span>
@@ -36,7 +36,7 @@
 			</div>
 		</div>
 		<!-- Sort -->
-		<div class="ui dropdown type jump item gt-mr-0">
+		<div class="ui small dropdown type jump item gt-mr-0">
 			<span class="text">
 				{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 			</span>
@@ -65,3 +65,4 @@
 		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}</span>
 	</div>
 {{end}}
+<div class="divider"></div>
diff --git a/templates/shared/search/button.tmpl b/templates/shared/search/button.tmpl
new file mode 100644
index 0000000000..7bb1662e15
--- /dev/null
+++ b/templates/shared/search/button.tmpl
@@ -0,0 +1,3 @@
+{{/* Disable (optional) - if search button has to be disabled */}}
+{{/* Tooltip (optional) - a tooltip to be displayed on hover */}}
+<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "search.search"}}" {{with .Tooltip}}data-tooltip-content="{{.}}"{{end}}{{if .Disabled}} disabled{{end}}>{{svg "octicon-search"}}</button>
diff --git a/templates/code/searchresults.tmpl b/templates/shared/search/code/results.tmpl
similarity index 70%
rename from templates/code/searchresults.tmpl
rename to templates/shared/search/code/results.tmpl
index 08bb12951d..de5ee0c311 100644
--- a/templates/code/searchresults.tmpl
+++ b/templates/shared/search/code/results.tmpl
@@ -1,6 +1,7 @@
 <div class="flex-text-block gt-fw">
 	{{range $term := .SearchResultLanguages}}
-	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0" href="{{AppSubUrl}}{{if $.ContextUser}}/{{$.ContextUser.Name}}/-/code{{else}}/explore/code{{end}}?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}">
+	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0"
+		href="{{$.Link}}?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&fuzzy={{$.IsFuzzy}}">
 		<i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i>
 		{{$term.Language}}
 		<div class="detail">{{$term.Count}}</div>
@@ -9,16 +10,20 @@
 </div>
 <div class="repository search">
 	{{range $result := .SearchResults}}
-		{{$repo := (index $.RepoMaps .RepoID)}}
+		{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
 		<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
 			<h4 class="ui top attached normal header gt-df gt-fw">
-				<span class="file gt-f1">
-					<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>
+				{{if not $.Repo}}
+					<span class="file gt-f1">
+						<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>
 						{{if $repo.IsArchived}}
 							<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
 						{{end}}
-					- {{.Filename}}
-				</span>
+						- {{.Filename}}
+					</span>
+				{{else}}
+					<span class="file gt-f1">{{.Filename}}</span>
+				{{end}}
 				<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
 			</h4>
 			<div class="ui attached table segment">
@@ -28,3 +33,4 @@
 		</div>
 	{{end}}
 </div>
+{{template "base/paginate" .}}
diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl
new file mode 100644
index 0000000000..545ec1ea65
--- /dev/null
+++ b/templates/shared/search/code/search.tmpl
@@ -0,0 +1,15 @@
+<form class="ui form ignore-dirty">
+	{{template "shared/search/combo_fuzzy" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind")}}
+</form>
+<div class="divider"></div>
+<div class="ui user list">
+	{{if .CodeIndexerUnavailable}}
+		<div class="ui error message">
+			<p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p>
+		</div>
+	{{else if .SearchResults}}
+		{{template "shared/search/code/results" .}}
+	{{else if .Keyword}}
+		<div>{{ctx.Locale.Tr "search.no_results"}}</div>
+	{{end}}
+</div>
diff --git a/templates/shared/search/combo.tmpl b/templates/shared/search/combo.tmpl
new file mode 100644
index 0000000000..788db95cc1
--- /dev/null
+++ b/templates/shared/search/combo.tmpl
@@ -0,0 +1,8 @@
+{{/* Value - value of the search field (for search results page) */}}
+{{/* Disabled (optional) - if search field/button has to be disabled */}}
+{{/* Placeholder (optional) - placeholder text to be used */}}
+{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}}
+<div class="ui small fluid action input">
+	{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
+	{{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
+</div>
diff --git a/templates/shared/search/combo_fuzzy.tmpl b/templates/shared/search/combo_fuzzy.tmpl
new file mode 100644
index 0000000000..3540a89ecb
--- /dev/null
+++ b/templates/shared/search/combo_fuzzy.tmpl
@@ -0,0 +1,10 @@
+{{/* Value - value of the search field (for search results page) */}}
+{{/* Disabled (optional) - if search field/button has to be disabled */}}
+{{/* Placeholder (optional) - placeholder text to be used */}}
+{{/* IsFuzzy - state of the fuzzy search toggle */}}
+{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}}
+<div class="ui small fluid action input">
+	{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
+	{{template "shared/search/fuzzy" dict "Disabled" .Disabled "IsFuzzy" .IsFuzzy}}
+	{{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
+</div>
diff --git a/templates/shared/search/fuzzy.tmpl b/templates/shared/search/fuzzy.tmpl
new file mode 100644
index 0000000000..6ddb03c004
--- /dev/null
+++ b/templates/shared/search/fuzzy.tmpl
@@ -0,0 +1,10 @@
+{{/* Disabled (optional) - if dropdown has to be disabled */}}
+{{/* IsFuzzy - state of the fuzzy search toggle */}}
+<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
+	<input name="fuzzy" type="hidden"{{if .Disabled}} disabled{{end}} value="{{.IsFuzzy}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+	<div class="text">{{if .IsFuzzy}}{{ctx.Locale.Tr "search.fuzzy"}}{{else}}{{ctx.Locale.Tr "search.match"}}{{end}}</div>
+	<div class="menu">
+		<div class="item" data-value="true" data-tooltip-content="{{ctx.Locale.Tr "search.fuzzy_tooltip"}}">{{ctx.Locale.Tr "search.fuzzy"}}</div>
+		<div class="item" data-value="false" data-tooltip-content="{{ctx.Locale.Tr "search.match_tooltip"}}">{{ctx.Locale.Tr "search.match"}}</div>
+	</div>
+</div>
diff --git a/templates/shared/search/input.tmpl b/templates/shared/search/input.tmpl
new file mode 100644
index 0000000000..195cefc2f6
--- /dev/null
+++ b/templates/shared/search/input.tmpl
@@ -0,0 +1,4 @@
+{{/* Value - value of the search field (for search results page) */}}
+{{/* Disabled (optional) - if search field has to be disabled */}}
+{{/* Placeholder (optional) - placeholder text to be used */}}
+<input type="search" spellcheck="false" name="q" maxlength="255" placeholder="{{with .Placeholder}}{{.}}{{else}}{{ctx.Locale.Tr "search.search"}}{{end}}"{{with .Value}} value="{{.}}"{{end}}{{if .Disabled}} disabled{{end}}>
diff --git a/templates/shared/searchinput.tmpl b/templates/shared/searchinput.tmpl
deleted file mode 100644
index 48b288c299..0000000000
--- a/templates/shared/searchinput.tmpl
+++ /dev/null
@@ -1 +0,0 @@
-<input type="search" spellcheck="false" name="q" maxlength="255" placeholder="{{ctx.Locale.Tr "explore.search"}}…"{{if .Value}} value="{{.Value}}"{{end}}{{if .Disabled}} disabled{{end}}>
diff --git a/templates/user/code.tmpl b/templates/user/code.tmpl
index f71f55c474..ff6c69d615 100644
--- a/templates/user/code.tmpl
+++ b/templates/user/code.tmpl
@@ -3,7 +3,7 @@
 	<div role="main" aria-label="{{.Title}}" class="page-content organization code">
 		{{template "org/header" .}}
 		<div class="ui container">
-			{{template "code/searchcombo" .}}
+			{{template "shared/search/code/search" .}}
 		</div>
 	</div>
 {{else}}
@@ -15,7 +15,7 @@
 				</div>
 				<div class="ui twelve wide column">
 					{{template "user/overview/header" .}}
-					{{template "code/searchcombo" .}}
+					{{template "shared/search/code/search" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 7b7023cfaa..0fbf9a7361 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -50,9 +50,9 @@
 							<input type="hidden" name="type" value="{{$.ViewType}}">
 							<input type="hidden" name="sort" value="{{$.SortType}}">
 							<input type="hidden" name="state" value="{{$.State}}">
-							{{template "shared/searchinput" dict "Value" $.Keyword}}
+							{{template "shared/search/input" dict "Value" $.Keyword}}
 							<button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}">{{svg "octicon-hash"}}</button>
-							<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button>
+							{{template "shared/search/button"}}
 						</div>
 					</form>
 					<!-- Sort -->
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index fd684fcabf..7b62c9fc27 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -46,14 +46,11 @@
 						</a>
 					</div>
 					<form class="list-header-search ui form ignore-dirty">
-						<div class="ui small search fluid action input">
-							<input type="hidden" name="type" value="{{$.ViewType}}">
+						<input type="hidden" name="type" value="{{$.ViewType}}">
 							<input type="hidden" name="repos" value="[{{range $.RepoIDs}}{{.}},{{end}}]">
 							<input type="hidden" name="sort" value="{{$.SortType}}">
 							<input type="hidden" name="state" value="{{$.State}}">
-							{{template "shared/searchinput" dict "Value" $.Keyword}}
-							<button class="ui small icon button" type="submit" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button>
-						</div>
+						{{template "shared/search/combo" dict "Value" $.Keyword}}
 					</form>
 					<!-- Sort -->
 					<div class="list-header-sort ui dropdown type jump item">
diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl
index ed6f906822..a879f1fb9d 100644
--- a/templates/user/dashboard/repolist.tmpl
+++ b/templates/user/dashboard/repolist.tmpl
@@ -9,7 +9,7 @@ const data = {
 	textOrganization: {{ctx.Locale.Tr "organization"}},
 	textMyRepos: {{ctx.Locale.Tr "home.my_repos"}},
 	textNewRepo: {{ctx.Locale.Tr "new_repo"}},
-	textSearchRepos: {{ctx.Locale.Tr "home.search_repos"}},
+	textSearchRepos: {{ctx.Locale.Tr "search.repo_kind"}},
 	textFilter: {{ctx.Locale.Tr "home.filter"}},
 	textShowArchived: {{ctx.Locale.Tr "home.show_archived"}},
 	textShowPrivate: {{ctx.Locale.Tr "home.show_private"}},
diff --git a/web_src/css/base.css b/web_src/css/base.css
index e53e0619c8..1c6b3fa488 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -282,16 +282,26 @@ ol.ui.list li,
 .ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
   min-width: 10em;
 }
-
-.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus,:hover) {
-  border-right-color: transparent;
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) {
+  border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover {
+  border-color: var(--color-input-border);
+}
+.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible {
+  border-bottom-left-radius: 0 !important;
+  border-bottom-right-radius: 0 !important;
 }
-
 .ui.action.input:not([class*="left action"]) > input,
 .ui.action.input:not([class*="left action"]) > input:hover {
-  border-right: 1px solid transparent;
+  border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .button:hover {
+  border-left-color: var(--color-primary);
 }
-
 .ui.action.input:not([class*="left action"]) > input:focus {
   border-right-color: var(--color-primary);
 }
@@ -610,10 +620,6 @@ ol.ui.list li,
   border-color: var(--color-primary);
 }
 
-.ui.selection.dropdown .menu {
-  margin: 0 -1.25px;
-}
-
 .ui.pointing.dropdown > .menu:not(.hidden)::after {
   background: var(--color-menu);
   box-shadow: -1px -1px 0 0 var(--color-secondary);
diff --git a/web_src/css/form.css b/web_src/css/form.css
index 1580a0b4cc..ca65b677d7 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -41,6 +41,11 @@ textarea,
   color: var(--color-input-text);
 }
 
+/* fix fomantic small dropdown having inconsistent padding with input */
+.ui.small.selection.dropdown {
+  padding: .67857143em 3.2em .67857143em 1em;
+}
+
 input:hover,
 textarea:hover,
 .ui.input input:hover,
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 6f742bbea0..2e8f335ce5 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -355,7 +355,7 @@ export default sfc; // activate the IDE's Vue plugin
         </a>
       </h4>
       <div class="ui attached segment repos-search">
-        <div class="ui fluid action left icon input" :class="{loading: isLoading}">
+        <div class="ui small fluid action left icon input" :class="{loading: isLoading}">
           <input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
           <i class="icon"><svg-icon name="octicon-search" :size="16"/></i>
           <div class="ui dropdown icon button" :title="textFilter">

From 4a377c033608d64a7a3e352d1a55955dcc988f87 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 15 Mar 2024 00:33:06 +0100
Subject: [PATCH 377/679] Update JS dependences (#29797)

Update all non-excluded JS deps, tested monaco, swagger and mermaid.
---
 package-lock.json | 1339 ++++++++++++++++++++++++++++-----------------
 package.json      |   24 +-
 2 files changed, 844 insertions(+), 519 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f75e3068b8..8f5e6d0ca9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,9 +5,9 @@
   "packages": {
     "": {
       "dependencies": {
-        "@citation-js/core": "0.7.6",
-        "@citation-js/plugin-bibtex": "0.7.8",
-        "@citation-js/plugin-csl": "0.7.6",
+        "@citation-js/core": "0.7.9",
+        "@citation-js/plugin-bibtex": "0.7.9",
+        "@citation-js/plugin-csl": "0.7.9",
         "@citation-js/plugin-software-formats": "0.6.1",
         "@claviska/jquery-minicolors": "2.3.6",
         "@github/markdown-toolbar-element": "2.2.3",
@@ -27,7 +27,7 @@
         "dayjs": "1.11.10",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
-        "esbuild-loader": "4.0.3",
+        "esbuild-loader": "4.1.0",
         "escape-goat": "4.0.0",
         "fast-glob": "3.3.2",
         "htmx.org": "1.9.10",
@@ -35,10 +35,10 @@
         "jquery": "3.7.1",
         "katex": "0.16.9",
         "license-checker-webpack-plugin": "0.2.1",
-        "mermaid": "10.8.0",
+        "mermaid": "10.9.0",
         "mini-css-extract-plugin": "2.8.1",
         "minimatch": "9.0.3",
-        "monaco-editor": "0.46.0",
+        "monaco-editor": "0.47.0",
         "monaco-editor-webpack-plugin": "7.1.0",
         "pdfobject": "2.3.0",
         "postcss": "8.4.35",
@@ -46,7 +46,7 @@
         "postcss-nesting": "12.1.0",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
-        "swagger-ui-dist": "5.11.8",
+        "swagger-ui-dist": "5.12.0",
         "tailwindcss": "3.4.1",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
@@ -67,7 +67,7 @@
         "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
         "@playwright/test": "1.42.1",
         "@stoplight/spectral-cli": "6.11.0",
-        "@stylistic/eslint-plugin-js": "1.6.3",
+        "@stylistic/eslint-plugin-js": "1.7.0",
         "@stylistic/stylelint-plugin": "2.1.0",
         "@vitejs/plugin-vue": "5.0.4",
         "eslint": "8.57.0",
@@ -77,12 +77,12 @@
         "eslint-plugin-jquery": "1.5.1",
         "eslint-plugin-no-jquery": "2.7.0",
         "eslint-plugin-no-use-extend-native": "0.5.0",
-        "eslint-plugin-regexp": "2.2.0",
+        "eslint-plugin-regexp": "2.3.0",
         "eslint-plugin-sonarjs": "0.24.0",
         "eslint-plugin-unicorn": "51.0.1",
-        "eslint-plugin-vitest": "0.3.22",
+        "eslint-plugin-vitest": "0.3.26",
         "eslint-plugin-vitest-globals": "1.4.0",
-        "eslint-plugin-vue": "9.22.0",
+        "eslint-plugin-vue": "9.23.0",
         "eslint-plugin-vue-scoped-css": "2.7.2",
         "eslint-plugin-wc": "2.0.4",
         "jsdom": "24.0.0",
@@ -92,7 +92,7 @@
         "stylelint-declaration-block-no-ignored-properties": "2.8.0",
         "stylelint-declaration-strict-value": "1.10.4",
         "svgo": "3.2.0",
-        "updates": "15.1.2",
+        "updates": "15.3.1",
         "vite-string-plugin": "1.1.5",
         "vitest": "1.3.1"
       },
@@ -323,9 +323,9 @@
       "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
     },
     "node_modules/@citation-js/core": {
-      "version": "0.7.6",
-      "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.6.tgz",
-      "integrity": "sha512-qbB6RjwSsx/AjlCSAqoWKN05VxpjADYe8GmnPJnRB7QeNiVmqaRc8NSQDdvQ+4qhCkQOtMH15Sa2Nde4cvlXhw==",
+      "version": "0.7.9",
+      "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.9.tgz",
+      "integrity": "sha512-fSbkB32JayDChZnAYC/kB+sWHRvxxL7ibVetyBOyzOc+5aCnjb6UVsbcfhnkOIEyAMoRRvWDyFmakEoTtA5ttQ==",
       "dependencies": {
         "@citation-js/date": "^0.5.0",
         "@citation-js/name": "^0.4.2",
@@ -353,9 +353,9 @@
       }
     },
     "node_modules/@citation-js/plugin-bibtex": {
-      "version": "0.7.8",
-      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.8.tgz",
-      "integrity": "sha512-20fUXe1zm1oCONFflGj3mgIk6DHspPjWrBirGfsyHmVSR/4xqnSbrqtztLiV15zt3tbKLepTaHm3ZTrcLOK0MA==",
+      "version": "0.7.9",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.9.tgz",
+      "integrity": "sha512-gIJpCd6vmmTOcRfDrSOjtoNhw2Mi94UwFxmgJ7GwkXyTYcNheW5VlMMo1tlqjakJGARQ0eOsKcI57gSPqJSS2g==",
       "dependencies": {
         "@citation-js/date": "^0.5.0",
         "@citation-js/name": "^0.4.2",
@@ -381,9 +381,9 @@
       }
     },
     "node_modules/@citation-js/plugin-csl": {
-      "version": "0.7.6",
-      "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.6.tgz",
-      "integrity": "sha512-H/dhzU56+D71Hzjto1x9PDtvsWaiI+Dx6Jj1vjiFtCCnbU/Zvqo5xFZNPstee+hFE6AsJ2xYlI8QujrGH+V1pQ==",
+      "version": "0.7.9",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.9.tgz",
+      "integrity": "sha512-mbD7CnUiPOuVnjeJwo+d0RGUcY0PE8n01gHyjq0qpTeS42EGmQ9+LzqfsTUVWWBndTwc6zLRuIF1qFAUHKE4oA==",
       "dependencies": {
         "@citation-js/date": "^0.5.0",
         "citeproc": "^2.4.6"
@@ -466,9 +466,9 @@
       }
     },
     "node_modules/@csstools/css-parser-algorithms": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.0.tgz",
-      "integrity": "sha512-YfEHq0eRH98ffb5/EsrrDspVWAuph6gDggAE74ZtjecsmyyWpW768hOyiONa8zwWGbIWYfa2Xp4tRTrpQQ00CQ==",
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
+      "integrity": "sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==",
       "dev": true,
       "funding": [
         {
@@ -484,13 +484,13 @@
         "node": "^14 || ^16 || >=18"
       },
       "peerDependencies": {
-        "@csstools/css-tokenizer": "^2.2.3"
+        "@csstools/css-tokenizer": "^2.2.4"
       }
     },
     "node_modules/@csstools/css-tokenizer": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz",
-      "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==",
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz",
+      "integrity": "sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw==",
       "dev": true,
       "funding": [
         {
@@ -507,9 +507,9 @@
       }
     },
     "node_modules/@csstools/media-query-list-parser": {
-      "version": "2.1.8",
-      "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.8.tgz",
-      "integrity": "sha512-DiD3vG5ciNzeuTEoh74S+JMjQDs50R3zlxHnBnfd04YYfA/kh2KiBCGhzqLxlJcNq+7yNQ3stuZZYLX6wK/U2g==",
+      "version": "2.1.9",
+      "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz",
+      "integrity": "sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA==",
       "dev": true,
       "funding": [
         {
@@ -525,8 +525,8 @@
         "node": "^14 || ^16 || >=18"
       },
       "peerDependencies": {
-        "@csstools/css-parser-algorithms": "^2.6.0",
-        "@csstools/css-tokenizer": "^2.2.3"
+        "@csstools/css-parser-algorithms": "^2.6.1",
+        "@csstools/css-tokenizer": "^2.2.4"
       }
     },
     "node_modules/@csstools/selector-resolve-nested": {
@@ -580,9 +580,9 @@
       }
     },
     "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
-      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
+      "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
       "cpu": [
         "ppc64"
       ],
@@ -595,9 +595,9 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
-      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
+      "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
       "cpu": [
         "arm"
       ],
@@ -610,9 +610,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
-      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
+      "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
       "cpu": [
         "arm64"
       ],
@@ -625,9 +625,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
-      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
+      "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
       "cpu": [
         "x64"
       ],
@@ -640,9 +640,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
-      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
+      "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
       "cpu": [
         "arm64"
       ],
@@ -655,9 +655,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
-      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
+      "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
       "cpu": [
         "x64"
       ],
@@ -670,9 +670,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
-      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
+      "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
       "cpu": [
         "arm64"
       ],
@@ -685,9 +685,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
-      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
+      "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
       "cpu": [
         "x64"
       ],
@@ -700,9 +700,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
-      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
+      "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
       "cpu": [
         "arm"
       ],
@@ -715,9 +715,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
-      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
+      "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
       "cpu": [
         "arm64"
       ],
@@ -730,9 +730,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
-      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
+      "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
       "cpu": [
         "ia32"
       ],
@@ -745,9 +745,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
-      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
+      "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
       "cpu": [
         "loong64"
       ],
@@ -760,9 +760,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
-      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
+      "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
       "cpu": [
         "mips64el"
       ],
@@ -775,9 +775,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
-      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
+      "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
       "cpu": [
         "ppc64"
       ],
@@ -790,9 +790,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
-      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
+      "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
       "cpu": [
         "riscv64"
       ],
@@ -805,9 +805,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
-      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
+      "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
       "cpu": [
         "s390x"
       ],
@@ -820,9 +820,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
-      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
+      "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
       "cpu": [
         "x64"
       ],
@@ -835,9 +835,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
+      "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
       "cpu": [
         "x64"
       ],
@@ -850,9 +850,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
+      "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
       "cpu": [
         "x64"
       ],
@@ -865,9 +865,9 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
-      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
+      "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
       "cpu": [
         "x64"
       ],
@@ -880,9 +880,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
-      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
+      "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
       "cpu": [
         "arm64"
       ],
@@ -895,9 +895,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
-      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
+      "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
       "cpu": [
         "ia32"
       ],
@@ -910,9 +910,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
-      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
+      "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
       "cpu": [
         "x64"
       ],
@@ -1250,12 +1250,12 @@
       }
     },
     "node_modules/@jridgewell/source-map": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
-      "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+      "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
       "dependencies": {
-        "@jridgewell/gen-mapping": "^0.3.0",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25"
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
@@ -1480,9 +1480,9 @@
       "dev": true
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz",
-      "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz",
+      "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==",
       "cpu": [
         "arm"
       ],
@@ -1493,9 +1493,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz",
-      "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz",
+      "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==",
       "cpu": [
         "arm64"
       ],
@@ -1506,9 +1506,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz",
-      "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz",
+      "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==",
       "cpu": [
         "arm64"
       ],
@@ -1519,9 +1519,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz",
-      "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz",
+      "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==",
       "cpu": [
         "x64"
       ],
@@ -1532,9 +1532,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz",
-      "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz",
+      "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==",
       "cpu": [
         "arm"
       ],
@@ -1545,9 +1545,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz",
-      "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz",
+      "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==",
       "cpu": [
         "arm64"
       ],
@@ -1558,9 +1558,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz",
-      "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz",
+      "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==",
       "cpu": [
         "arm64"
       ],
@@ -1571,9 +1571,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz",
-      "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz",
+      "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==",
       "cpu": [
         "riscv64"
       ],
@@ -1584,9 +1584,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz",
-      "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz",
+      "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==",
       "cpu": [
         "x64"
       ],
@@ -1597,9 +1597,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz",
-      "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz",
+      "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==",
       "cpu": [
         "x64"
       ],
@@ -1610,9 +1610,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz",
-      "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz",
+      "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==",
       "cpu": [
         "arm64"
       ],
@@ -1623,9 +1623,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz",
-      "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz",
+      "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==",
       "cpu": [
         "ia32"
       ],
@@ -1636,9 +1636,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz",
-      "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz",
+      "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==",
       "cpu": [
         "x64"
       ],
@@ -2106,9 +2106,9 @@
       "dev": true
     },
     "node_modules/@stylistic/eslint-plugin-js": {
-      "version": "1.6.3",
-      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.3.tgz",
-      "integrity": "sha512-ckdz51oHxD2FaxgY2piJWJVJiwgp8Uu96s+as2yB3RMwavn3nHBrpliVukXY9S/DmMicPRB2+H8nBk23GDG+qA==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.7.0.tgz",
+      "integrity": "sha512-PN6On/+or63FGnhhMKSQfYcWutRlzOiYlVdLM6yN7lquoBTqUJHYnl4TA4MHwiAt46X5gRxDr1+xPZ1lOLcL+Q==",
       "dev": true,
       "dependencies": {
         "@types/eslint": "^8.56.2",
@@ -2256,9 +2256,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.24",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
-      "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
+      "version": "20.11.27",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz",
+      "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -2301,16 +2301,16 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz",
-      "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz",
+      "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "7.1.0",
-        "@typescript-eslint/type-utils": "7.1.0",
-        "@typescript-eslint/utils": "7.1.0",
-        "@typescript-eslint/visitor-keys": "7.1.0",
+        "@typescript-eslint/scope-manager": "7.2.0",
+        "@typescript-eslint/type-utils": "7.2.0",
+        "@typescript-eslint/utils": "7.2.0",
+        "@typescript-eslint/visitor-keys": "7.2.0",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
@@ -2336,15 +2336,15 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz",
-      "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz",
+      "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.1.0",
-        "@typescript-eslint/types": "7.1.0",
-        "@typescript-eslint/typescript-estree": "7.1.0",
-        "@typescript-eslint/visitor-keys": "7.1.0",
+        "@typescript-eslint/scope-manager": "7.2.0",
+        "@typescript-eslint/types": "7.2.0",
+        "@typescript-eslint/typescript-estree": "7.2.0",
+        "@typescript-eslint/visitor-keys": "7.2.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2364,13 +2364,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz",
-      "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz",
+      "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.1.0",
-        "@typescript-eslint/visitor-keys": "7.1.0"
+        "@typescript-eslint/types": "7.2.0",
+        "@typescript-eslint/visitor-keys": "7.2.0"
       },
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2381,13 +2381,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz",
-      "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz",
+      "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.1.0",
-        "@typescript-eslint/utils": "7.1.0",
+        "@typescript-eslint/typescript-estree": "7.2.0",
+        "@typescript-eslint/utils": "7.2.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2408,9 +2408,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz",
-      "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz",
+      "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2421,13 +2421,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz",
-      "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz",
+      "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.1.0",
-        "@typescript-eslint/visitor-keys": "7.1.0",
+        "@typescript-eslint/types": "7.2.0",
+        "@typescript-eslint/visitor-keys": "7.2.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2449,17 +2449,17 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz",
-      "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz",
+      "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "7.1.0",
-        "@typescript-eslint/types": "7.1.0",
-        "@typescript-eslint/typescript-estree": "7.1.0",
+        "@typescript-eslint/scope-manager": "7.2.0",
+        "@typescript-eslint/types": "7.2.0",
+        "@typescript-eslint/typescript-estree": "7.2.0",
         "semver": "^7.5.4"
       },
       "engines": {
@@ -2474,12 +2474,12 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz",
-      "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz",
+      "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.1.0",
+        "@typescript-eslint/types": "7.2.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2579,9 +2579,9 @@
       }
     },
     "node_modules/@vitest/snapshot/node_modules/magic-string": {
-      "version": "0.30.7",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
-      "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+      "version": "0.30.8",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
       "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2670,9 +2670,9 @@
       }
     },
     "node_modules/@vue/compiler-sfc/node_modules/magic-string": {
-      "version": "0.30.7",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
-      "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+      "version": "0.30.8",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
       },
@@ -2734,9 +2734,9 @@
       "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
     },
     "node_modules/@webassemblyjs/ast": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
-      "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
+      "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
       "dependencies": {
         "@webassemblyjs/helper-numbers": "1.11.6",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
@@ -2753,9 +2753,9 @@
       "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
     },
     "node_modules/@webassemblyjs/helper-buffer": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
-      "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
+      "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
     },
     "node_modules/@webassemblyjs/helper-numbers": {
       "version": "1.11.6",
@@ -2773,14 +2773,14 @@
       "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
     },
     "node_modules/@webassemblyjs/helper-wasm-section": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
-      "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
+      "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
-        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
+        "@webassemblyjs/helper-buffer": "1.12.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.11.6"
+        "@webassemblyjs/wasm-gen": "1.12.1"
       }
     },
     "node_modules/@webassemblyjs/ieee754": {
@@ -2805,26 +2805,26 @@
       "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
     },
     "node_modules/@webassemblyjs/wasm-edit": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
-      "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
+      "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
-        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
+        "@webassemblyjs/helper-buffer": "1.12.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/helper-wasm-section": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.11.6",
-        "@webassemblyjs/wasm-opt": "1.11.6",
-        "@webassemblyjs/wasm-parser": "1.11.6",
-        "@webassemblyjs/wast-printer": "1.11.6"
+        "@webassemblyjs/helper-wasm-section": "1.12.1",
+        "@webassemblyjs/wasm-gen": "1.12.1",
+        "@webassemblyjs/wasm-opt": "1.12.1",
+        "@webassemblyjs/wasm-parser": "1.12.1",
+        "@webassemblyjs/wast-printer": "1.12.1"
       }
     },
     "node_modules/@webassemblyjs/wasm-gen": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
-      "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
+      "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
         "@webassemblyjs/ieee754": "1.11.6",
         "@webassemblyjs/leb128": "1.11.6",
@@ -2832,22 +2832,22 @@
       }
     },
     "node_modules/@webassemblyjs/wasm-opt": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
-      "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
+      "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
-        "@webassemblyjs/helper-buffer": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.11.6",
-        "@webassemblyjs/wasm-parser": "1.11.6"
+        "@webassemblyjs/ast": "1.12.1",
+        "@webassemblyjs/helper-buffer": "1.12.1",
+        "@webassemblyjs/wasm-gen": "1.12.1",
+        "@webassemblyjs/wasm-parser": "1.12.1"
       }
     },
     "node_modules/@webassemblyjs/wasm-parser": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
-      "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
+      "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
         "@webassemblyjs/helper-api-error": "1.11.6",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
         "@webassemblyjs/ieee754": "1.11.6",
@@ -2856,11 +2856,11 @@
       }
     },
     "node_modules/@webassemblyjs/wast-printer": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
-      "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
+      "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
         "@xtuc/long": "4.2.2"
       }
     },
@@ -3430,11 +3430,14 @@
       }
     },
     "node_modules/binary-extensions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
       "engines": {
         "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/boolbase": {
@@ -3584,9 +3587,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001591",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
-      "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
+      "version": "1.0.30001597",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
+      "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
       "funding": [
         {
           "type": "opencollective",
@@ -4149,9 +4152,9 @@
       }
     },
     "node_modules/d3": {
-      "version": "7.8.5",
-      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
-      "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+      "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
       "dependencies": {
         "d3-array": "3",
         "d3-axis": "3",
@@ -4356,9 +4359,9 @@
       }
     },
     "node_modules/d3-geo": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
-      "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+      "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
       "dependencies": {
         "d3-array": "2.5.0 - 3"
       },
@@ -4468,9 +4471,9 @@
       }
     },
     "node_modules/d3-scale-chromatic": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
-      "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
       "dependencies": {
         "d3-color": "1 - 3",
         "d3-interpolate": "1 - 3"
@@ -4876,9 +4879,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.690",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz",
-      "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA=="
+      "version": "1.4.706",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.706.tgz",
+      "integrity": "sha512-fO01fufoGd6jKK3HR8ofBapF3ZPfgxNJ/ua9xQAhFu93TwWIs4d+weDn3kje3GB4S7aGUTfk5nvdU5F7z5mF9Q=="
     },
     "node_modules/elkjs": {
       "version": "0.9.2",
@@ -4899,9 +4902,9 @@
       }
     },
     "node_modules/enhanced-resolve": {
-      "version": "5.15.1",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz",
-      "integrity": "sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg==",
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz",
+      "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==",
       "dependencies": {
         "graceful-fs": "^4.2.4",
         "tapable": "^2.2.0"
@@ -5124,9 +5127,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
-      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
+      "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
@@ -5135,37 +5138,37 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.19.12",
-        "@esbuild/android-arm": "0.19.12",
-        "@esbuild/android-arm64": "0.19.12",
-        "@esbuild/android-x64": "0.19.12",
-        "@esbuild/darwin-arm64": "0.19.12",
-        "@esbuild/darwin-x64": "0.19.12",
-        "@esbuild/freebsd-arm64": "0.19.12",
-        "@esbuild/freebsd-x64": "0.19.12",
-        "@esbuild/linux-arm": "0.19.12",
-        "@esbuild/linux-arm64": "0.19.12",
-        "@esbuild/linux-ia32": "0.19.12",
-        "@esbuild/linux-loong64": "0.19.12",
-        "@esbuild/linux-mips64el": "0.19.12",
-        "@esbuild/linux-ppc64": "0.19.12",
-        "@esbuild/linux-riscv64": "0.19.12",
-        "@esbuild/linux-s390x": "0.19.12",
-        "@esbuild/linux-x64": "0.19.12",
-        "@esbuild/netbsd-x64": "0.19.12",
-        "@esbuild/openbsd-x64": "0.19.12",
-        "@esbuild/sunos-x64": "0.19.12",
-        "@esbuild/win32-arm64": "0.19.12",
-        "@esbuild/win32-ia32": "0.19.12",
-        "@esbuild/win32-x64": "0.19.12"
+        "@esbuild/aix-ppc64": "0.20.2",
+        "@esbuild/android-arm": "0.20.2",
+        "@esbuild/android-arm64": "0.20.2",
+        "@esbuild/android-x64": "0.20.2",
+        "@esbuild/darwin-arm64": "0.20.2",
+        "@esbuild/darwin-x64": "0.20.2",
+        "@esbuild/freebsd-arm64": "0.20.2",
+        "@esbuild/freebsd-x64": "0.20.2",
+        "@esbuild/linux-arm": "0.20.2",
+        "@esbuild/linux-arm64": "0.20.2",
+        "@esbuild/linux-ia32": "0.20.2",
+        "@esbuild/linux-loong64": "0.20.2",
+        "@esbuild/linux-mips64el": "0.20.2",
+        "@esbuild/linux-ppc64": "0.20.2",
+        "@esbuild/linux-riscv64": "0.20.2",
+        "@esbuild/linux-s390x": "0.20.2",
+        "@esbuild/linux-x64": "0.20.2",
+        "@esbuild/netbsd-x64": "0.20.2",
+        "@esbuild/openbsd-x64": "0.20.2",
+        "@esbuild/sunos-x64": "0.20.2",
+        "@esbuild/win32-arm64": "0.20.2",
+        "@esbuild/win32-ia32": "0.20.2",
+        "@esbuild/win32-x64": "0.20.2"
       }
     },
     "node_modules/esbuild-loader": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.0.3.tgz",
-      "integrity": "sha512-YpaSRisj7TSg6maKKKG9OJGGm0BZ7EXeov8J8cXEYdugjlAJ0wL7aj2JactoQvPJ113v2Ar204pdJWrZsAQc8Q==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.1.0.tgz",
+      "integrity": "sha512-543TtIvqbqouEMlOHg4xKoDQkmdImlwIpyAIgpUtDPvMuklU/c2k+Qt2O3VeDBgAwozxmlEbjOzV+F8CZ0g+Bw==",
       "dependencies": {
-        "esbuild": "^0.19.0",
+        "esbuild": "^0.20.0",
         "get-tsconfig": "^4.7.0",
         "loader-utils": "^2.0.4",
         "webpack-sources": "^1.4.3"
@@ -5698,9 +5701,9 @@
       }
     },
     "node_modules/eslint-plugin-regexp": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.2.0.tgz",
-      "integrity": "sha512-0kwpiWiLRVBkVr3oIRQLl196sXP/NF6DQFefv9jtR4ZOgQR+6WID2pIZ0I+wIt54qgBPwBB7Gm2a+ueh8/WsFQ==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.3.0.tgz",
+      "integrity": "sha512-T8JUs7ssRGbuXb+CGfdUJbcxTBMCNOpNqNBLuC8JUKAEIez72J37RaOi5/4dAUsGz92GbWVtqTLPSJZGyP/sQA==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
@@ -5764,12 +5767,12 @@
       }
     },
     "node_modules/eslint-plugin-vitest": {
-      "version": "0.3.22",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.22.tgz",
-      "integrity": "sha512-atkFGQ7aVgcuSeSMDqnyevIyUpfBPMnosksgEPrKE7Y8xQlqG/5z2IQ6UDau05zXaaFv7Iz8uzqvIuKshjZ0Zw==",
+      "version": "0.3.26",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.26.tgz",
+      "integrity": "sha512-oxe5JSPgRjco8caVLTh7Ti8PxpwJdhSV0hTQAmkFcNcmy/9DnqLB/oNVRA11RmVRP//2+jIIT6JuBEcpW3obYg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/utils": "^6.21.0"
+        "@typescript-eslint/utils": "^7.1.1"
       },
       "engines": {
         "node": "^18.0.0 || >= 20.0.0"
@@ -5793,110 +5796,10 @@
       "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==",
       "dev": true
     },
-    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
-      "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0"
-      },
-      "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      }
-    },
-    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/types": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
-      "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
-      "dev": true,
-      "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      }
-    },
-    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
-      "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0",
-        "debug": "^4.3.4",
-        "globby": "^11.1.0",
-        "is-glob": "^4.0.3",
-        "minimatch": "9.0.3",
-        "semver": "^7.5.4",
-        "ts-api-utils": "^1.0.1"
-      },
-      "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/utils": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
-      "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
-      "dev": true,
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "@types/json-schema": "^7.0.12",
-        "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.21.0",
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/typescript-estree": "6.21.0",
-        "semver": "^7.5.4"
-      },
-      "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
-      }
-    },
-    "node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
-      "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
-        "eslint-visitor-keys": "^3.4.1"
-      },
-      "engines": {
-        "node": "^16.0.0 || >=18.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      }
-    },
     "node_modules/eslint-plugin-vue": {
-      "version": "9.22.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.22.0.tgz",
-      "integrity": "sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==",
+      "version": "9.23.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.23.0.tgz",
+      "integrity": "sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
@@ -6533,9 +6436,9 @@
       }
     },
     "node_modules/get-tsconfig": {
-      "version": "4.7.2",
-      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
-      "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+      "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
       "dependencies": {
         "resolve-pkg-maps": "^1.0.0"
       },
@@ -6808,9 +6711,9 @@
       "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="
     },
     "node_modules/hasown": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
-      "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
       "dependencies": {
         "function-bind": "^1.1.2"
       },
@@ -7052,9 +6955,9 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     "node_modules/ini": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
-      "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz",
+      "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==",
       "dev": true,
       "engines": {
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
@@ -7292,10 +7195,13 @@
       }
     },
     "node_modules/is-map": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
-      "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+      "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
       "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -7405,10 +7311,13 @@
       }
     },
     "node_modules/is-set": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
-      "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+      "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
       "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -7495,10 +7404,13 @@
       }
     },
     "node_modules/is-weakmap": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
-      "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+      "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
       "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -7516,13 +7428,16 @@
       }
     },
     "node_modules/is-weakset": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
-      "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
+      "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.1"
+        "call-bind": "^1.0.7",
+        "get-intrinsic": "^1.2.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -8364,9 +8279,9 @@
       }
     },
     "node_modules/mermaid": {
-      "version": "10.8.0",
-      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.8.0.tgz",
-      "integrity": "sha512-9CzfSreRjdDJxX796+jW4zjEq0DVw5xVF0nWsqff8OTbrt+ml0TZ5PyYUjjUZJa2NYxYJZZXewEquxGiM8qZEA==",
+      "version": "10.9.0",
+      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz",
+      "integrity": "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==",
       "dependencies": {
         "@braintree/sanitize-url": "^6.0.1",
         "@types/d3-scale": "^4.0.3",
@@ -8379,6 +8294,7 @@
         "dayjs": "^1.11.7",
         "dompurify": "^3.0.5",
         "elkjs": "^0.9.0",
+        "katex": "^0.16.9",
         "khroma": "^2.0.0",
         "lodash-es": "^4.17.21",
         "mdast-util-from-markdown": "^1.3.0",
@@ -8925,9 +8841,9 @@
       }
     },
     "node_modules/monaco-editor": {
-      "version": "0.46.0",
-      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz",
-      "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ=="
+      "version": "0.47.0",
+      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz",
+      "integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw=="
     },
     "node_modules/monaco-editor-webpack-plugin": {
       "version": "7.1.0",
@@ -9912,9 +9828,9 @@
       }
     },
     "node_modules/postcss-selector-parser": {
-      "version": "6.0.15",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
-      "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
+      "version": "6.0.16",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
+      "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -10518,13 +10434,13 @@
       }
     },
     "node_modules/safe-array-concat": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
-      "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+      "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
-        "get-intrinsic": "^1.2.2",
+        "call-bind": "^1.0.7",
+        "get-intrinsic": "^1.2.4",
         "has-symbols": "^1.0.3",
         "isarray": "^2.0.5"
       },
@@ -10655,17 +10571,17 @@
       }
     },
     "node_modules/seroval": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.4.tgz",
-      "integrity": "sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.5.tgz",
+      "integrity": "sha512-TM+Z11tHHvQVQKeNlOUonOWnsNM+2IBwZ4vwoi4j3zKzIpc5IDw8WPwCfcc8F17wy6cBcJGbZbFOR0UCuTZHQA==",
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/seroval-plugins": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.4.tgz",
-      "integrity": "sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.5.tgz",
+      "integrity": "sha512-8+pDC1vOedPXjKG7oz8o+iiHrtF2WswaMQJ7CKFpccvSYfrzmvKY9zOJWCg+881722wIHfwkdnRmiiDm9ym+zQ==",
       "engines": {
         "node": ">=10"
       },
@@ -10674,17 +10590,17 @@
       }
     },
     "node_modules/set-function-length": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
-      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.1.2",
+        "define-data-property": "^1.1.4",
         "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.3",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -11471,9 +11387,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.11.8",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.8.tgz",
-      "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA=="
+      "version": "5.12.0",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.12.0.tgz",
+      "integrity": "sha512-Rt1xUpbHulJVGbiQjq9yy9/r/0Pg6TmpcG+fXTaMePDc8z5WUw4LfaWts5qcNv/8ewPvBIbY7DKq7qReIKNCCQ=="
     },
     "node_modules/symbol-tree": {
       "version": "3.2.4",
@@ -11615,9 +11531,9 @@
       }
     },
     "node_modules/terser": {
-      "version": "5.28.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz",
-      "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==",
+      "version": "5.29.2",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
+      "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
@@ -11842,9 +11758,9 @@
       "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ=="
     },
     "node_modules/ts-api-utils": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz",
-      "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
       "dev": true,
       "engines": {
         "node": ">=16"
@@ -12003,9 +11919,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.3.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
-      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+      "version": "5.4.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
+      "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
       "devOptional": true,
       "peer": true,
       "bin": {
@@ -12109,9 +12025,9 @@
       }
     },
     "node_modules/updates": {
-      "version": "15.1.2",
-      "resolved": "https://registry.npmjs.org/updates/-/updates-15.1.2.tgz",
-      "integrity": "sha512-+/JT4NChl82iexV9G80TY5HF3ubQ5O9UTOk3LlCo4Y4aRCYvo1h4bJE8YkP0PE7KiFRWIQq/rPmUYrY2QF8wVA==",
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/updates/-/updates-15.3.1.tgz",
+      "integrity": "sha512-DqHT1aJ6p6jVLWRiAeuVx/TQotvEwUjgrY1Mlc0a2qYk+eKEQVXugQ4M+6QoVMA3X1NFAVsb02d93pmWam4bBA==",
       "dev": true,
       "bin": {
         "updates": "bin/updates.js"
@@ -12207,9 +12123,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.1.4",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
-      "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz",
+      "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==",
       "dev": true,
       "dependencies": {
         "esbuild": "^0.19.3",
@@ -12289,12 +12205,418 @@
       "integrity": "sha512-KRCIFX3PWVUuEjpi9O7EKLT9E27OqOA3RimIvVx6cziLAUxvnk2VvHQfMrP+mKkqyqqSmnnYyTig3OyDnK/zlA==",
       "dev": true
     },
+    "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/android-arm": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/android-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/android-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/darwin-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-arm": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-ia32": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-loong64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-s390x": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/linux-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/sunos-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/win32-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/win32-ia32": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/vite/node_modules/@esbuild/win32-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/vite/node_modules/@types/estree": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
       "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
       "dev": true
     },
+    "node_modules/vite/node_modules/esbuild": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
+      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.19.12",
+        "@esbuild/android-arm": "0.19.12",
+        "@esbuild/android-arm64": "0.19.12",
+        "@esbuild/android-x64": "0.19.12",
+        "@esbuild/darwin-arm64": "0.19.12",
+        "@esbuild/darwin-x64": "0.19.12",
+        "@esbuild/freebsd-arm64": "0.19.12",
+        "@esbuild/freebsd-x64": "0.19.12",
+        "@esbuild/linux-arm": "0.19.12",
+        "@esbuild/linux-arm64": "0.19.12",
+        "@esbuild/linux-ia32": "0.19.12",
+        "@esbuild/linux-loong64": "0.19.12",
+        "@esbuild/linux-mips64el": "0.19.12",
+        "@esbuild/linux-ppc64": "0.19.12",
+        "@esbuild/linux-riscv64": "0.19.12",
+        "@esbuild/linux-s390x": "0.19.12",
+        "@esbuild/linux-x64": "0.19.12",
+        "@esbuild/netbsd-x64": "0.19.12",
+        "@esbuild/openbsd-x64": "0.19.12",
+        "@esbuild/sunos-x64": "0.19.12",
+        "@esbuild/win32-arm64": "0.19.12",
+        "@esbuild/win32-ia32": "0.19.12",
+        "@esbuild/win32-x64": "0.19.12"
+      }
+    },
     "node_modules/vite/node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -12310,9 +12632,9 @@
       }
     },
     "node_modules/vite/node_modules/rollup": {
-      "version": "4.12.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz",
-      "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
+      "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==",
       "dev": true,
       "dependencies": {
         "@types/estree": "1.0.5"
@@ -12325,19 +12647,19 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.12.0",
-        "@rollup/rollup-android-arm64": "4.12.0",
-        "@rollup/rollup-darwin-arm64": "4.12.0",
-        "@rollup/rollup-darwin-x64": "4.12.0",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.12.0",
-        "@rollup/rollup-linux-arm64-gnu": "4.12.0",
-        "@rollup/rollup-linux-arm64-musl": "4.12.0",
-        "@rollup/rollup-linux-riscv64-gnu": "4.12.0",
-        "@rollup/rollup-linux-x64-gnu": "4.12.0",
-        "@rollup/rollup-linux-x64-musl": "4.12.0",
-        "@rollup/rollup-win32-arm64-msvc": "4.12.0",
-        "@rollup/rollup-win32-ia32-msvc": "4.12.0",
-        "@rollup/rollup-win32-x64-msvc": "4.12.0",
+        "@rollup/rollup-android-arm-eabi": "4.13.0",
+        "@rollup/rollup-android-arm64": "4.13.0",
+        "@rollup/rollup-darwin-arm64": "4.13.0",
+        "@rollup/rollup-darwin-x64": "4.13.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.13.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.13.0",
+        "@rollup/rollup-linux-arm64-musl": "4.13.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.13.0",
+        "@rollup/rollup-linux-x64-gnu": "4.13.0",
+        "@rollup/rollup-linux-x64-musl": "4.13.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.13.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.13.0",
+        "@rollup/rollup-win32-x64-msvc": "4.13.0",
         "fsevents": "~2.3.2"
       }
     },
@@ -12407,9 +12729,9 @@
       }
     },
     "node_modules/vitest/node_modules/magic-string": {
-      "version": "0.30.7",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
-      "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+      "version": "0.30.8",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
       "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -12535,9 +12857,9 @@
       }
     },
     "node_modules/watchpack": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
-      "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
+      "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
       "dependencies": {
         "glob-to-regexp": "^0.4.1",
         "graceful-fs": "^4.1.2"
@@ -12849,31 +13171,34 @@
       }
     },
     "node_modules/which-collection": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
-      "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+      "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
       "dev": true,
       "dependencies": {
-        "is-map": "^2.0.1",
-        "is-set": "^2.0.1",
-        "is-weakmap": "^2.0.1",
-        "is-weakset": "^2.0.1"
+        "is-map": "^2.0.3",
+        "is-set": "^2.0.3",
+        "is-weakmap": "^2.0.2",
+        "is-weakset": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/which-typed-array": {
-      "version": "1.1.14",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz",
-      "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==",
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+      "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
       "dev": true,
       "dependencies": {
-        "available-typed-arrays": "^1.0.6",
-        "call-bind": "^1.0.5",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
         "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.1"
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -13062,9 +13387,9 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/yaml": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz",
-      "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
+      "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
       "bin": {
         "yaml": "bin.mjs"
       },
diff --git a/package.json b/package.json
index 26632480e8..d4aec0e88e 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,9 @@
     "node": ">= 18.0.0"
   },
   "dependencies": {
-    "@citation-js/core": "0.7.6",
-    "@citation-js/plugin-bibtex": "0.7.8",
-    "@citation-js/plugin-csl": "0.7.6",
+    "@citation-js/core": "0.7.9",
+    "@citation-js/plugin-bibtex": "0.7.9",
+    "@citation-js/plugin-csl": "0.7.9",
     "@citation-js/plugin-software-formats": "0.6.1",
     "@claviska/jquery-minicolors": "2.3.6",
     "@github/markdown-toolbar-element": "2.2.3",
@@ -26,7 +26,7 @@
     "dayjs": "1.11.10",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
-    "esbuild-loader": "4.0.3",
+    "esbuild-loader": "4.1.0",
     "escape-goat": "4.0.0",
     "fast-glob": "3.3.2",
     "htmx.org": "1.9.10",
@@ -34,10 +34,10 @@
     "jquery": "3.7.1",
     "katex": "0.16.9",
     "license-checker-webpack-plugin": "0.2.1",
-    "mermaid": "10.8.0",
+    "mermaid": "10.9.0",
     "mini-css-extract-plugin": "2.8.1",
     "minimatch": "9.0.3",
-    "monaco-editor": "0.46.0",
+    "monaco-editor": "0.47.0",
     "monaco-editor-webpack-plugin": "7.1.0",
     "pdfobject": "2.3.0",
     "postcss": "8.4.35",
@@ -45,7 +45,7 @@
     "postcss-nesting": "12.1.0",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
-    "swagger-ui-dist": "5.11.8",
+    "swagger-ui-dist": "5.12.0",
     "tailwindcss": "3.4.1",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
@@ -66,7 +66,7 @@
     "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
     "@playwright/test": "1.42.1",
     "@stoplight/spectral-cli": "6.11.0",
-    "@stylistic/eslint-plugin-js": "1.6.3",
+    "@stylistic/eslint-plugin-js": "1.7.0",
     "@stylistic/stylelint-plugin": "2.1.0",
     "@vitejs/plugin-vue": "5.0.4",
     "eslint": "8.57.0",
@@ -76,12 +76,12 @@
     "eslint-plugin-jquery": "1.5.1",
     "eslint-plugin-no-jquery": "2.7.0",
     "eslint-plugin-no-use-extend-native": "0.5.0",
-    "eslint-plugin-regexp": "2.2.0",
+    "eslint-plugin-regexp": "2.3.0",
     "eslint-plugin-sonarjs": "0.24.0",
     "eslint-plugin-unicorn": "51.0.1",
-    "eslint-plugin-vitest": "0.3.22",
+    "eslint-plugin-vitest": "0.3.26",
     "eslint-plugin-vitest-globals": "1.4.0",
-    "eslint-plugin-vue": "9.22.0",
+    "eslint-plugin-vue": "9.23.0",
     "eslint-plugin-vue-scoped-css": "2.7.2",
     "eslint-plugin-wc": "2.0.4",
     "jsdom": "24.0.0",
@@ -91,7 +91,7 @@
     "stylelint-declaration-block-no-ignored-properties": "2.8.0",
     "stylelint-declaration-strict-value": "1.10.4",
     "svgo": "3.2.0",
-    "updates": "15.1.2",
+    "updates": "15.3.1",
     "vite-string-plugin": "1.1.5",
     "vitest": "1.3.1"
   },

From 256a1eeb9a67b18c62a10f5909b584b7b220848a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 15 Mar 2024 03:05:31 +0100
Subject: [PATCH 378/679] Add `<overflow-menu>`, rename webcomponents (#29400)

1. Add `<overflow-menu>` web component
2. Rename `<gitea-origin-url>` to `<origin-url>` and make filenames
match.

<img width="439" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/2fbe4ca4-110b-4ad2-8e17-c1e116ccbd74">

<img width="444" alt="Screenshot 2024-03-02 at 21 36 52"
src="https://github.com/go-gitea/gitea/assets/115237/aa8f786e-dc8c-4030-b12d-7cfb74bdfd6e">

<img width="537" alt="Screenshot 2024-03-03 at 03 05 06"
src="https://github.com/go-gitea/gitea/assets/115237/fddd50aa-adf1-4b4b-bd7f-caf30c7b2245">


![image](https://github.com/go-gitea/gitea/assets/115237/0f43770c-834c-4a05-8e3d-d30eb8653786)


![image](https://github.com/go-gitea/gitea/assets/115237/4b4c6bd7-843f-4f49-808f-6b3aed5e9f9a)

TODO:

- [x] Check if removal of `requestAnimationFrame` is possible to avoid
flash of content. Likely needs a `MutationObserver`.
- [x] Hide tippy when button is removed from DOM.
- [x] ~~Implement right-aligned items
(https://github.com/go-gitea/gitea/pull/28976)~~. Not going to do it.
- [x] Clean up CSS so base element has no background and add background
via tailwind instead.
- [x] Use it for org and user page.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/timeutil/datetime.go                  |   2 +-
 modules/timeutil/datetime_test.go             |  10 +-
 options/locale/locale_en-US.ini               |   1 +
 package-lock.json                             |   6 -
 package.json                                  |   1 -
 templates/base/head_script.tmpl               |   1 +
 templates/devtest/gitea-ui.tmpl               |  43 ++++-
 templates/explore/navbar.tmpl                 |   6 +-
 templates/org/menu.tmpl                       |  71 ++++---
 templates/package/content/alpine.tmpl         |   4 +-
 templates/package/content/cargo.tmpl          |   4 +-
 templates/package/content/chef.tmpl           |   2 +-
 templates/package/content/composer.tmpl       |   2 +-
 templates/package/content/conan.tmpl          |   2 +-
 templates/package/content/conda.tmpl          |   6 +-
 templates/package/content/cran.tmpl           |   2 +-
 templates/package/content/debian.tmpl         |   4 +-
 templates/package/content/generic.tmpl        |   2 +-
 templates/package/content/go.tmpl             |   2 +-
 templates/package/content/helm.tmpl           |   2 +-
 templates/package/content/maven.tmpl          |   8 +-
 templates/package/content/npm.tmpl            |   2 +-
 templates/package/content/nuget.tmpl          |   2 +-
 templates/package/content/pub.tmpl            |   2 +-
 templates/package/content/pypi.tmpl           |   2 +-
 templates/package/content/rpm.tmpl            |   4 +-
 templates/package/content/rubygems.tmpl       |   4 +-
 templates/package/content/swift.tmpl          |   2 +-
 templates/package/content/vagrant.tmpl        |   2 +-
 templates/repo/diff/image_diff.tmpl           |   6 +-
 templates/repo/header.tmpl                    |   8 +-
 .../view_content/pull_merge_instruction.tmpl  |   2 +-
 templates/repo/view_file.tmpl                 |   8 +-
 templates/user/auth/link_account.tmpl         |   6 +-
 templates/user/auth/signin_navbar.tmpl        |   6 +-
 templates/user/auth/signup_openid_navbar.tmpl |   6 +-
 templates/user/overview/header.tmpl           |  75 ++++----
 web_src/css/base.css                          |  73 +------
 web_src/css/modules/tippy.css                 |  29 ++-
 web_src/css/repo.css                          |  10 -
 web_src/css/repo/header.css                   |  11 --
 web_src/css/repo/linebutton.css               |   5 -
 web_src/css/shared/repoorg.css                |   7 -
 web_src/js/components/DashboardRepoList.vue   |  62 +++---
 web_src/js/modules/tippy.js                   |  14 +-
 web_src/js/webcomponents/README.md            |   7 +-
 ...{GiteaAbsoluteDate.js => absolute-date.js} |   2 +-
 web_src/js/webcomponents/index.js             |   5 +
 .../{GiteaOriginUrl.js => origin-url.js}      |   2 +-
 ...eaOriginUrl.test.js => origin-url.test.js} |   2 +-
 web_src/js/webcomponents/overflow-menu.js     | 179 ++++++++++++++++++
 .../{polyfill.js => polyfills.js}             |   0
 web_src/js/webcomponents/webcomponents.js     |   6 -
 webpack.config.js                             |  19 +-
 54 files changed, 461 insertions(+), 290 deletions(-)
 rename web_src/js/webcomponents/{GiteaAbsoluteDate.js => absolute-date.js} (95%)
 create mode 100644 web_src/js/webcomponents/index.js
 rename web_src/js/webcomponents/{GiteaOriginUrl.js => origin-url.js} (91%)
 rename web_src/js/webcomponents/{GiteaOriginUrl.test.js => origin-url.test.js} (94%)
 create mode 100644 web_src/js/webcomponents/overflow-menu.js
 rename web_src/js/webcomponents/{polyfill.js => polyfills.js} (100%)
 delete mode 100644 web_src/js/webcomponents/webcomponents.js

diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go
index 50c8d44f13..3ae44cb714 100644
--- a/modules/timeutil/datetime.go
+++ b/modules/timeutil/datetime.go
@@ -56,7 +56,7 @@ func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
 	switch format {
 	case "short", "long": // date only
 		attrs = append(attrs, `month="`+format+`"`, `day="numeric"`)
-		return template.HTML(fmt.Sprintf(`<gitea-absolute-date %s date="%s">%s</gitea-absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
+		return template.HTML(fmt.Sprintf(`<absolute-date %s date="%s">%s</absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
 	case "full": // full date including time
 		attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
 		return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
diff --git a/modules/timeutil/datetime_test.go b/modules/timeutil/datetime_test.go
index 39aecbc43b..ac2ce35ba2 100644
--- a/modules/timeutil/datetime_test.go
+++ b/modules/timeutil/datetime_test.go
@@ -28,19 +28,19 @@ func TestDateTime(t *testing.T) {
 	assert.EqualValues(t, "-", DateTime("short", TimeStamp(0)))
 
 	actual := DateTime("short", "invalid")
-	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="invalid">invalid</gitea-absolute-date>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="invalid">invalid</absolute-date>`, actual)
 
 	actual = DateTime("short", refTimeStr)
-	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</gitea-absolute-date>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</absolute-date>`, actual)
 
 	actual = DateTime("short", refTime)
-	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</gitea-absolute-date>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</absolute-date>`, actual)
 
 	actual = DateTime("short", refDateStr)
-	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01">2018-01-01</gitea-absolute-date>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01">2018-01-01</absolute-date>`, actual)
 
 	actual = DateTime("short", refTimeStamp)
-	assert.EqualValues(t, `<gitea-absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</gitea-absolute-date>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</absolute-date>`, actual)
 
 	actual = DateTime("full", refTimeStamp)
 	assert.EqualValues(t, `<relative-time weekday="" year="numeric" format="datetime" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" data-tooltip-content data-tooltip-interactive="true" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 8c014955d0..a54367e221 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -25,6 +25,7 @@ enable_javascript = This website requires JavaScript.
 toc = Table of Contents
 licenses = Licenses
 return_to_gitea = Return to Gitea
+more_items = More items
 
 username = Username
 email = Email Address
diff --git a/package-lock.json b/package-lock.json
index 8f5e6d0ca9..9ed28e671a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,6 @@
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
         "@primer/octicons": "19.8.0",
-        "@webcomponents/custom-elements": "1.6.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
         "asciinema-player": "3.7.0",
@@ -2864,11 +2863,6 @@
         "@xtuc/long": "4.2.2"
       }
     },
-    "node_modules/@webcomponents/custom-elements": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz",
-      "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww=="
-    },
     "node_modules/@webpack-cli/configtest": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
diff --git a/package.json b/package.json
index d4aec0e88e..efa71a7747 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,6 @@
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
     "@primer/octicons": "19.8.0",
-    "@webcomponents/custom-elements": "1.6.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
     "asciinema-player": "3.7.0",
diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
index 4a723f63b9..22e08e9c8f 100644
--- a/templates/base/head_script.tmpl
+++ b/templates/base/head_script.tmpl
@@ -41,6 +41,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
 			remove_label_str: {{ctx.Locale.Tr "remove_label_str"}},
 			modal_confirm: {{ctx.Locale.Tr "modal.confirm"}},
 			modal_cancel: {{ctx.Locale.Tr "modal.cancel"}},
+			more_items: {{ctx.Locale.Tr "more_items"}},
 		},
 	};
 	{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index e551572b96..0b1f982ee4 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -105,18 +105,45 @@
 	</div>
 
 	<div>
-		<h1>GiteaOriginUrl</h1>
-		<div><gitea-origin-url data-url="test/url"></gitea-origin-url></div>
-		<div><gitea-origin-url data-url="/test/url"></gitea-origin-url></div>
+		<h1>&lt;origin-url&gt;</h1>
+		<div><origin-url data-url="test/url"></origin-url></div>
+		<div><origin-url data-url="/test/url"></origin-url></div>
+	</div>
+
+	<div>
+		<h1>&lt;overflow-menu&gt;</h1>
+		<overflow-menu class="ui secondary pointing tabular borderless menu">
+			<div class="overflow-menu-items">
+				<a class="active item">item</a>
+				<a class="item">item 1</a>
+				<a class="item">item 2</a>
+				<a class="item">item 3</a>
+				<a class="item">item 4</a>
+				<a class="item">item 5</a>
+				<a class="item">item 6</a>
+				<a class="item">item 7</a>
+				<a class="item">item 8</a>
+				<a class="item">item 9</a>
+				<a class="item">item 10</a>
+				<a class="item">item 11</a>
+				<a class="item">item 12</a>
+				<a class="item">item 13</a>
+				<a class="item">item 14</a>
+				<a class="item">item 15</a>
+				<a class="item">item 16</a>
+				<a class="item">item 17</a>
+				<a class="item">item 18</a>
+			</div>
+		</overflow-menu>
 	</div>
 
 	<div>
 		<h1>GiteaAbsoluteDate</h1>
-		<div><gitea-absolute-date date="2024-03-11" year="numeric" day="numeric" month="short"></gitea-absolute-date></div>
-		<div><gitea-absolute-date date="2024-03-11" year="numeric" day="numeric" month="long"></gitea-absolute-date></div>
-		<div><gitea-absolute-date date="2024-03-11" year="" day="numeric" month="numeric"></gitea-absolute-date></div>
-		<div><gitea-absolute-date date="2024-03-11" year="" day="numeric" month="numeric" weekday="long"></gitea-absolute-date></div>
-		<div><gitea-absolute-date date="2024-03-11T19:00:00-05:00" year="" day="numeric" month="numeric" weekday="long"></gitea-absolute-date></div>
+		<div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="short"></absolute-date></div>
+		<div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="long"></absolute-date></div>
+		<div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric"></absolute-date></div>
+		<div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div>
+		<div><absolute-date date="2024-03-11T19:00:00-05:00" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div>
 		<div class="tw-text-text-light-2">relative-time: <relative-time format="datetime" datetime="2024-03-11" year="" day="numeric" month="numeric"></relative-time></div>
 	</div>
 
diff --git a/templates/explore/navbar.tmpl b/templates/explore/navbar.tmpl
index 7f2aea497a..8841613b9f 100644
--- a/templates/explore/navbar.tmpl
+++ b/templates/explore/navbar.tmpl
@@ -1,5 +1,5 @@
-<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-	<div class="new-menu-inner">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar">
+	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsExploreRepositories}}active {{end}}item" href="{{AppSubUrl}}/explore/repos">
 			{{svg "octicon-repo"}} {{ctx.Locale.Tr "explore.repos"}}
 		</a>
@@ -17,4 +17,4 @@
 		</a>
 		{{end}}
 	</div>
-</div>
+</overflow-menu>
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index f07b26865a..8eacc17e82 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -1,50 +1,49 @@
 <div class="ui container">
-	<div class="ui secondary stackable pointing menu">
-		<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
-			{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
-			{{if .RepoCount}}
-				<div class="ui small label">{{.RepoCount}}</div>
+	<overflow-menu class="ui secondary pointing tabular borderless menu">
+		<div class="overflow-menu-items">
+			<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
+				{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
+				{{if .RepoCount}}
+					<div class="ui small label">{{.RepoCount}}</div>
+				{{end}}
+			</a>
+			{{if .CanReadProjects}}
+			<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects">
+				{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
+				{{if .ProjectCount}}
+					<div class="ui small label">{{.ProjectCount}}</div>
+				{{end}}
+			</a>
 			{{end}}
-		</a>
-		{{if .CanReadProjects}}
-		<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects">
-			{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
-			{{if .ProjectCount}}
-				<div class="ui small label">{{.ProjectCount}}</div>
+			{{if and .IsPackageEnabled .CanReadPackages}}
+			<a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages">
+				{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
+			</a>
 			{{end}}
-		</a>
-		{{end}}
-		{{if and .IsPackageEnabled .CanReadPackages}}
-		<a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages">
-			{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
-		</a>
-		{{end}}
-		{{if and .IsRepoIndexerEnabled .CanReadCode}}
-		<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
-			{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
-		</a>
-		{{end}}
-		{{if .NumMembers}}
+			{{if and .IsRepoIndexerEnabled .CanReadCode}}
+			<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
+				{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
+			</a>
+			{{end}}
+			{{if .NumMembers}}
 			<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
 				{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
 				<div class="ui small label">{{.NumMembers}}</div>
 			</a>
-		{{end}}
-		{{if .IsOrganizationMember}}
+			{{end}}
+			{{if .IsOrganizationMember}}
 			<a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams">
 				{{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}}
 				{{if .NumTeams}}
 					<div class="ui small label">{{.NumTeams}}</div>
 				{{end}}
 			</a>
-		{{end}}
-
-		{{if .IsOrganizationOwner}}
-			<div class="right menu">
-				<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
-				{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
-				</a>
-			</div>
-		{{end}}
-	</div>
+			{{end}}
+			{{if .IsOrganizationOwner}}
+			<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
+			{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
+			</a>
+			{{end}}
+		</div>
+	</overflow-menu>
 </div>
diff --git a/templates/package/content/alpine.tmpl b/templates/package/content/alpine.tmpl
index 7bc22ae382..5c144b9779 100644
--- a/templates/package/content/alpine.tmpl
+++ b/templates/package/content/alpine.tmpl
@@ -4,12 +4,12 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></gitea-origin-url>/$branch/$repository</code></pre></div>
+				<div class="markup"><pre class="code-block"><code><origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></origin-url>/$branch/$repository</code></pre></div>
 				<p>{{ctx.Locale.Tr "packages.alpine.registry.info"}}</p>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key"}}</label>
-				<div class="markup"><pre class="code-block"><code>curl -JO <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>curl -JO <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.install"}}</label>
diff --git a/templates/package/content/cargo.tmpl b/templates/package/content/cargo.tmpl
index eff6d6c3b3..7fd88a284a 100644
--- a/templates/package/content/cargo.tmpl
+++ b/templates/package/content/cargo.tmpl
@@ -8,8 +8,8 @@
 default = "gitea"
 
 [registries.gitea]
-index = "sparse+<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></gitea-origin-url>" # Sparse index
-# index = "<gitea-origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></gitea-origin-url>" # Git
+index = "sparse+<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></origin-url>" # Sparse index
+# index = "<origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></origin-url>" # Git
 
 [net]
 git-fetch-with-cli = true</code></pre></div>
diff --git a/templates/package/content/chef.tmpl b/templates/package/content/chef.tmpl
index c8172b8126..03ce9f852b 100644
--- a/templates/package/content/chef.tmpl
+++ b/templates/package/content/chef.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></gitea-origin-url>'</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></origin-url>'</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.chef.install"}}</label>
diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl
index 70bfbc4488..c2dc6345c3 100644
--- a/templates/package/content/composer.tmpl
+++ b/templates/package/content/composer.tmpl
@@ -7,7 +7,7 @@
 				<div class="markup"><pre class="code-block"><code>{
 	"repositories": [{
 			"type": "composer",
-			"url": "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></gitea-origin-url>"
+			"url": "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></origin-url>"
 		}
 	]
 }</code></pre></div>
diff --git a/templates/package/content/conan.tmpl b/templates/package/content/conan.tmpl
index c5019c6fd6..b68a45fde3 100644
--- a/templates/package/content/conan.tmpl
+++ b/templates/package/content/conan.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>conan remote add gitea <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>conan remote add gitea <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.install"}}</label>
diff --git a/templates/package/content/conda.tmpl b/templates/package/content/conda.tmpl
index 0172966145..031b51aa10 100644
--- a/templates/package/content/conda.tmpl
+++ b/templates/package/content/conda.tmpl
@@ -4,11 +4,11 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>channel_alias: <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url>
+				<div class="markup"><pre class="code-block"><code>channel_alias: <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
 channels:
-&#32;&#32;- <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url>
+&#32;&#32;- <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
 default_channels:
-&#32;&#32;- <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url></code></pre></div>
+&#32;&#32;- <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conda.install"}}</label>
diff --git a/templates/package/content/cran.tmpl b/templates/package/content/cran.tmpl
index 3b5c741701..ae58e6f334 100644
--- a/templates/package/content/cran.tmpl
+++ b/templates/package/content/cran.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></gitea-origin-url>")))</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></origin-url>")))</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.cran.install"}}</label>
diff --git a/templates/package/content/debian.tmpl b/templates/package/content/debian.tmpl
index 08b50b46ff..73b8257835 100644
--- a/templates/package/content/debian.tmpl
+++ b/templates/package/content/debian.tmpl
@@ -4,8 +4,8 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>sudo curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></gitea-origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
-echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></gitea-origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
+				<div class="markup"><pre class="code-block"><code>sudo curl <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
+echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
 sudo apt update</code></pre></div>
 				<p>{{ctx.Locale.Tr "packages.debian.registry.info"}}</p>
 			</div>
diff --git a/templates/package/content/generic.tmpl b/templates/package/content/generic.tmpl
index b5a6059f75..2fd952105f 100644
--- a/templates/package/content/generic.tmpl
+++ b/templates/package/content/generic.tmpl
@@ -6,7 +6,7 @@
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.generic.download"}}</label>
 				<div class="markup"><pre class="code-block"><code>
 {{- range .PackageDescriptor.Files -}}
-curl -OJ <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></gitea-origin-url>
+curl -OJ <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></origin-url>
 {{end -}}
 				</code></pre></div>
 			</div>
diff --git a/templates/package/content/go.tmpl b/templates/package/content/go.tmpl
index c74c19095a..80d1ab231a 100644
--- a/templates/package/content/go.tmpl
+++ b/templates/package/content/go.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.go.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>GOPROXY=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></gitea-origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>GOPROXY=<origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go"}}</label>
diff --git a/templates/package/content/helm.tmpl b/templates/package/content/helm.tmpl
index 3fc217fbb0..da846e934d 100644
--- a/templates/package/content/helm.tmpl
+++ b/templates/package/content/helm.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.helm.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></gitea-origin-url>
+				<div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></origin-url>
 helm repo update</code></pre></div>
 			</div>
 			<div class="field">
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl
index 9c694094b9..3a7de335de 100644
--- a/templates/package/content/maven.tmpl
+++ b/templates/package/content/maven.tmpl
@@ -7,19 +7,19 @@
 				<div class="markup"><pre class="code-block"><code>&lt;repositories&gt;
 	&lt;repository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-			&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt;
+			&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/repository&gt;
 &lt;/repositories&gt;
 
 &lt;distributionManagement&gt;
 	&lt;repository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-		&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt;
+		&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/repository&gt;
 
 	&lt;snapshotRepository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-		&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt;
+		&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/snapshotRepository&gt;
 &lt;/distributionManagement&gt;</code></pre></div>
 			</div>
@@ -37,7 +37,7 @@
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.maven.download"}}</label>
-				<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/"}}</label>
diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl
index bf15ec34e9..a78a07d874 100644
--- a/templates/package/content/npm.tmpl
+++ b/templates/package/content/npm.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.npm.install"}}</label>
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl
index 2008cf4cc8..0911260fba 100644
--- a/templates/package/content/nuget.tmpl
+++ b/templates/package/content/nuget.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.install"}}</label>
diff --git a/templates/package/content/pub.tmpl b/templates/package/content/pub.tmpl
index e0608e533f..f2c7ac938f 100644
--- a/templates/package/content/pub.tmpl
+++ b/templates/package/content/pub.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pub.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/"}}</label>
diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl
index d0ce2cd65d..817fced97b 100644
--- a/templates/package/content/pypi.tmpl
+++ b/templates/package/content/pypi.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>pip install --index-url <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></gitea-origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label>
diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl
index 28d875fca3..3faa8a0dc7 100644
--- a/templates/package/content/rpm.tmpl
+++ b/templates/package/content/rpm.tmpl
@@ -11,13 +11,13 @@
 # {{ctx.Locale.Tr "packages.rpm.distros.redhat"}}
 {{- range $group := .Groups}}
 	{{- if $group}}{{$group = print "/" $group}}{{end}}
-dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url>
+dnf config-manager --add-repo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
 {{- end}}
 
 # {{ctx.Locale.Tr "packages.rpm.distros.suse"}}
 {{- range $group := .Groups}}
 	{{- if $group}}{{$group = print "/" $group}}{{end}}
-zypper addrepo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url>
+zypper addrepo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
 {{- end}}</code></pre></div>
 			</div>
 			<div class="field">
diff --git a/templates/package/content/rubygems.tmpl b/templates/package/content/rubygems.tmpl
index e19aab7080..610dfc7856 100644
--- a/templates/package/content/rubygems.tmpl
+++ b/templates/package/content/rubygems.tmpl
@@ -4,11 +4,11 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install"}}:</label>
-				<div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version &quot;{{.PackageDescriptor.Version.Version}}&quot; --source &quot;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></gitea-origin-url>&quot;</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version &quot;{{.PackageDescriptor.Version.Version}}&quot; --source &quot;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>&quot;</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.rubygems.install2"}}:</label>
-				<div class="markup"><pre class="code-block"><code>source "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></gitea-origin-url>" do
+				<div class="markup"><pre class="code-block"><code>source "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>" do
 	gem "{{.PackageDescriptor.Package.Name}}", "{{.PackageDescriptor.Version.Version}}"
 end</code></pre></div>
 			</div>
diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl
index 819cc7f3d0..aacbc83980 100644
--- a/templates/package/content/swift.tmpl
+++ b/templates/package/content/swift.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.swift.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>swift package-registry set <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>swift package-registry set <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install"}}</label>
diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl
index cd294b4ea5..7666284b87 100644
--- a/templates/package/content/vagrant.tmpl
+++ b/templates/package/content/vagrant.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.vagrant.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></gitea-origin-url>"</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></origin-url>"</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/"}}</label>
diff --git a/templates/repo/diff/image_diff.tmpl b/templates/repo/diff/image_diff.tmpl
index 02cca784f6..9ad7916398 100644
--- a/templates/repo/diff/image_diff.tmpl
+++ b/templates/repo/diff/image_diff.tmpl
@@ -7,15 +7,15 @@
 			data-mime-before="{{.sniffedTypeBase.GetMimeType}}"
 			data-mime-after="{{.sniffedTypeHead.GetMimeType}}"
 		>
-			<div class="ui secondary pointing tabular top attached borderless menu new-menu">
-				<div class="new-menu-inner">
+			<overflow-menu class="ui secondary pointing tabular top attached borderless menu">
+				<div class="overflow-menu-items tw-justify-center">
 					<a class="item active" data-tab="diff-side-by-side-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.side_by_side"}}</a>
 					{{if and .blobBase .blobHead}}
 					<a class="item" data-tab="diff-swipe-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.swipe"}}</a>
 					<a class="item" data-tab="diff-overlay-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.overlay"}}</a>
 					{{end}}
 				</div>
-			</div>
+			</overflow-menu>
 			<div class="image-diff-tabs is-loading">
 				<div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side-{{.file.Index}}">
 					<div class="diff-side-by-side">
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 0f64380337..6e0a9985f7 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -126,9 +126,9 @@
 		{{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}}
 	</div>
 {{end}}
-	<div class="ui container secondary pointing tabular top attached borderless menu new-menu navbar">
+	<overflow-menu class="ui container secondary pointing tabular top attached borderless menu navbar tw-pt-0 tw-my-0">
 		{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}}
-			<div class="new-menu-inner">
+			<div class="overflow-menu-items">
 				{{if .Permission.CanRead $.UnitTypeCode}}
 				<a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.BranchNameSubURL}}{{end}}">
 					{{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}}
@@ -220,12 +220,12 @@
 				{{end}}
 			</div>
 		{{else if .Permission.IsAdmin}}
-			<div class="new-menu-inner">
+			<div class="overflow-menu-items">
 				<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings">
 					{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
 				</a>
 			</div>
 		{{end}}
-	</div>
+	</overflow-menu>
 	<div class="ui tabs divider"></div>
 </div>
diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
index a2269feeaf..12b0c4b4e0 100644
--- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl
+++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
@@ -8,7 +8,7 @@
 	{{end}}
 	<div class="ui secondary segment">
 		{{if eq .PullRequest.Flow 0}}
-		<div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}<gitea-origin-url data-url="{{.PullRequest.HeadRepo.Link}}"></gitea-origin-url>{{else}}origin{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div>
+		<div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}<origin-url data-url="{{.PullRequest.HeadRepo.Link}}"></origin-url>{{else}}origin{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div>
 		<div>git checkout {{$localBranch}}</div>
 		{{else}}
 		<div>git fetch -u origin {{.GetGitRefName}}:{{$localBranch}}</div>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 75f45b293f..cdd415a47a 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -135,12 +135,12 @@
 						{{end}}
 					</tbody>
 				</table>
-				<div class="code-line-menu ui vertical pointing menu tippy-target">
+				<div class="code-line-menu tippy-target">
 					{{if $.Permission.CanRead $.UnitTypeIssues}}
-						<a class="item ref-in-new-issue" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
+						<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
 					{{end}}
-					<a class="item view_git_blame" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
-					<a class="item copy-line-permalink" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
+					<a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
+					<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
 				</div>
 				{{end}}
 			{{end}}
diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl
index 5235cbf69f..81ea92c959 100644
--- a/templates/user/auth/link_account.tmpl
+++ b/templates/user/auth/link_account.tmpl
@@ -1,7 +1,7 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content user link-account">
-	<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-		<div class="new-menu-inner">
+	<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar tw-bg-header-wrapper">
+		<div class="overflow-menu-items tw-justify-center">
 			<!-- TODO handle .ShowRegistrationButton once other login bugs are fixed -->
 			{{if not .AllowOnlyInternalRegistration}}
 				<a class="item {{if not .user_exists}}active{{end}}"
@@ -14,7 +14,7 @@
 				{{ctx.Locale.Tr "auth.oauth_signin_tab"}}
 			</a>
 		</div>
-	</div>
+	</overflow-menu>
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
 			<div class="ui tab {{if not .user_exists}}active{{end}}"
diff --git a/templates/user/auth/signin_navbar.tmpl b/templates/user/auth/signin_navbar.tmpl
index bc7fd03e13..a576883065 100644
--- a/templates/user/auth/signin_navbar.tmpl
+++ b/templates/user/auth/signin_navbar.tmpl
@@ -1,6 +1,6 @@
 {{if or .EnableOpenIDSignIn .EnableSSPI}}
-<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-	<div class="new-menu-inner">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar tw-bg-header-wrapper">
+	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsLogin}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login">
 			{{ctx.Locale.Tr "auth.login_userpass"}}
 		</a>
@@ -20,5 +20,5 @@
 		</a>
 		{{end}}
 	</div>
-</div>
+</overflow-menu>
 {{end}}
diff --git a/templates/user/auth/signup_openid_navbar.tmpl b/templates/user/auth/signup_openid_navbar.tmpl
index 075f2e4d7b..9cf81b048f 100644
--- a/templates/user/auth/signup_openid_navbar.tmpl
+++ b/templates/user/auth/signup_openid_navbar.tmpl
@@ -1,5 +1,5 @@
-<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-	<div class="new-menu-inner">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar tw-bg-header-wrapper">
+	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsOpenIDConnect}}active {{end}}item" href="{{AppSubUrl}}/user/openid/connect">
 			{{ctx.Locale.Tr "auth.openid_connect_title"}}
 		</a>
@@ -9,4 +9,4 @@
 			</a>
 		{{end}}
 	</div>
-</div>
+</overflow-menu>
diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl
index cf5e21fa62..275c4e295e 100644
--- a/templates/user/overview/header.tmpl
+++ b/templates/user/overview/header.tmpl
@@ -1,49 +1,50 @@
-<div class="ui secondary stackable pointing menu">
-	{{if and .HasProfileReadme .ContextUser.IsIndividual}}
-	<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
-		{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
-	</a>
-	{{end}}
-	<a class="{{if eq .TabName "repositories"}}active {{end}} item" href="{{.ContextUser.HomeLink}}?tab=repositories">
-		{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
-		{{if .RepoCount}}
-			<div class="ui small label">{{.RepoCount}}</div>
+<overflow-menu class="ui secondary pointing tabular borderless menu">
+	<div class="overflow-menu-items">
+		{{if and .HasProfileReadme .ContextUser.IsIndividual}}
+		<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
+			{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
+		</a>
 		{{end}}
-	</a>
-	{{if or .ContextUser.IsIndividual .CanReadProjects}}
-	<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item">
-		{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
-		{{if .ProjectCount}}
-			<div class="ui small label">{{.ProjectCount}}</div>
+		<a class="{{if eq .TabName "repositories"}}active {{end}} item" href="{{.ContextUser.HomeLink}}?tab=repositories">
+			{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
+			{{if .RepoCount}}
+				<div class="ui small label">{{.RepoCount}}</div>
+			{{end}}
+		</a>
+		{{if or .ContextUser.IsIndividual .CanReadProjects}}
+		<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item">
+			{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
+			{{if .ProjectCount}}
+				<div class="ui small label">{{.ProjectCount}}</div>
+			{{end}}
+		</a>
 		{{end}}
-	</a>
-	{{end}}
-	{{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}}
-		<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
-			{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
-		</a>
-	{{end}}
-	{{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual .CanReadCode)}}
-		<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
-			{{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}}
-		</a>
-	{{end}}
-
-	{{if .ContextUser.IsIndividual}}
-		<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
-			{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
-		</a>
-		{{if not .DisableStars}}
+		{{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}}
+			<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
+				{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
+			</a>
+		{{end}}
+		{{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual .CanReadCode)}}
+			<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
+				{{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}}
+			</a>
+		{{end}}
+		{{if .ContextUser.IsIndividual}}
+			<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
+				{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
+			</a>
+			{{if not .DisableStars}}
 			<a class="{{if eq .TabName "stars"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=stars">
 				{{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}}
 				{{if .ContextUser.NumStars}}
 					<div class="ui small label">{{.ContextUser.NumStars}}</div>
 				{{end}}
 			</a>
-		{{else}}
+			{{else}}
 			<a class="{{if eq .TabName "watching"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=watching">
 				{{svg "octicon-eye"}} {{ctx.Locale.Tr "user.watched"}}
 			</a>
+			{{end}}
 		{{end}}
-	{{end}}
-</div>
+	</div>
+</overflow-menu>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 1c6b3fa488..510a28ad9f 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -248,7 +248,7 @@ a.label,
 }
 
 .page-content .header-wrapper,
-.page-content .new-menu {
+.page-content overflow-menu {
   margin-top: -15px !important;
   padding-top: 15px !important;
 }
@@ -1353,75 +1353,21 @@ strong.attention-caution, span.attention-caution {
   }
 }
 
-.ui.menu.new-menu {
-  margin-bottom: 15px;
-  background: var(--color-header-wrapper);
+overflow-menu {
+  margin-bottom: 15px !important;
   border-bottom: 1px solid var(--color-secondary) !important;
-  overflow: auto;
-}
-
-.ui.menu.new-menu .new-menu-inner {
   display: flex;
-  margin-left: auto;
-  margin-right: auto;
-  overflow-x: auto;
-  width: 100%;
-  mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 60px), transparent 100%);
-  -webkit-mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 60px), transparent 100%);
 }
 
-.ui.menu.new-menu .item {
+overflow-menu .overflow-menu-items {
+  display: flex;
+  flex: 1;
+}
+
+overflow-menu .overflow-menu-items .item {
   margin-bottom: 0 !important; /* reset fomantic's margin, because the active menu has special bottom border */
 }
 
-@media (max-width: 767.98px) {
-  .ui.menu.new-menu .item {
-    width: auto !important;
-  }
-}
-
-.ui.menu.new-menu .item:first-child {
-  margin-left: auto; /* "justify-content: center" doesn't work with "overflow: auto", so use margin: auto */
-}
-
-.ui.menu.new-menu .item:last-child {
-  padding-right: 30px !important;
-  margin-right: auto;
-}
-
-.ui.menu.new-menu::-webkit-scrollbar {
-  height: 6px;
-  display: none;
-}
-
-.ui.menu.new-menu::-webkit-scrollbar-track {
-  background: none !important;
-}
-
-.ui.menu.new-menu::-webkit-scrollbar-thumb {
-  box-shadow: none !important;
-}
-
-.ui.menu.new-menu:hover::-webkit-scrollbar {
-  display: block;
-}
-
-.repos-search {
-  padding-bottom: 0 !important;
-}
-
-.repos-filter {
-  margin-top: 0 !important;
-  border-bottom-width: 0 !important;
-  margin-bottom: 2px !important;
-  justify-content: space-evenly;
-}
-
-.ui.secondary.pointing.menu.repos-filter .item {
-  padding-left: 4.5px;
-  padding-right: 4.5px;
-}
-
 .activity-bar-graph {
   background-color: var(--color-primary);
   color: var(--color-primary-contrast);
@@ -1927,7 +1873,6 @@ table th[data-sortt-desc] .svg {
   background: var(--color-body);
   border-color: var(--color-secondary);
   color: var(--color-text);
-  margin-top: 1px; /* offset fomantic's margin-bottom: -1px */
 }
 
 .ui.segment .ui.tabular.menu .active.item,
diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css
index d65ecc89fb..76d36b4293 100644
--- a/web_src/css/modules/tippy.css
+++ b/web_src/css/modules/tippy.css
@@ -5,6 +5,11 @@
   display: none !important;
 }
 
+/* show target element once it's been moved by tippy.js */
+.tippy-content .tippy-target {
+  display: unset !important;
+}
+
 [data-tippy-root] {
   max-width: calc(100vw - 32px);
 }
@@ -46,18 +51,40 @@
 .tippy-box[data-theme="menu"] {
   background-color: var(--color-menu);
   color: var(--color-text);
+  box-shadow: 0 6px 18px var(--color-shadow);
 }
 
 .tippy-box[data-theme="menu"] .tippy-content {
-  padding: 0;
+  padding: 4px 0;
 }
 
 .tippy-box[data-theme="menu"] .tippy-svg-arrow-inner {
   fill: var(--color-menu);
 }
 
+.tippy-box[data-theme="menu"] .item {
+  display: flex;
+  align-items: center;
+  padding: 9px 18px;
+  color: inherit;
+  text-decoration: none;
+  gap: 10px;
+}
+
+.tippy-box[data-theme="menu"] .item:hover {
+  background: var(--color-hover);
+}
+
+.tippy-box[data-theme="menu"] .item:focus {
+  background: var(--color-active);
+}
+
 /* box-with-header theme to look like .ui.attached.segment. can contain .ui.attached.header */
 
+.tippy-box[data-theme="box-with-header"] {
+  box-shadow: 0 6px 18px var(--color-shadow);
+}
+
 .tippy-box[data-theme="box-with-header"] .tippy-content {
   background: var(--color-box-body);
   border-radius: var(--border-radius);
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index c9c27acf34..23b4e94a06 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2787,16 +2787,6 @@ tbody.commit-list {
   border-left: 1px solid var(--color-secondary);
 }
 
-.repository .ui.menu.new-menu {
-  background: none !important;
-}
-
-@media (max-width: 1200px) {
-  .repository .ui.menu.new-menu::after {
-    background: none !important;
-  }
-}
-
 .migrate-entries {
   display: grid !important;
   grid-template-columns: repeat(3, 1fr);
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index 0eb03136ef..4461e3338e 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -74,17 +74,6 @@
   background-color: var(--color-header-wrapper);
 }
 
-.repository .header-wrapper .new-menu {
-  padding-top: 0 !important;
-  margin-top: 0 !important;
-  margin-bottom: 0 !important;
-}
-
-.repository .header-wrapper .new-menu .item {
-  margin-left: 0 !important;
-  margin-right: 0 !important;
-}
-
 @media (max-width: 767.98px) {
   .repo-header .flex-item {
     flex-grow: 1;
diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css
index 1e5e51eac5..79be5a7a9e 100644
--- a/web_src/css/repo/linebutton.css
+++ b/web_src/css/repo/linebutton.css
@@ -2,11 +2,6 @@
   color: var(--color-text-dark) !important;
 }
 
-.code-line-menu {
-  width: auto !important;
-  border: none !important; /* the border is provided by tippy, not using the `.ui.menu` border */
-}
-
 .code-line-button {
   background-color: var(--color-menu);
   color: var(--color-text-light);
diff --git a/web_src/css/shared/repoorg.css b/web_src/css/shared/repoorg.css
index 7f0a805d0f..5573ae47b8 100644
--- a/web_src/css/shared/repoorg.css
+++ b/web_src/css/shared/repoorg.css
@@ -5,13 +5,6 @@
   margin-left: 15px;
 }
 
-.repository .ui.secondary.stackable.pointing.menu,
-.organization .ui.secondary.stackable.pointing.menu {
-  flex-wrap: wrap;
-  margin-top: 5px;
-  margin-bottom: 10px;
-}
-
 .repository .ui.tabs.container,
 .organization .ui.tabs.container {
   margin-top: 14px;
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 2e8f335ce5..b9ee531d2a 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -384,28 +384,30 @@ export default sfc; // activate the IDE's Vue plugin
             </div>
           </div>
         </div>
-        <div class="ui secondary tiny pointing borderless menu center grid repos-filter">
-          <a class="item" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
-            {{ textAll }}
-            <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
-            {{ textSources }}
-            <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
-            {{ textForks }}
-            <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
-            {{ textMirrors }}
-            <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
-            {{ textCollaborative }}
-            <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-        </div>
+        <overflow-menu class="ui secondary pointing tabular borderless menu repos-filter">
+          <div class="overflow-menu-items tw-justify-center">
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
+              {{ textAll }}
+              <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
+              {{ textSources }}
+              <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
+              {{ textForks }}
+              <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
+              {{ textMirrors }}
+              <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
+              {{ textCollaborative }}
+              <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+          </div>
+        </overflow-menu>
       </div>
       <div v-if="repos.length" class="ui attached table segment gt-rounded-bottom">
         <ul class="repo-owner-name-list">
@@ -501,6 +503,22 @@ ul li:not(:last-child) {
   border-bottom: 1px solid var(--color-secondary);
 }
 
+.repos-search {
+  padding-bottom: 0 !important;
+}
+
+.repos-filter {
+  padding-top: 0 !important;
+  margin-top: 0 !important;
+  border-bottom-width: 0 !important;
+  margin-bottom: 2px !important;
+}
+
+.repos-filter .item {
+  padding-left: 6px !important;
+  padding-right: 6px !important;
+}
+
 .repo-list-link {
   min-width: 0; /* for text truncation */
   display: flex;
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index 489afc0ae1..e7eb39f457 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -7,7 +7,8 @@ const visibleInstances = new Set();
 export function createTippy(target, opts = {}) {
   // the callback functions should be destructured from opts,
   // because we should use our own wrapper functions to handle them, do not let the user override them
-  const {onHide, onShow, onDestroy, ...other} = opts;
+  const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
+
   const instance = tippy(target, {
     appendTo: document.body,
     animation: false,
@@ -35,17 +36,14 @@ export function createTippy(target, opts = {}) {
       return onShow?.(instance);
     },
     arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`,
-    role: 'menu', // HTML role attribute, only tooltips should use "tooltip"
-    theme: other.role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
+    role: role || 'menu', // HTML role attribute
+    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
     plugins: [followCursor],
     ...other,
   });
 
-  // for popups where content refers to a DOM element, we use the 'tippy-target' class
-  // to initially hide the content, now we can remove it as the content has been removed
-  // from the DOM by tippy
-  if (other.content instanceof Element) {
-    other.content.classList.remove('tippy-target');
+  if (role === 'menu') {
+    target.setAttribute('aria-haspopup', 'true');
   }
 
   return instance;
diff --git a/web_src/js/webcomponents/README.md b/web_src/js/webcomponents/README.md
index 0fde507310..45af58e1d2 100644
--- a/web_src/js/webcomponents/README.md
+++ b/web_src/js/webcomponents/README.md
@@ -6,7 +6,6 @@ https://developer.mozilla.org/en-US/docs/Web/Web_Components
 
 # Guidelines
 
-* These components are loaded in `<head>` (before DOM body),
-  so they should have their own dependencies and should be very light,
-  then they won't affect the page loading time too much.
-* If the component is not a public one, it's suggested to have its own `Gitea` or `gitea-` prefix to avoid conflicts.
+* These components are loaded in `<head>` (before DOM body) in a separate entry point, they need to be lightweight to not affect the page loading time too much.
+* Do not import `svg.js` into a web component because that file is currently not tree-shakeable, import svg files individually insteat.
+* All our components must be added to `webpack.config.js` so they work correctly in Vue.
diff --git a/web_src/js/webcomponents/GiteaAbsoluteDate.js b/web_src/js/webcomponents/absolute-date.js
similarity index 95%
rename from web_src/js/webcomponents/GiteaAbsoluteDate.js
rename to web_src/js/webcomponents/absolute-date.js
index 660aa99d07..d12ea0a437 100644
--- a/web_src/js/webcomponents/GiteaAbsoluteDate.js
+++ b/web_src/js/webcomponents/absolute-date.js
@@ -1,4 +1,4 @@
-window.customElements.define('gitea-absolute-date', class extends HTMLElement {
+window.customElements.define('absolute-date', class extends HTMLElement {
   static observedAttributes = ['date', 'year', 'month', 'weekday', 'day'];
 
   update = () => {
diff --git a/web_src/js/webcomponents/index.js b/web_src/js/webcomponents/index.js
new file mode 100644
index 0000000000..7cec9da734
--- /dev/null
+++ b/web_src/js/webcomponents/index.js
@@ -0,0 +1,5 @@
+import './polyfills.js';
+import '@github/relative-time-element';
+import './origin-url.js';
+import './overflow-menu.js';
+import './absolute-date.js';
diff --git a/web_src/js/webcomponents/GiteaOriginUrl.js b/web_src/js/webcomponents/origin-url.js
similarity index 91%
rename from web_src/js/webcomponents/GiteaOriginUrl.js
rename to web_src/js/webcomponents/origin-url.js
index 6e6f84d739..09aa77f2c0 100644
--- a/web_src/js/webcomponents/GiteaOriginUrl.js
+++ b/web_src/js/webcomponents/origin-url.js
@@ -15,7 +15,7 @@ export function toOriginUrl(urlStr) {
   return urlStr;
 }
 
-window.customElements.define('gitea-origin-url', class extends HTMLElement {
+window.customElements.define('origin-url', class extends HTMLElement {
   connectedCallback() {
     this.textContent = toOriginUrl(this.getAttribute('data-url'));
   }
diff --git a/web_src/js/webcomponents/GiteaOriginUrl.test.js b/web_src/js/webcomponents/origin-url.test.js
similarity index 94%
rename from web_src/js/webcomponents/GiteaOriginUrl.test.js
rename to web_src/js/webcomponents/origin-url.test.js
index f0629842b8..3b2ab89f2a 100644
--- a/web_src/js/webcomponents/GiteaOriginUrl.test.js
+++ b/web_src/js/webcomponents/origin-url.test.js
@@ -1,4 +1,4 @@
-import {toOriginUrl} from './GiteaOriginUrl.js';
+import {toOriginUrl} from './origin-url.js';
 
 test('toOriginUrl', () => {
   const oldLocation = window.location;
diff --git a/web_src/js/webcomponents/overflow-menu.js b/web_src/js/webcomponents/overflow-menu.js
new file mode 100644
index 0000000000..9fa4585567
--- /dev/null
+++ b/web_src/js/webcomponents/overflow-menu.js
@@ -0,0 +1,179 @@
+import {throttle} from 'throttle-debounce';
+import {createTippy} from '../modules/tippy.js';
+import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
+
+window.customElements.define('overflow-menu', class extends HTMLElement {
+  updateItems = throttle(100, () => {
+    if (!this.tippyContent) {
+      const div = document.createElement('div');
+      div.classList.add('tippy-target');
+      div.tabIndex = '-1'; // for initial focus, programmatic focus only
+      div.addEventListener('keydown', (e) => {
+        if (e.key === 'Tab') {
+          const items = this.tippyContent.querySelectorAll('[role="menuitem"]');
+          if (e.shiftKey) {
+            if (document.activeElement === items[0]) {
+              e.preventDefault();
+              items[items.length - 1].focus();
+            }
+          } else {
+            if (document.activeElement === items[items.length - 1]) {
+              e.preventDefault();
+              items[0].focus();
+            }
+          }
+        } else if (e.key === 'Escape') {
+          e.preventDefault();
+          e.stopPropagation();
+          this.button._tippy.hide();
+          this.button.focus();
+        } else if (e.key === ' ' || e.code === 'Enter') {
+          if (document.activeElement?.matches('[role="menuitem"]')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.click();
+          }
+        } else if (e.key === 'ArrowDown') {
+          if (document.activeElement?.matches('.tippy-target')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.querySelector('[role="menuitem"]:first-of-type').focus();
+          } else if (document.activeElement?.matches('[role="menuitem"]')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.nextElementSibling?.focus();
+          }
+        } else if (e.key === 'ArrowUp') {
+          if (document.activeElement?.matches('.tippy-target')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.querySelector('[role="menuitem"]:last-of-type').focus();
+          } else if (document.activeElement?.matches('[role="menuitem"]')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.previousElementSibling?.focus();
+          }
+        }
+      });
+      this.append(div);
+      this.tippyContent = div;
+    }
+
+    // move items in tippy back into the menu items for subsequent measurement
+    for (const item of this.tippyItems || []) {
+      this.menuItemsEl.append(item);
+    }
+
+    // measure which items are partially outside the element and move them into the button menu
+    this.tippyItems = [];
+    const menuRight = this.offsetLeft + this.offsetWidth;
+    const menuItems = this.menuItemsEl.querySelectorAll('.item');
+    for (const item of menuItems) {
+      const itemRight = item.offsetLeft + item.offsetWidth;
+      if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button
+        this.tippyItems.push(item);
+      }
+    }
+
+    // if there are no overflown items, remove any previously created button
+    if (!this.tippyItems?.length) {
+      const btn = this.querySelector('.overflow-menu-button');
+      btn?._tippy?.destroy();
+      btn?.remove();
+      return;
+    }
+
+    // remove aria role from items that moved from tippy to menu
+    for (const item of menuItems) {
+      if (!this.tippyItems.includes(item)) {
+        item.removeAttribute('role');
+      }
+    }
+
+    // move all items that overflow into tippy
+    for (const item of this.tippyItems) {
+      item.setAttribute('role', 'menuitem');
+      this.tippyContent.append(item);
+    }
+
+    // update existing tippy
+    if (this.button?._tippy) {
+      this.button._tippy.setContent(this.tippyContent);
+      return;
+    }
+
+    // create button initially
+    const btn = document.createElement('button');
+    btn.classList.add('overflow-menu-button', 'btn', 'tw-px-2', 'hover:tw-text-text-dark');
+    btn.setAttribute('aria-label', window.config.i18n.more_items);
+    btn.innerHTML = octiconKebabHorizontal;
+    this.append(btn);
+    this.button = btn;
+
+    createTippy(btn, {
+      trigger: 'click',
+      hideOnClick: true,
+      interactive: true,
+      placement: 'bottom-end',
+      role: 'menu',
+      content: this.tippyContent,
+      onShow: () => { // FIXME: onShown doesn't work (never be called)
+        setTimeout(() => {
+          this.tippyContent.focus();
+        }, 0);
+      },
+    });
+  });
+
+  init() {
+    // ResizeObserver triggers on initial render, so we don't manually call `updateItems` here which
+    // also avoids a full-page FOUC in Firefox that happens when `updateItems` is called too soon.
+    this.resizeObserver = new ResizeObserver((entries) => {
+      for (const entry of entries) {
+        const newWidth = entry.contentBoxSize[0].inlineSize;
+        if (newWidth !== this.lastWidth) {
+          requestAnimationFrame(() => {
+            this.updateItems();
+          });
+          this.lastWidth = newWidth;
+        }
+      }
+    });
+    this.resizeObserver.observe(this);
+  }
+
+  connectedCallback() {
+    this.setAttribute('role', 'navigation');
+
+    // check whether the mandatory `.overflow-menu-items` element is present initially which happens
+    // with Vue which renders differently than browsers. If it's not there, like in the case of browser
+    // template rendering, wait for its addition.
+    // The eslint rule is not sophisticated enough or aware of this problem, see
+    // https://github.com/43081j/eslint-plugin-wc/pull/130
+    const menuItemsEl = this.querySelector('.overflow-menu-items'); // eslint-disable-line wc/no-child-traversal-in-connectedcallback
+    if (menuItemsEl) {
+      this.menuItemsEl = menuItemsEl;
+      this.init();
+    } else {
+      this.mutationObserver = new MutationObserver((mutations) => {
+        for (const mutation of mutations) {
+          for (const node of mutation.addedNodes) {
+            if (!isDocumentFragmentOrElementNode(node)) continue;
+            if (node.classList.contains('overflow-menu-items')) {
+              this.menuItemsEl = node;
+              this.mutationObserver?.disconnect();
+              this.init();
+            }
+          }
+        }
+      });
+      this.mutationObserver.observe(this, {childList: true});
+    }
+  }
+
+  disconnectedCallback() {
+    this.mutationObserver?.disconnect();
+    this.resizeObserver?.disconnect();
+  }
+});
diff --git a/web_src/js/webcomponents/polyfill.js b/web_src/js/webcomponents/polyfills.js
similarity index 100%
rename from web_src/js/webcomponents/polyfill.js
rename to web_src/js/webcomponents/polyfills.js
diff --git a/web_src/js/webcomponents/webcomponents.js b/web_src/js/webcomponents/webcomponents.js
deleted file mode 100644
index 03348d895f..0000000000
--- a/web_src/js/webcomponents/webcomponents.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import '@webcomponents/custom-elements'; // polyfill for some browsers like PaleMoon
-import './polyfill.js';
-
-import '@github/relative-time-element';
-import './GiteaOriginUrl.js';
-import './GiteaAbsoluteDate.js';
diff --git a/webpack.config.js b/webpack.config.js
index d073a3e9f1..eb23778bae 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -43,6 +43,18 @@ if ('ENABLE_SOURCEMAP' in env) {
   sourceMaps = isProduction ? 'reduced' : 'true';
 }
 
+// define which web components we use for Vue to not interpret them as Vue components
+const webComponents = new Set([
+  // our own, in web_src/js/webcomponents
+  'overflow-menu',
+  'origin-url',
+  'absolute-date',
+  // from dependencies
+  'markdown-toolbar',
+  'relative-time',
+  'text-expander',
+]);
+
 const filterCssImport = (url, ...args) => {
   const cssFile = args[1] || args[0]; // resourcePath is 2nd argument for url and 3rd for import
   const importedFile = url.replace(/[?#].+/, '').toLowerCase();
@@ -72,7 +84,7 @@ export default {
       fileURLToPath(new URL('web_src/css/index.css', import.meta.url)),
     ],
     webcomponents: [
-      fileURLToPath(new URL('web_src/js/webcomponents/webcomponents.js', import.meta.url)),
+      fileURLToPath(new URL('web_src/js/webcomponents/index.js', import.meta.url)),
     ],
     swagger: [
       fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)),
@@ -121,6 +133,11 @@ export default {
         test: /\.vue$/i,
         exclude: /node_modules/,
         loader: 'vue-loader',
+        options: {
+          compilerOptions: {
+            isCustomElement: (tag) => webComponents.has(tag),
+          },
+        },
       },
       {
         test: /\.js$/i,

From 94512ee0628dc0d2b697441a4355ace54b6515cd Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 15 Mar 2024 03:38:13 +0100
Subject: [PATCH 379/679] Fix Citation modal responsiveness and clipboard copy
 (#29799)

The modal was broken in two ways:

- On small screens, the input box was partially hanging outside the
modal. Fixed with flexbox and increased modal width.
- The clipboard copy was not working because the modal had both
`data-clipboard-text` and `data-clipboard-target`, while we only support
one of those. Made a small tweak in clipboard as well so that it will
still fall back to target if text is empty.
---
 templates/repo/cite/cite_buttons.tmpl |  2 +-
 templates/repo/cite/cite_modal.tmpl   | 14 ++++++--------
 web_src/css/repo.css                  | 10 +++-------
 web_src/js/features/clipboard.js      |  6 ++----
 4 files changed, 12 insertions(+), 20 deletions(-)

diff --git a/templates/repo/cite/cite_buttons.tmpl b/templates/repo/cite/cite_buttons.tmpl
index 9953c92c8a..426ca3858e 100644
--- a/templates/repo/cite/cite_buttons.tmpl
+++ b/templates/repo/cite/cite_buttons.tmpl
@@ -6,6 +6,6 @@ BibTeX
 </button>
 <!-- the value will be updated by initCitationFileCopyContent, the code below is used to avoid UI flicking  -->
 <input id="citation-copy-content" value="" size="1" readonly>
-<button class="ui icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="" data-clipboard-target="#citation-copy-content">
+<button class="ui icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-target="#citation-copy-content">
 	{{svg "octicon-copy"}}
 </button>
diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl
index c34c77e0c4..fb251442ca 100644
--- a/templates/repo/cite/cite_modal.tmpl
+++ b/templates/repo/cite/cite_modal.tmpl
@@ -1,16 +1,14 @@
-<div class="ui tiny modal" id="cite-repo-modal">
+<div class="ui small modal" id="cite-repo-modal">
 	<div class="header">
 		{{ctx.Locale.Tr "repo.cite_this_repo"}}
 	</div>
 	<div class="content">
 		<div class="ui stackable secondary menu">
-			<div class="fitted item">
-				<div class="ui action input" id="citation-panel">
-					{{template "repo/cite/cite_buttons" .}}
-					<a id="goto-citation-btn" class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}">
-						{{svg "octicon-file-moved"}}
-					</a>
-				</div>
+			<div class="ui action input" id="citation-panel">
+				{{template "repo/cite/cite_buttons" .}}
+				<a id="goto-citation-btn" class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}">
+					{{svg "octicon-file-moved"}}
+				</a>
 			</div>
 		</div>
 	</div>
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 23b4e94a06..e71b408804 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2035,13 +2035,8 @@
 }
 
 #cite-repo-modal #citation-panel {
-  width: 500px;
-}
-
-@media (max-width: 767.98px) {
-  #cite-repo-modal #citation-panel {
-    width: 100%;
-  }
+  display: flex;
+  width: 100%;
 }
 
 #cite-repo-modal #citation-panel input {
@@ -2061,6 +2056,7 @@
   padding: 5px 10px;
   font-size: 1.2em;
   line-height: 1.4;
+  flex: 1;
 }
 
 #cite-repo-modal #citation-panel #citation-copy-apa,
diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
index 8be5505c8b..daf7e2ae2d 100644
--- a/web_src/js/features/clipboard.js
+++ b/web_src/js/features/clipboard.js
@@ -15,10 +15,8 @@ export function initGlobalCopyToClipboardListener() {
 
     e.preventDefault();
 
-    let text;
-    if (target.hasAttribute('data-clipboard-text')) {
-      text = target.getAttribute('data-clipboard-text');
-    } else {
+    let text = target.getAttribute('data-clipboard-text');
+    if (!text) {
       text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
     }
 

From 2eb7c564df950fb96a1970559719003e979ff30a Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Fri, 15 Mar 2024 11:43:10 +0800
Subject: [PATCH 380/679] Improve branch select list ui in go templates
 (#29729)

Relate:[#27417](https://github.com/go-gitea/gitea/issues/27471)
Reference:  [#26631](https://github.com/go-gitea/gitea/pull/26631)

Before


![image](https://github.com/go-gitea/gitea/assets/37935145/88ca8da5-25dd-4f60-bea8-a80107f19cc5)

After


![image](https://github.com/go-gitea/gitea/assets/37935145/3cb180dc-1331-43e9-8633-be5e288401e8)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 options/locale/locale_en-US.ini               |  3 +-
 templates/repo/branch_dropdown.tmpl           |  2 +-
 templates/repo/diff/compare.tmpl              |  2 +-
 .../repo/issue/branch_selector_field.tmpl     | 13 +++++---
 templates/repo/issue/view_title.tmpl          |  2 +-
 templates/repo/wiki/view.tmpl                 |  2 +-
 web_src/css/repo.css                          | 30 +++++++++++++++++++
 7 files changed, 45 insertions(+), 9 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a54367e221..dc16d78fc7 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -156,6 +156,8 @@ filter.not_template = Not Template
 filter.public = Public
 filter.private = Private
 
+no_results_found = No results found.
+
 [search]
 search = Search...
 type_tooltip = Search type
@@ -1763,7 +1765,6 @@ pulls.compare_compare = pull from
 pulls.switch_comparison_type = Switch comparison type
 pulls.switch_head_and_base = Switch head and base
 pulls.filter_branch = Filter branch
-pulls.no_results = No results found.
 pulls.show_all_commits = Show all commits
 pulls.show_changes_since_your_last_review = Show changes since your last review
 pulls.showing_only_single_commit = Showing only changes of commit %[1]s
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index 8a5cdc7cc7..367c6aab98 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -56,7 +56,7 @@
 		'repoLink': {{.root.RepoLink}},
 		'treePath': {{.root.TreePath}},
 		'branchNameSubURL': {{.root.BranchNameSubURL}},
-		'noResults': {{ctx.Locale.Tr "repo.pulls.no_results"}},
+		'noResults': {{ctx.Locale.Tr "no_results_found"}},
 	};
 	{{if .release}}
 	data.release = {
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index 970e29b476..773db40e18 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -29,7 +29,7 @@
 	{{- end -}}
 	<div class="ui segment choose branch">
 		<a class="gt-mr-3" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
-		<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+		<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 			<div class="ui basic small button">
 				<span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_base"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_base"}}{{end}}: {{$BaseCompareName}}:{{$.BaseBranch}}</span>
 				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl
index 9b7a05ce35..4160e47465 100644
--- a/templates/repo/issue/branch_selector_field.tmpl
+++ b/templates/repo/issue/branch_selector_field.tmpl
@@ -4,8 +4,8 @@
 <form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form">
 	{{$.CsrfTokenHtml}}
 </form>
-
-<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+{{/* TODO: share this branch selector dropdown with the same in repo page */}}
+<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 	<div class="ui basic small button">
 		<span class="text branch-name">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span>
 		{{if .HasIssuesOrPullsWritePermission}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}{{end}}
@@ -18,12 +18,12 @@
 		<div class="header">
 			<div class="ui grid">
 				<div class="two column row">
-					<a class="reference column" href="#" data-target="#branch-list">
+					<a class="reference column muted" href="#" data-target="#branch-list">
 						<span class="text black">
 							{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}}
 						</span>
 					</a>
-					<a class="reference column" href="#" data-target="#tag-list">
+					<a class="reference column muted" href="#" data-target="#tag-list">
 						<span class="text">
 							{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}}
 						</span>
@@ -31,12 +31,15 @@
 				</div>
 			</div>
 		</div>
+		<div class="branch-tag-divider"></div>
 		<div id="branch-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}}">
 			{{if .Reference}}
 				<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
 			{{end}}
 			{{range .Branches}}
 				<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector">{{.}}</div>
+			{{else}}
+				<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
 			{{end}}
 		</div>
 		<div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} gt-hidden">
@@ -45,6 +48,8 @@
 			{{end}}
 			{{range .Tags}}
 				<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div>
+			{{else}}
+				<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index e98e27924d..370826e0fd 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -77,7 +77,7 @@
 							</div>
 						</div>
 						{{svg "octicon-arrow-right"}}
-						<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+						<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 							<div class="ui basic small button">
 								<span class="text" id="pull-target-branch" data-basename="{{$.BaseName}}" data-branch="{{$.BaseBranch}}">{{ctx.Locale.Tr "repo.pulls.compare_base"}}: {{$.BaseName}}:{{$.BaseBranch}}</span>
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 541b1e9b42..19da3fd199 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -5,7 +5,7 @@
 	<div class="ui container">
 		<div class="repo-button-row">
 			<div class="gt-df gt-ac">
-				<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+				<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 					<div class="ui basic small button">
 						<span class="text">
 							{{ctx.Locale.Tr "repo.wiki.page"}}:
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index e71b408804..b7e8efd47e 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -3009,3 +3009,33 @@ tbody.commit-list {
 #cherry-pick-modal .scrolling.menu {
   max-height: 200px;
 }
+
+/* Branch tag selector - TODO: Merge this into the same selector on repo page */
+.repository .issue-content .issue-content-right  .ui.grid .column.row {
+  padding: 10px;
+  padding-bottom: 0;
+}
+.repository .issue-content .issue-content-right  .ui.grid .column.muted {
+  padding: 0;
+}
+.repository .issue-content .issue-content-right  .ui.grid .column.muted .text {
+  display: inline-block;
+  padding: 10px;
+  width: 100%;
+  text-align: center;
+  border: 1px solid transparent;
+  border-bottom: none;
+}
+.repository .issue-content .issue-content-right .ui.grid .column.muted .text.black {
+  border-color: var(--color-secondary);
+  background: var(--color-menu);
+  border-top-left-radius: var(--border-radius);
+  border-top-right-radius: var(--border-radius);
+}
+.repository .issue-content .issue-content-right .ui.dropdown  .scrolling.menu {
+  border-top: none;
+}
+.repository .issue-content .issue-content-right .branch-tag-divider {
+  margin-top: -1px;
+  border-top: 1px solid var(--color-secondary);
+}

From 0827552d9ab6bec5fccef86139cbad3ae7b582b7 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 15 Mar 2024 05:45:45 +0100
Subject: [PATCH 381/679] Remove scrollbar customizations (#29800)

Fixes https://github.com/go-gitea/gitea/issues/29652. Removes all
scrollbar customization as per popular vote on
https://github.com/go-gitea/gitea/issues/29652#issuecomment-1985846162.

There is one more case of `-webkit-scrollbar` left in CSS and
https://github.com/go-gitea/gitea/pull/29400 will get rid of that as
well.
---
 web_src/css/base.css | 24 ------------------------
 1 file changed, 24 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 510a28ad9f..4083ec0413 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -105,33 +105,9 @@ progress::-moz-progress-bar {
 }
 
 * {
-  scrollbar-color: var(--color-primary) transparent;
   caret-color: var(--color-caret);
 }
 
-::-webkit-scrollbar {
-  width: 10px;
-  height: 10px;
-}
-
-::-webkit-scrollbar-thumb {
-  box-shadow: inset 0 0 0 6px var(--color-primary);
-  border: 2px solid transparent;
-  border-radius: var(--border-radius);
-}
-
-::-webkit-scrollbar-thumb:window-inactive {
-  box-shadow: inset 0 0 0 6px var(--color-primary);
-}
-
-::-webkit-scrollbar-thumb:hover {
-  box-shadow: inset 0 0 0 6px var(--color-primary-dark-2);
-}
-
-::-webkit-scrollbar-corner {
-  background: transparent;
-}
-
 ::file-selector-button {
   border: 1px solid var(--color-light-border);
   color: var(--color-text-light);

From 277f90d4164c09dc42a0fc6b1041147e76874f41 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 15 Mar 2024 13:13:09 +0800
Subject: [PATCH 382/679] Fix codeowner detected diff base branch to mergebase
 (#29783)

Fix #29763

This PR fixes 2 problems with CodeOwner in the pull request.
- Don't use the pull request base branch but merge-base as a diff base to
detect the code owner.
- CodeOwner detection in fork repositories will be disabled because
almost all the fork repositories will not change CODEOWNERS files but it
should not be used on fork repositories' pull requests.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/issues/pull.go                 |  72 ---------------
 services/issue/issue.go               |   2 +-
 services/issue/pull.go                | 122 +++++++++++++++++++++++++
 services/pull/pull.go                 |   2 +-
 tests/integration/pull_review_test.go | 124 ++++++++++++++++++++++++++
 5 files changed, 248 insertions(+), 74 deletions(-)
 create mode 100644 services/issue/pull.go

diff --git a/models/issues/pull.go b/models/issues/pull.go
index 80b149da5c..dc1b1b956a 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -19,7 +19,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -884,77 +883,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
 	return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
 }
 
-func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error {
-	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
-
-	if pr.IsWorkInProgress(ctx) {
-		return nil
-	}
-
-	if err := pr.LoadBaseRepo(ctx); err != nil {
-		return err
-	}
-
-	repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
-	if err != nil {
-		return err
-	}
-	defer repo.Close()
-
-	commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
-	if err != nil {
-		return err
-	}
-
-	var data string
-	for _, file := range files {
-		if blob, err := commit.GetBlobByPath(file); err == nil {
-			data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
-			if err == nil {
-				break
-			}
-		}
-	}
-
-	rules, _ := GetCodeOwnersFromContent(ctx, data)
-	changedFiles, err := repo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
-	if err != nil {
-		return err
-	}
-
-	uniqUsers := make(map[int64]*user_model.User)
-	uniqTeams := make(map[string]*org_model.Team)
-	for _, rule := range rules {
-		for _, f := range changedFiles {
-			if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) {
-				for _, u := range rule.Users {
-					uniqUsers[u.ID] = u
-				}
-				for _, t := range rule.Teams {
-					uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t
-				}
-			}
-		}
-	}
-
-	for _, u := range uniqUsers {
-		if u.ID != pull.Poster.ID {
-			if _, err := AddReviewRequest(ctx, pull, u, pull.Poster); err != nil {
-				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
-				return err
-			}
-		}
-	}
-	for _, t := range uniqTeams {
-		if _, err := AddTeamReviewRequest(ctx, pull, t, pull.Poster); err != nil {
-			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
-			return err
-		}
-	}
-
-	return nil
-}
-
 // GetCodeOwnersFromContent returns the code owners configuration
 // Return empty slice if files missing
 // Return warning messages on parsing errors
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 0753813b64..7662c9d9e8 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -90,7 +90,7 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 	}
 
 	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
-		if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest); err != nil {
+		if err := PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest); err != nil {
 			return err
 		}
 	}
diff --git a/services/issue/pull.go b/services/issue/pull.go
new file mode 100644
index 0000000000..698e2622f5
--- /dev/null
+++ b/services/issue/pull.go
@@ -0,0 +1,122 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	org_model "code.gitea.io/gitea/models/organization"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+func getMergeBase(repo *git.Repository, pr *issues_model.PullRequest, baseBranch, headBranch string) (string, error) {
+	// Add a temporary remote
+	tmpRemote := fmt.Sprintf("mergebase-%d-%d", pr.ID, time.Now().UnixNano())
+	if err := repo.AddRemote(tmpRemote, repo.Path, false); err != nil {
+		return "", fmt.Errorf("AddRemote: %w", err)
+	}
+	defer func() {
+		if err := repo.RemoveRemote(tmpRemote); err != nil {
+			log.Error("getMergeBase: RemoveRemote: %v", err)
+		}
+	}()
+
+	mergeBase, _, err := repo.GetMergeBase(tmpRemote, baseBranch, headBranch)
+	return mergeBase, err
+}
+
+func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue, pr *issues_model.PullRequest) error {
+	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
+
+	if pr.IsWorkInProgress(ctx) {
+		return nil
+	}
+
+	if err := pr.LoadHeadRepo(ctx); err != nil {
+		return err
+	}
+
+	if pr.HeadRepo.IsFork {
+		return nil
+	}
+
+	if err := pr.LoadBaseRepo(ctx); err != nil {
+		return err
+	}
+
+	repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+	if err != nil {
+		return err
+	}
+	defer repo.Close()
+
+	commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
+	if err != nil {
+		return err
+	}
+
+	var data string
+	for _, file := range files {
+		if blob, err := commit.GetBlobByPath(file); err == nil {
+			data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
+			if err == nil {
+				break
+			}
+		}
+	}
+
+	rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
+
+	// get the mergebase
+	mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
+	if err != nil {
+		return err
+	}
+
+	// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
+	// between the merge base and the head commit but not the base branch and the head commit
+	changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.HeadCommitID)
+	if err != nil {
+		return err
+	}
+
+	uniqUsers := make(map[int64]*user_model.User)
+	uniqTeams := make(map[string]*org_model.Team)
+	for _, rule := range rules {
+		for _, f := range changedFiles {
+			if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) {
+				for _, u := range rule.Users {
+					uniqUsers[u.ID] = u
+				}
+				for _, t := range rule.Teams {
+					uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t
+				}
+			}
+		}
+	}
+
+	for _, u := range uniqUsers {
+		if u.ID != pull.Poster.ID {
+			if _, err := issues_model.AddReviewRequest(ctx, pull, u, pull.Poster); err != nil {
+				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
+				return err
+			}
+		}
+	}
+	for _, t := range uniqTeams {
+		if _, err := issues_model.AddTeamReviewRequest(ctx, pull, t, pull.Poster); err != nil {
+			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 9133a72acf..57873b63ba 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -136,7 +136,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
 		}
 
 		if !pr.IsWorkInProgress(ctx) {
-			if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, pr); err != nil {
+			if err := issue_service.PullRequestCodeOwnersReview(ctx, issue, pr); err != nil {
 				return err
 			}
 		}
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 68d80a1021..7166a740ab 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -5,9 +5,20 @@ package integration
 
 import (
 	"net/http"
+	"net/url"
+	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	repo_service "code.gitea.io/gitea/services/repository"
+	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestPullView_ReviewerMissed(t *testing.T) {
@@ -20,3 +31,116 @@ func TestPullView_ReviewerMissed(t *testing.T) {
 	req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
 	session.MakeRequest(t, req, http.StatusOK)
 }
+
+func TestPullView_CodeOwner(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+		// Create the repo.
+		repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+			Name:             "test_codeowner",
+			Readme:           "Default",
+			AutoInit:         true,
+			ObjectFormatName: git.Sha1ObjectFormat.Name(),
+			DefaultBranch:    "master",
+		})
+		assert.NoError(t, err)
+
+		// add CODEOWNERS to default branch
+		_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			OldBranch: repo.DefaultBranch,
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      "CODEOWNERS",
+					ContentReader: strings.NewReader("README.md @user5\n"),
+				},
+			},
+		})
+		assert.NoError(t, err)
+
+		t.Run("First Pull Request", func(t *testing.T) {
+			// create a new branch to prepare for pull request
+			_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+				NewBranch: "codeowner-basebranch",
+				Files: []*files_service.ChangeRepoFile{
+					{
+						Operation:     "update",
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader("# This is a new project\n"),
+					},
+				},
+			})
+			assert.NoError(t, err)
+
+			// Create a pull request.
+			session := loginUser(t, "user2")
+			testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request")
+
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch"})
+			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
+		})
+
+		// change the default branch CODEOWNERS file to change README.md's codeowner
+		_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "update",
+					TreePath:      "CODEOWNERS",
+					ContentReader: strings.NewReader("README.md @user8\n"),
+				},
+			},
+		})
+		assert.NoError(t, err)
+
+		t.Run("Second Pull Request", func(t *testing.T) {
+			// create a new branch to prepare for pull request
+			_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+				NewBranch: "codeowner-basebranch2",
+				Files: []*files_service.ChangeRepoFile{
+					{
+						Operation:     "update",
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader("# This is a new project2\n"),
+					},
+				},
+			})
+			assert.NoError(t, err)
+
+			// Create a pull request.
+			session := loginUser(t, "user2")
+			testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2")
+
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"})
+			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
+		})
+
+		t.Run("Forked Repo Pull Request", func(t *testing.T) {
+			user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+			forkedRepo, err := repo_service.ForkRepository(db.DefaultContext, user2, user5, repo_service.ForkRepoOptions{
+				BaseRepo: repo,
+				Name:     "test_codeowner_fork",
+			})
+			assert.NoError(t, err)
+
+			// create a new branch to prepare for pull request
+			_, err = files_service.ChangeRepoFiles(db.DefaultContext, forkedRepo, user5, &files_service.ChangeRepoFilesOptions{
+				NewBranch: "codeowner-basebranch-forked",
+				Files: []*files_service.ChangeRepoFile{
+					{
+						Operation:     "update",
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader("# This is a new forked project\n"),
+					},
+				},
+			})
+			assert.NoError(t, err)
+
+			session := loginUser(t, "user5")
+			testPullCreate(t, session, "user5", "test_codeowner_fork", false, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2")
+
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch-forked"})
+			unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
+		})
+	})
+}

From 0d3ec8e2adfcf49329b52d74367698b62ffb3f73 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 15 Mar 2024 10:13:01 +0100
Subject: [PATCH 383/679] Use `Temporal.PlainDate` for absolute dates (#29804)

Use the upcoming
[Temporal.PlainDate](https://tc39.es/proposal-temporal/docs/plaindate.html)
via polyfill. If there is any remaining bugs in `<absolute-date>` this
will iron them out. I opted for the lightweight polyfill because both
seem to achieve our goal of localizeable absolute dates.

- With
[`@js-temporal/polyfill`](https://www.npmjs.com/package/@js-temporal/polyfill)
chunk size goes from 81.4 KiB to 274 KiB
- With
[`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill)
chunk size goes from 81.4 KiB to 142 KiB

Also see [this
table](https://github.com/fullcalendar/temporal-polyfill?tab=readme-ov-file#comparison-with-js-temporalpolyfill)
for more comparisons of these polyfills. Soon there will be
[treeshakable
API](https://github.com/fullcalendar/temporal-polyfill?tab=readme-ov-file#tree-shakable-api)
as well which will further reduce size.
---
 package-lock.json                             | 14 +++++++++++++
 package.json                                  |  1 +
 web_src/js/webcomponents/absolute-date.js     | 20 +++++++++----------
 .../js/webcomponents/absolute-date.test.js    | 15 ++++++++++++++
 4 files changed, 40 insertions(+), 10 deletions(-)
 create mode 100644 web_src/js/webcomponents/absolute-date.test.js

diff --git a/package-lock.json b/package-lock.json
index 9ed28e671a..1b8ed2ddc2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -47,6 +47,7 @@
         "sortablejs": "1.15.2",
         "swagger-ui-dist": "5.12.0",
         "tailwindcss": "3.4.1",
+        "temporal-polyfill": "0.2.3",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
         "tippy.js": "6.3.7",
@@ -11524,6 +11525,19 @@
         "node": ">=6"
       }
     },
+    "node_modules/temporal-polyfill": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.2.3.tgz",
+      "integrity": "sha512-7ZJRc7wq/1XjrOQYkkNpgo2qfE9XLrUU8D/DS+LAC/T0bYqZ46rW6dow0sOTXTPZS4bwer8bD/0OyuKQBfA3yw==",
+      "dependencies": {
+        "temporal-spec": "^0.2.0"
+      }
+    },
+    "node_modules/temporal-spec": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.2.0.tgz",
+      "integrity": "sha512-r1AT0XdEp8TMQ13FLvOt8mOtAxDQsRt2QU5rSWCA7YfshddU651Y1NHVrceLANvixKdf9fYO8B/S9fXHodB7HQ=="
+    },
     "node_modules/terser": {
       "version": "5.29.2",
       "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
diff --git a/package.json b/package.json
index efa71a7747..80a577ba20 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "sortablejs": "1.15.2",
     "swagger-ui-dist": "5.12.0",
     "tailwindcss": "3.4.1",
+    "temporal-polyfill": "0.2.3",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
     "tippy.js": "6.3.7",
diff --git a/web_src/js/webcomponents/absolute-date.js b/web_src/js/webcomponents/absolute-date.js
index d12ea0a437..d2be455302 100644
--- a/web_src/js/webcomponents/absolute-date.js
+++ b/web_src/js/webcomponents/absolute-date.js
@@ -1,3 +1,9 @@
+import {Temporal} from 'temporal-polyfill';
+
+export function toAbsoluteLocaleDate(dateStr, lang, opts) {
+  return Temporal.PlainDate.from(dateStr).toLocaleString(lang ?? [], opts);
+}
+
 window.customElements.define('absolute-date', class extends HTMLElement {
   static observedAttributes = ['date', 'year', 'month', 'weekday', 'day'];
 
@@ -7,19 +13,13 @@ window.customElements.define('absolute-date', class extends HTMLElement {
     const weekday = this.getAttribute('weekday') ?? '';
     const day = this.getAttribute('day') ?? '';
     const lang = this.closest('[lang]')?.getAttribute('lang') ||
-      this.ownerDocument.documentElement.getAttribute('lang') ||
-      '';
+      this.ownerDocument.documentElement.getAttribute('lang') || '';
 
-    // only extract the `yyyy-mm-dd` part. When converting to Date, it will become midnight UTC and when rendered
-    // as localized date, will have a offset towards UTC, which we remove to shift the timestamp to midnight in the
-    // localized date. We should eventually use `Temporal.PlainDate` which will make the correction unnecessary.
-    // - https://stackoverflow.com/a/14569783/808699
-    // - https://tc39.es/proposal-temporal/docs/plaindate.html
-    const date = new Date(this.getAttribute('date').substring(0, 10));
-    const correctedDate = new Date(date.getTime() - date.getTimezoneOffset() * -60000);
+    // only use the first 10 characters, e.g. the `yyyy-mm-dd` part
+    const dateStr = this.getAttribute('date').substring(0, 10);
 
     if (!this.shadowRoot) this.attachShadow({mode: 'open'});
-    this.shadowRoot.textContent = correctedDate.toLocaleString(lang ?? [], {
+    this.shadowRoot.textContent = toAbsoluteLocaleDate(dateStr, lang, {
       ...(year && {year}),
       ...(month && {month}),
       ...(weekday && {weekday}),
diff --git a/web_src/js/webcomponents/absolute-date.test.js b/web_src/js/webcomponents/absolute-date.test.js
new file mode 100644
index 0000000000..ba04451b65
--- /dev/null
+++ b/web_src/js/webcomponents/absolute-date.test.js
@@ -0,0 +1,15 @@
+import {toAbsoluteLocaleDate} from './absolute-date.js';
+
+test('toAbsoluteLocaleDate', () => {
+  expect(toAbsoluteLocaleDate('2024-03-15', 'en-US', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+  })).toEqual('March 15, 2024');
+
+  expect(toAbsoluteLocaleDate('2024-03-15', 'de-DE', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+  })).toEqual('15. März 2024');
+});

From 7a6260f889e80856e2fb00bdfb8df90ec7652536 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 15 Mar 2024 17:45:30 +0800
Subject: [PATCH 384/679] Improve repo search UI (#29767)

1. Introduce a special "flex-items-block" for menu items, to align the
dropdown menu items
2. Simplify the "repo search" form
3. Add missing "TopicOnly" search option

Screenshots:

The old UI items don't align:

<details>

![image](https://github.com/go-gitea/gitea/assets/2114189/b965ac00-bad6-4d2f-9103-8841bd940aa5)

</details>


New UI (doesn't change much, but the items align)

<details>

![image](https://github.com/go-gitea/gitea/assets/2114189/a1add892-21dc-423b-90d5-5569faa3dced)


![image](https://github.com/go-gitea/gitea/assets/2114189/fb4040b2-96d8-4fb2-a0ed-760b9881fd86)

</details>

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 templates/admin/user/list.tmpl          |  2 +-
 templates/devtest/gitea-ui.tmpl         |  9 +++----
 templates/shared/repo_search.tmpl       | 32 +++++++++++--------------
 templates/shared/search/input.tmpl      |  2 +-
 tests/integration/explore_repos_test.go | 11 +++++++--
 web_src/css/base.css                    |  8 ++++++-
 6 files changed, 37 insertions(+), 27 deletions(-)

diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 11c2fa5940..091cbe7287 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -15,7 +15,7 @@
 					<div class="ui dropdown type jump item">
 						<span class="text">{{ctx.Locale.Tr "admin.users.list_status_filter.menu_text"}}</span>
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-						<div class="menu">
+						<div class="menu flex-items-menu">
 							<a class="item j-reset-status-filter">{{ctx.Locale.Tr "admin.users.list_status_filter.reset"}}</a>
 							<div class="divider"></div>
 							<label class="item"><input type="radio" name="status_filter[is_admin]" value="1"> {{ctx.Locale.Tr "admin.users.list_status_filter.is_admin"}}</label>
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 0b1f982ee4..284566046d 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -204,12 +204,13 @@
 
 		<h2>Dropdown with SVG</h2>
 		<div>
-			<div class="ui dropdown" style="border: 1px red dashed" data-tooltip-content="border for demo purpose only">
-				<span class="text">simple</span>
+			<div class="ui dropdown tw-border tw-border-red tw-border-dashed" data-tooltip-content="border for demo purpose only">
+				<span class="text">search-input &amp; flex-item in menu</span>
 				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-				<div class="menu">
+				<div class="menu flex-items-menu">
 					<div class="ui icon search input"><i class="icon">{{svg "octicon-search"}}</i><input type="text" value="search input in menu"></div>
-					<div class="item">item</div>
+					<div class="item"><input type="radio">item</div>
+					<div class="item"><input type="radio">item</div>
 				</div>
 			</div>
 			<div class="ui search selection dropdown">
diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo_search.tmpl
index 7ba0070863..7fcb5d2361 100644
--- a/templates/shared/repo_search.tmpl
+++ b/templates/shared/repo_search.tmpl
@@ -1,22 +1,18 @@
 <div class="ui small secondary filter menu">
-	<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-flex-row tw-gap-x-2 gt-ac">
-		{{if .Language}}<input hidden name="language" value="{{.Language}}">{{end}}
+	<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-gap-x-2">
+		{{if .Language}}<input type="hidden" name="language" value="{{.Language}}">{{end}}
+		{{if .PageIsExploreRepositories}}<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">{{end}}
+		{{if .TabName}}<input type="hidden" name="tab" value="{{.TabName}}">{{end}}
+		{{if .TopicOnly}}<input type="hidden" name="topic" value="{{.TopicOnly}}">{{end}}
 		<div class="ui small fluid action input tw-flex-1">
 			{{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.repo_kind")}}
-			{{if .PageIsExploreRepositories}}
-				<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">
-			{{else if .TabName}}
-				<input type="hidden" name="tab" value="{{.TabName}}">
-			{{end}}
 			{{template "shared/search/button"}}
 		</div>
 		<!-- Filter -->
-		<div class="ui small dropdown type jump item tw-mr-0">
-			<span class="text">
-				{{ctx.Locale.Tr "filter"}}
-			</span>
+		<div class="item ui small dropdown jump">
+			<span class="text">{{ctx.Locale.Tr "filter"}}</span>
 			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-			<div class="menu">
+			<div class="menu flex-items-menu">
 				<label class="item"><input type="radio" name="clear-filter"> {{ctx.Locale.Tr "filter.clear"}}</label>
 				<div class="divider"></div>
 				<label class="item"><input type="radio" name="archived" {{if .IsArchived.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_archived"}}</label>
@@ -36,10 +32,8 @@
 			</div>
 		</div>
 		<!-- Sort -->
-		<div class="ui small dropdown type jump item gt-mr-0">
-			<span class="text">
-				{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-			</span>
+		<div class="item ui small dropdown jump">
+			<span class="text">{{ctx.Locale.Tr "repo.issues.filter_sort"}}</span>
 			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 			<div class="menu">
 				<label class="{{if eq .SortType "newest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "newest"}}checked{{end}} value="newest"> {{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</label>
@@ -61,8 +55,10 @@
 	</form>
 </div>
 {{if and .PageIsExploreRepositories .OnlyShowRelevant}}
-	<div class="ui message explore-relevancy-note">
-		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}</span>
+	<div class="ui message">
+		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">
+			{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}
+		</span>
 	</div>
 {{end}}
 <div class="divider"></div>
diff --git a/templates/shared/search/input.tmpl b/templates/shared/search/input.tmpl
index 195cefc2f6..75bed07b80 100644
--- a/templates/shared/search/input.tmpl
+++ b/templates/shared/search/input.tmpl
@@ -1,4 +1,4 @@
 {{/* Value - value of the search field (for search results page) */}}
 {{/* Disabled (optional) - if search field has to be disabled */}}
 {{/* Placeholder (optional) - placeholder text to be used */}}
-<input type="search" spellcheck="false" name="q" maxlength="255" placeholder="{{with .Placeholder}}{{.}}{{else}}{{ctx.Locale.Tr "search.search"}}{{end}}"{{with .Value}} value="{{.}}"{{end}}{{if .Disabled}} disabled{{end}}>
+<input type="search" name="q"{{with .Value}} value="{{.}}"{{end}} maxlength="255" spellcheck="false" placeholder="{{with .Placeholder}}{{.}}{{else}}{{ctx.Locale.Tr "search.search"}}{{end}}"{{if .Disabled}} disabled{{end}}>
diff --git a/tests/integration/explore_repos_test.go b/tests/integration/explore_repos_test.go
index 26fd1dde64..1e3ab314fd 100644
--- a/tests/integration/explore_repos_test.go
+++ b/tests/integration/explore_repos_test.go
@@ -8,11 +8,18 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestExploreRepos(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
-	req := NewRequest(t, "GET", "/explore/repos")
-	MakeRequest(t, req, http.StatusOK)
+	req := NewRequest(t, "GET", "/explore/repos?q=TheKeyword&topic=1&language=TheLang")
+	resp := MakeRequest(t, req, http.StatusOK)
+	respStr := resp.Body.String()
+
+	assert.Contains(t, respStr, `<input type="hidden" name="topic" value="true">`)
+	assert.Contains(t, respStr, `<input type="hidden" name="language" value="TheLang">`)
+	assert.Contains(t, respStr, `<input type="search" name="q" value="TheKeyword"`)
 }
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 4083ec0413..2c055b7439 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1980,7 +1980,6 @@ table th[data-sortt-desc] .svg {
 .ui.ui.button,
 .ui.ui.dropdown,
 .ui.ui.label,
-.flex-items-inline > .item,
 .flex-text-inline {
   display: inline-flex;
   align-items: center;
@@ -2017,3 +2016,10 @@ table th[data-sortt-desc] .svg {
   align-items: center;
   gap: .25rem;
 }
+
+/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content */
+.ui.dropdown .menu.flex-items-menu > .item {
+  display: flex !important;
+  align-items: center;
+  gap: .5rem;
+}

From d08f4360c96e130e0454b76ecef9405f2bd312a1 Mon Sep 17 00:00:00 2001
From: coldWater <254244460@qq.com>
Date: Fri, 15 Mar 2024 18:59:11 +0800
Subject: [PATCH 385/679] Refactor graceful manager, fix misused WaitGroup
 (#29738)

Follow #29629
---
 modules/graceful/manager.go         |  5 ++-
 modules/graceful/manager_common.go  |  5 +--
 modules/graceful/manager_unix.go    | 44 ++++++++++++------------
 modules/graceful/manager_windows.go | 52 +++++++++++++++--------------
 4 files changed, 55 insertions(+), 51 deletions(-)

diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go
index f3f412863a..3f1115066a 100644
--- a/modules/graceful/manager.go
+++ b/modules/graceful/manager.go
@@ -233,7 +233,10 @@ func (g *Manager) setStateTransition(old, new state) bool {
 // At the moment the total number of servers (numberOfServersToCreate) are pre-defined as a const before global init,
 // so this function MUST be called if a server is not used.
 func (g *Manager) InformCleanup() {
-	g.createServerWaitGroup.Done()
+	g.createServerCond.L.Lock()
+	defer g.createServerCond.L.Unlock()
+	g.createdServer++
+	g.createServerCond.Signal()
 }
 
 // Done allows the manager to be viewed as a context.Context, it returns a channel that is closed when the server is finished terminating
diff --git a/modules/graceful/manager_common.go b/modules/graceful/manager_common.go
index 27196e1531..f6dbcc748d 100644
--- a/modules/graceful/manager_common.go
+++ b/modules/graceful/manager_common.go
@@ -42,8 +42,9 @@ type Manager struct {
 	terminateCtxCancel     context.CancelFunc
 	managerCtxCancel       context.CancelFunc
 	runningServerWaitGroup sync.WaitGroup
-	createServerWaitGroup  sync.WaitGroup
 	terminateWaitGroup     sync.WaitGroup
+	createServerCond       sync.Cond
+	createdServer          int
 	shutdownRequested      chan struct{}
 
 	toRunAtShutdown  []func()
@@ -52,7 +53,7 @@ type Manager struct {
 
 func newGracefulManager(ctx context.Context) *Manager {
 	manager := &Manager{ctx: ctx, shutdownRequested: make(chan struct{})}
-	manager.createServerWaitGroup.Add(numberOfServersToCreate)
+	manager.createServerCond.L = &sync.Mutex{}
 	manager.prepare(ctx)
 	manager.start()
 	return manager
diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go
index edf5fc248f..f49c42650c 100644
--- a/modules/graceful/manager_unix.go
+++ b/modules/graceful/manager_unix.go
@@ -57,20 +57,27 @@ func (g *Manager) start() {
 	// Handle clean up of unused provided listeners	and delayed start-up
 	startupDone := make(chan struct{})
 	go func() {
-		defer close(startupDone)
-		// Wait till we're done getting all the listeners and then close the unused ones
-		func() {
-			// FIXME: there is a fundamental design problem of the "manager" and the "wait group".
-			// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
-			// There is no clear solution besides a complete rewriting of the "manager"
-			defer func() {
-				_ = recover()
-			}()
-			g.createServerWaitGroup.Wait()
+		defer func() {
+			close(startupDone)
+			// Close the unused listeners and ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
+			_ = CloseProvidedListeners()
 		}()
-		// Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
-		_ = CloseProvidedListeners()
-		g.notify(readyMsg)
+		// Wait for all servers to be created
+		g.createServerCond.L.Lock()
+		for {
+			if g.createdServer >= numberOfServersToCreate {
+				g.createServerCond.L.Unlock()
+				g.notify(readyMsg)
+				return
+			}
+			select {
+			case <-g.IsShutdown():
+				g.createServerCond.L.Unlock()
+				return
+			default:
+			}
+			g.createServerCond.Wait()
+		}
 	}()
 	if setting.StartupTimeout > 0 {
 		go func() {
@@ -78,16 +85,7 @@ func (g *Manager) start() {
 			case <-startupDone:
 				return
 			case <-g.IsShutdown():
-				func() {
-					// When WaitGroup counter goes negative it will panic - we don't care about this so we can just ignore it.
-					defer func() {
-						_ = recover()
-					}()
-					// Ensure that the createServerWaitGroup stops waiting
-					for {
-						g.createServerWaitGroup.Done()
-					}
-				}()
+				g.createServerCond.Signal()
 				return
 			case <-time.After(setting.StartupTimeout):
 				log.Error("Startup took too long! Shutting down")
diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go
index ecf30af3f3..d776e0e9f9 100644
--- a/modules/graceful/manager_windows.go
+++ b/modules/graceful/manager_windows.go
@@ -149,33 +149,35 @@ hammerLoop:
 func (g *Manager) awaitServer(limit time.Duration) bool {
 	c := make(chan struct{})
 	go func() {
-		defer close(c)
-		func() {
-			// FIXME: there is a fundamental design problem of the "manager" and the "wait group".
-			// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
-			// There is no clear solution besides a complete rewriting of the "manager"
-			defer func() {
-				_ = recover()
-			}()
-			g.createServerWaitGroup.Wait()
-		}()
+		g.createServerCond.L.Lock()
+		for {
+			if g.createdServer >= numberOfServersToCreate {
+				g.createServerCond.L.Unlock()
+				close(c)
+				return
+			}
+			select {
+			case <-g.IsShutdown():
+				g.createServerCond.L.Unlock()
+				return
+			default:
+			}
+			g.createServerCond.Wait()
+		}
 	}()
+
+	var tc <-chan time.Time
 	if limit > 0 {
-		select {
-		case <-c:
-			return true // completed normally
-		case <-time.After(limit):
-			return false // timed out
-		case <-g.IsShutdown():
-			return false
-		}
-	} else {
-		select {
-		case <-c:
-			return true // completed normally
-		case <-g.IsShutdown():
-			return false
-		}
+		tc = time.After(limit)
+	}
+	select {
+	case <-c:
+		return true // completed normally
+	case <-tc:
+		return false // timed out
+	case <-g.IsShutdown():
+		g.createServerCond.Signal()
+		return false
 	}
 }
 

From 3b6e57273ae3fbefefd60daa0f826b0b3d15cf27 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 15 Mar 2024 15:12:08 +0200
Subject: [PATCH 386/679] Fix `for` attribute not pointing to the ID of the
 color picker (#29813)

It didn't include the word picker.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 templates/projects/view.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 1d03477a9f..a6e84024bc 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -41,7 +41,7 @@
 						</div>
 
 						<div class="field color-field">
-							<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
+							<label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
 							<div class="color picker column">
 								<input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
 								{{template "repo/issue/label_precolors"}}

From 66928946372c85cd41d404f63e3c1da1bdeca3ca Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 15 Mar 2024 16:27:51 +0100
Subject: [PATCH 387/679] Tweak labeler (#29809)

- `poetry.toml` does not picture dependencies
- Add `.vue` files to `modifies/js`
---
 .github/labeler.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/labeler.yml b/.github/labeler.yml
index a1209c77b8..980b9c337c 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -67,11 +67,10 @@ modifies/dependencies:
       - any-glob-to-any-file:
           - "package.json"
           - "package-lock.json"
-          - "poetry.toml"
+          - "pyproject.toml"
           - "poetry.lock"
           - "go.mod"
           - "go.sum"
-          - "pyproject.toml"
 
 modifies/go:
   - changed-files:
@@ -82,3 +81,4 @@ modifies/js:
   - changed-files:
       - any-glob-to-any-file:
           - "**/*.js"
+          - "**/*.vue"

From bfb0a5a41ecb040f66ab664e22250571f339826a Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 15 Mar 2024 20:02:43 +0200
Subject: [PATCH 388/679] Remove jQuery AJAX from the comment edit box (#29812)

- Removed all jQuery AJAX calls and replaced with our fetch wrapper
- Tested the file addition and removal functionality and it works as
before

# Demo using `fetch` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/846ed6d5-3798-43ca-920c-d619e9c3d745)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-legacy.js | 48 ++++++++++++++++++------------
 1 file changed, 29 insertions(+), 19 deletions(-)

diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 60950fd171..24fcc7c223 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -24,7 +24,7 @@ import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
 import {hideElem, showElem} from '../utils/dom.js';
 import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {attachRefIssueContextPopup} from './contextpopup.js';
-import {POST} from '../modules/fetch.js';
+import {POST, GET} from '../modules/fetch.js';
 
 const {csrfToken} = window.config;
 
@@ -83,7 +83,7 @@ export function initRepoCommentForm() {
           await POST(form.attr('action'), {data: params});
           window.location.reload();
         } catch (error) {
-          console.error('Error:', error);
+          console.error(error);
         }
       } else if (editMode === '') {
         $selectBranch.find('.ui .branch-name').text(selectedValue);
@@ -355,14 +355,15 @@ async function onEditContent(event) {
           const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
           $dropzone.find('.files').append(input);
         });
-        this.on('removedfile', (file) => {
+        this.on('removedfile', async (file) => {
           if (disableRemovedfileEvent) return;
           $(`#${file.uuid}`).remove();
           if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
-            $.post($dropzone.attr('data-remove-url'), {
-              file: file.uuid,
-              _csrf: csrfToken,
-            });
+            try {
+              await POST($dropzone.attr('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
+            } catch (error) {
+              console.error(error);
+            }
           }
         });
         this.on('submit', () => {
@@ -370,8 +371,10 @@ async function onEditContent(event) {
             fileUuidDict[fileUuid].submitted = true;
           });
         });
-        this.on('reload', () => {
-          $.getJSON($editContentZone.attr('data-attachment-url'), (data) => {
+        this.on('reload', async () => {
+          try {
+            const response = await GET($editContentZone.attr('data-attachment-url'));
+            const data = await response.json();
             // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
             disableRemovedfileEvent = true;
             dz.removeAllFiles(true);
@@ -390,7 +393,9 @@ async function onEditContent(event) {
               const input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
               $dropzone.find('.files').append(input);
             }
-          });
+          } catch (error) {
+            console.error(error);
+          }
         });
       },
     });
@@ -406,22 +411,25 @@ async function onEditContent(event) {
     }
   };
 
-  const saveAndRefresh = (dz) => {
+  const saveAndRefresh = async (dz) => {
     showElem($renderContent);
     hideElem($editContentZone);
-    $.post($editContentZone.attr('data-update-url'), {
-      _csrf: csrfToken,
-      content: comboMarkdownEditor.value(),
-      context: $editContentZone.attr('data-context'),
-      files: dz.files.map((file) => file.uuid),
-    }, (data) => {
+
+    try {
+      const params = new URLSearchParams({
+        content: comboMarkdownEditor.value(),
+        context: $editContentZone.attr('data-context'),
+      });
+      for (const file of dz.files) params.append('files[]', file.uuid);
+
+      const response = await POST($editContentZone.attr('data-update-url'), {data: params});
+      const data = await response.json();
       if (!data.content) {
         $renderContent.html($('#no-content').html());
         $rawContent.text('');
       } else {
         $renderContent.html(data.content);
         $rawContent.text(comboMarkdownEditor.value());
-
         const refIssues = $renderContent.find('p .ref-issue');
         attachRefIssueContextPopup(refIssues);
       }
@@ -442,7 +450,9 @@ async function onEditContent(event) {
       }
       initMarkupContent();
       initCommentContent();
-    });
+    } catch (error) {
+      console.error(error);
+    }
   };
 
   if (!$editContentZone.html()) {

From aa3012849ea59cdfb8464b0b71f28bffad19d54c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 15 Mar 2024 19:14:33 +0100
Subject: [PATCH 389/679] Dark theme color enhancements (#29822)

- Few very minor colors tweaks to dark theme. Slightly darker
background, slightly bluer secondary colors.
- Alias `--color-nav-hover-bg` in both themes.

Before:
<img width="1013" alt="Screenshot 2024-03-15 at 18 43 59"
src="https://github.com/go-gitea/gitea/assets/115237/ce4bdb0d-6e25-4fd6-88f5-dc8f9e3093cd">

After:
<img width="1016" alt="Screenshot 2024-03-15 at 19 02 04"
src="https://github.com/go-gitea/gitea/assets/115237/4a6dd5a1-a5b4-4fc2-9835-05a0c2c58c42">


Before:
<img width="1340" alt="Screenshot 2024-03-15 at 18 40 19"
src="https://github.com/go-gitea/gitea/assets/115237/4465fa9c-d529-4a05-94d7-e21080e0a153">

After:
<img width="1341" alt="Screenshot 2024-03-15 at 19 00 51"
src="https://github.com/go-gitea/gitea/assets/115237/6595afef-592b-42c4-a6cd-196968ba5881">
---
 web_src/css/themes/theme-gitea-dark.css  | 140 +++++++++++------------
 web_src/css/themes/theme-gitea-light.css |   2 +-
 2 files changed, 71 insertions(+), 71 deletions(-)

diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index c187888a38..4e38d75f65 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -30,45 +30,45 @@
   --color-primary-alpha-90: #4183c4e1;
   --color-primary-hover: var(--color-primary-light-1);
   --color-primary-active: var(--color-primary-light-2);
-  --color-secondary: #3f4346;
-  --color-secondary-dark-1: #464a4d;
-  --color-secondary-dark-2: #4f5356;
-  --color-secondary-dark-3: #5f6366;
-  --color-secondary-dark-4: #72767a;
-  --color-secondary-dark-5: #7f8488;
-  --color-secondary-dark-6: #8d9297;
-  --color-secondary-dark-7: #999ea3;
-  --color-secondary-dark-8: #a6abaf;
-  --color-secondary-dark-9: #aeb3b8;
-  --color-secondary-dark-10: #babfc4;
-  --color-secondary-dark-11: #c5cbd0;
-  --color-secondary-dark-12: #ced4da;
-  --color-secondary-dark-13: #d1d7dd;
-  --color-secondary-light-1: #313538;
-  --color-secondary-light-2: #272b2e;
-  --color-secondary-light-3: #1e2225;
-  --color-secondary-light-4: #171b1e;
-  --color-secondary-alpha-10: #3f434619;
-  --color-secondary-alpha-20: #3f434633;
-  --color-secondary-alpha-30: #3f43464b;
-  --color-secondary-alpha-40: #3f434666;
-  --color-secondary-alpha-50: #3f434680;
-  --color-secondary-alpha-60: #3f434699;
-  --color-secondary-alpha-70: #3f4346b3;
-  --color-secondary-alpha-80: #3f4346cc;
-  --color-secondary-alpha-90: #3f4346e1;
+  --color-secondary: #3b444a;
+  --color-secondary-dark-1: #424b51;
+  --color-secondary-dark-2: #4a545b;
+  --color-secondary-dark-3: #59646c;
+  --color-secondary-dark-4: #6b7681;
+  --color-secondary-dark-5: #78858f;
+  --color-secondary-dark-6: #87929d;
+  --color-secondary-dark-7: #939ea9;
+  --color-secondary-dark-8: #a1acb4;
+  --color-secondary-dark-9: #aab3bc;
+  --color-secondary-dark-10: #b6bfc8;
+  --color-secondary-dark-11: #c2cbd3;
+  --color-secondary-dark-12: #ccd4dc;
+  --color-secondary-dark-13: #cfd7df;
+  --color-secondary-light-1: #2e353b;
+  --color-secondary-light-2: #2b353e;
+  --color-secondary-light-3: #1c2227;
+  --color-secondary-light-4: #161b1f;
+  --color-secondary-alpha-10: #3b444a19;
+  --color-secondary-alpha-20: #3b444a33;
+  --color-secondary-alpha-30: #3b444a4b;
+  --color-secondary-alpha-40: #3b444a66;
+  --color-secondary-alpha-50: #3b444a80;
+  --color-secondary-alpha-60: #3b444a99;
+  --color-secondary-alpha-70: #3b444ab3;
+  --color-secondary-alpha-80: #3b444acc;
+  --color-secondary-alpha-90: #3b444ae1;
   --color-secondary-button: var(--color-secondary-dark-4);
   --color-secondary-hover: var(--color-secondary-dark-3);
   --color-secondary-active: var(--color-secondary-dark-2);
   /* console colors - used for actions console and console files */
   --color-console-fg: #f8f8f9;
   --color-console-fg-subtle: #bec4c8;
-  --color-console-bg: #181b1d;
-  --color-console-border: #313538;
-  --color-console-hover-bg: #ffffff16;
-  --color-console-active-bg: #313538;
-  --color-console-menu-bg: #272b2e;
-  --color-console-menu-border: #464a4d;
+  --color-console-bg: #171b1e;
+  --color-console-border: #2e353b;
+  --color-console-hover-bg: #e8e8ff16;
+  --color-console-active-bg: #2e353b;
+  --color-console-menu-bg: #252b30;
+  --color-console-menu-border: #424b51;
   /* named colors */
   --color-red: #cc4848;
   --color-orange: #cc580c;
@@ -81,7 +81,7 @@
   --color-purple: #b259d0;
   --color-pink: #d22e8b;
   --color-brown: #a47252;
-  --color-black: #1f2326;
+  --color-black: #1d2328;
   /* light variants - produced via Sass scale-color(color, $lightness: +10%) */
   --color-red-light: #d15a5a;
   --color-orange-light: #f6a066;
@@ -94,7 +94,7 @@
   --color-purple-light: #ba6ad5;
   --color-pink-light: #d74397;
   --color-brown-light: #b08061;
-  --color-black-light: #46494d;
+  --color-black-light: #424851;
   /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
   --color-red-dark-1: #c23636;
   --color-orange-dark-1: #f38236;
@@ -107,7 +107,7 @@
   --color-purple-dark-1: #a742c9;
   --color-pink-dark-1: #be297d;
   --color-brown-dark-1: #94674a;
-  --color-black-dark-1: #2c2f35;
+  --color-black-dark-1: #292e38;
   /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
   --color-red-dark-2: #ad3030;
   --color-orange-dark-2: #f16e17;
@@ -120,9 +120,9 @@
   --color-purple-dark-2: #9834b9;
   --color-pink-dark-2: #a9246f;
   --color-brown-dark-2: #835b42;
-  --color-black-dark-2: #292a2e;
+  --color-black-dark-2: #272930;
   /* ansi colors used for actions console and console files */
-  --color-ansi-black: #1f2326;
+  --color-ansi-black: #1d2328;
   --color-ansi-red: #cc4848;
   --color-ansi-green: #87ab63;
   --color-ansi-yellow: #cc9903;
@@ -130,7 +130,7 @@
   --color-ansi-magenta: #d22e8b;
   --color-ansi-cyan: #00918a;
   --color-ansi-white: var(--color-console-fg-subtle);
-  --color-ansi-bright-black: #46494d;
+  --color-ansi-bright-black: #424851;
   --color-ansi-bright-red: #d15a5a;
   --color-ansi-bright-green: #93b373;
   --color-ansi-bright-yellow: #eaaf03;
@@ -139,8 +139,8 @@
   --color-ansi-bright-cyan: #00b6ad;
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
-  --color-grey: #3c4043;
-  --color-grey-light: #898e92;
+  --color-grey: #384147;
+  --color-grey-light: #828f99;
   --color-gold: #b1983b;
   --color-white: #ffffff;
   --color-diff-removed-word-bg: #6f3333;
@@ -151,7 +151,7 @@
   --color-diff-removed-row-border: #634343;
   --color-diff-moved-row-border: #bcca6f;
   --color-diff-added-row-border: #314a37;
-  --color-diff-inactive: #24282b;
+  --color-diff-inactive: #22282d;
   --color-error-border: #a04141;
   --color-error-bg: #522;
   --color-error-bg-active: #744;
@@ -180,10 +180,10 @@
   --color-orange-badge-hover-bg: #f2711c4d;
   --color-git: #f05133;
   /* target-based colors */
-  --color-body: #1f2326;
-  --color-box-header: #202427;
-  --color-box-body: #191d20;
-  --color-box-body-highlight: #1d2124;
+  --color-body: #1e2224;
+  --color-box-header: #1a1d1f;
+  --color-box-body: #14171a;
+  --color-box-body-highlight: #121517;
   --color-text-dark: #f8f8f9;
   --color-text: #ced2d5;
   --color-text-light: #bec4c8;
@@ -191,46 +191,46 @@
   --color-text-light-2: #8d969c;
   --color-text-light-3: #747f87;
   --color-footer: var(--color-nav-bg);
-  --color-timeline: #383c3f;
+  --color-timeline: #353c42;
   --color-input-text: var(--color-text-dark);
-  --color-input-background: #161a1d;
-  --color-input-toggle-background: #313538;
+  --color-input-background: #151a1e;
+  --color-input-toggle-background: #2e353b;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: #191d20;
-  --color-light: #00000028;
+  --color-header-wrapper: #181c20;
+  --color-light: #00001728;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
-  --color-light-border: #ffffff28;
-  --color-hover: #ffffff19;
-  --color-active: #ffffff24;
-  --color-menu: #161a1d;
-  --color-card: #161a1d;
-  --color-markup-table-row: #ffffff06;
-  --color-markup-code-block: #ffffff16;
-  --color-button: #161a1d;
+  --color-light-border: #e8e8ff28;
+  --color-hover: #e8e8ff19;
+  --color-active: #e8e8ff24;
+  --color-menu: #151a1e;
+  --color-card: #151a1e;
+  --color-markup-table-row: #e8e8ff06;
+  --color-markup-code-block: #e8e8ff16;
+  --color-button: #151a1e;
   --color-code-bg: #191d20;
   --color-code-sidebar-bg: #1b1f22;
-  --color-shadow: #00000058;
+  --color-shadow: #00001758;
   --color-secondary-bg: #2f3135;
   --color-expand-button: #414348;
   --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-5);
   --color-project-board-bg: var(--color-secondary-light-2);
-  --color-project-board-dark-label: #111111;
-  --color-project-board-light-label: #eeeeee;
+  --color-project-board-dark-label: #0e1011;
+  --color-project-board-light-label: #dde0e2;
   --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
-  --color-reaction-bg: #ffffff12;
+  --color-reaction-bg: #e8e8ff12;
   --color-reaction-hover-bg: var(--color-primary-light-4);
   --color-reaction-active-bg: var(--color-primary-light-5);
-  --color-tooltip-text: #ffffff;
-  --color-tooltip-bg: #000000f0;
-  --color-nav-bg: #1b1f22;
-  --color-nav-hover-bg: #272b2e;
+  --color-tooltip-text: #fafafb;
+  --color-tooltip-bg: #000017f0;
+  --color-nav-bg: #16191c;
+  --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
   --color-label-text: var(--color-text);
-  --color-label-bg: #7a7f834b;
-  --color-label-hover-bg: #7a7f83a0;
-  --color-label-active-bg: #7a7f83ff;
+  --color-label-bg: #73828e4b;
+  --color-label-hover-bg: #73828ea0;
+  --color-label-active-bg: #73828eff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-5);
   --color-active-line: #534d1b;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index fbe2458ed6..4d0f7c6c13 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -225,7 +225,7 @@
   --color-tooltip-text: #ffffff;
   --color-tooltip-bg: #000000f0;
   --color-nav-bg: #ffffff;
-  --color-nav-hover-bg: #ebebeb;
+  --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
   --color-label-text: var(--color-text);
   --color-label-bg: #9d9d9d4b;

From c00633971a4e66835fffe7cfcea0f427689cf550 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 15 Mar 2024 20:24:27 +0200
Subject: [PATCH 390/679] Upgrade `htmx` to v1.9.11 (#29821)

Also added BSD Zero Clause License to the list of allowed licenses in
webpack.

Tested various `htmx` operations. Nothing broke.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 package-lock.json | 8 ++++----
 package.json      | 2 +-
 webpack.config.js | 5 ++---
 3 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 1b8ed2ddc2..1ec1a62105 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
         "esbuild-loader": "4.1.0",
         "escape-goat": "4.0.0",
         "fast-glob": "3.3.2",
-        "htmx.org": "1.9.10",
+        "htmx.org": "1.9.11",
         "idiomorph": "0.3.0",
         "jquery": "3.7.1",
         "katex": "0.16.9",
@@ -6780,9 +6780,9 @@
       }
     },
     "node_modules/htmx.org": {
-      "version": "1.9.10",
-      "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.10.tgz",
-      "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA=="
+      "version": "1.9.11",
+      "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.11.tgz",
+      "integrity": "sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw=="
     },
     "node_modules/http-proxy-agent": {
       "version": "7.0.2",
diff --git a/package.json b/package.json
index 80a577ba20..1be87e8b39 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
     "esbuild-loader": "4.1.0",
     "escape-goat": "4.0.0",
     "fast-glob": "3.3.2",
-    "htmx.org": "1.9.10",
+    "htmx.org": "1.9.11",
     "idiomorph": "0.3.0",
     "jquery": "3.7.1",
     "katex": "0.16.9",
diff --git a/webpack.config.js b/webpack.config.js
index eb23778bae..321ae561a4 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -243,11 +243,10 @@ export default {
       },
       override: {
         'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33
-        'htmx.org@1.9.10': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause"
-        'idiomorph@0.3.0': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause"
+        'idiomorph@0.3.0': {licenseName: 'BSD-2-Clause'}, // https://github.com/bigskysoftware/idiomorph/pull/37
       },
       emitError: true,
-      allow: '(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)',
+      allow: '(Apache-2.0 OR 0BSD OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)',
     }) : new AddAssetPlugin('licenses.txt', `Licenses are disabled during development`),
   ],
   performance: {

From 397997093870226dafb70c4a3133f2ebe0fe3e2c Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 15 Mar 2024 23:23:56 +0200
Subject: [PATCH 391/679] Remove jQuery AJAX from the project page (#29814)

Removed all jQuery AJAX calls and replaced with our fetch wrapper.

Tested the following functionalities and they work as before:
- column creation
- column deletion
- issue movement between columns
- column reordering
- column edit
- default column changing

# Demo using `fetch` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/99e6898f-baa3-462c-acec-46a910874dbe)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-projects.js | 128 +++++++++++++--------------
 1 file changed, 60 insertions(+), 68 deletions(-)

diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index 5a2a7e72ef..5dd80b29b9 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -2,8 +2,7 @@ import $ from 'jquery';
 import {useLightTextOnBackground} from '../utils/color.js';
 import tinycolor from 'tinycolor2';
 import {createSortable} from '../modules/sortable.js';
-
-const {csrfToken} = window.config;
+import {POST, DELETE, PUT} from '../modules/fetch.js';
 
 function updateIssueCount(cards) {
   const parent = cards.parentElement;
@@ -11,22 +10,23 @@ function updateIssueCount(cards) {
   parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt;
 }
 
-function createNewColumn(url, columnTitle, projectColorInput) {
-  $.ajax({
-    url,
-    data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val()}),
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-    contentType: 'application/json',
-    method: 'POST',
-  }).done(() => {
+async function createNewColumn(url, columnTitle, projectColorInput) {
+  try {
+    await POST(url, {
+      data: {
+        title: columnTitle.val(),
+        color: projectColorInput.val(),
+      },
+    });
+  } catch (error) {
+    console.error(error);
+  } finally {
     columnTitle.closest('form').removeClass('dirty');
     window.location.reload();
-  });
+  }
 }
 
-function moveIssue({item, from, to, oldIndex}) {
+async function moveIssue({item, from, to, oldIndex}) {
   const columnCards = to.getElementsByClassName('issue-card');
   updateIssueCount(from);
   updateIssueCount(to);
@@ -38,18 +38,14 @@ function moveIssue({item, from, to, oldIndex}) {
     })),
   };
 
-  $.ajax({
-    url: `${to.getAttribute('data-url')}/move`,
-    data: JSON.stringify(columnSorting),
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-    contentType: 'application/json',
-    type: 'POST',
-    error: () => {
-      from.insertBefore(item, from.children[oldIndex]);
-    },
-  });
+  try {
+    await POST(`${to.getAttribute('data-url')}/move`, {
+      data: columnSorting,
+    });
+  } catch (error) {
+    console.error(error);
+    from.insertBefore(item, from.children[oldIndex]);
+  }
 }
 
 async function initRepoProjectSortable() {
@@ -67,20 +63,21 @@ async function initRepoProjectSortable() {
     ghostClass: 'card-ghost',
     delayOnTouchOnly: true,
     delay: 500,
-    onSort: () => {
+    onSort: async () => {
       boardColumns = mainBoard.getElementsByClassName('project-column');
       for (let i = 0; i < boardColumns.length; i++) {
         const column = boardColumns[i];
         if (parseInt($(column).data('sorting')) !== i) {
-          $.ajax({
-            url: $(column).data('url'),
-            data: JSON.stringify({sorting: i, color: rgbToHex($(column).css('backgroundColor'))}),
-            headers: {
-              'X-Csrf-Token': csrfToken,
-            },
-            contentType: 'application/json',
-            method: 'PUT',
-          });
+          try {
+            await PUT($(column).data('url'), {
+              data: {
+                sorting: i,
+                color: rgbToHex($(column).css('backgroundColor')),
+              },
+            });
+          } catch (error) {
+            console.error(error);
+          }
         }
       }
     },
@@ -118,18 +115,19 @@ export function initRepoProject() {
       setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor')));
     }
 
-    $(this).find('.edit-project-column-button').on('click', function (e) {
+    $(this).find('.edit-project-column-button').on('click', async function (e) {
       e.preventDefault();
 
-      $.ajax({
-        url: $(this).data('url'),
-        data: JSON.stringify({title: projectTitleInput.val(), color: projectColorInput.val()}),
-        headers: {
-          'X-Csrf-Token': csrfToken,
-        },
-        contentType: 'application/json',
-        method: 'PUT',
-      }).done(() => {
+      try {
+        await PUT($(this).data('url'), {
+          data: {
+            title: projectTitleInput.val(),
+            color: projectColorInput.val(),
+          },
+        });
+      } catch (error) {
+        console.error(error);
+      } finally {
         projectTitleLabel.text(projectTitleInput.val());
         projectTitleInput.closest('form').removeClass('dirty');
         if (projectColorInput.val()) {
@@ -137,7 +135,7 @@ export function initRepoProject() {
         }
         boardColumn.attr('style', `background: ${projectColorInput.val()}!important`);
         $('.ui.modal').modal('hide');
-      });
+      }
     });
   });
 
@@ -146,19 +144,16 @@ export function initRepoProject() {
     const showButton = $(boardColumn).find('.default-project-column-show');
     const commitButton = $(this).find('.actions > .ok.button');
 
-    $(commitButton).on('click', (e) => {
+    $(commitButton).on('click', async (e) => {
       e.preventDefault();
 
-      $.ajax({
-        method: 'POST',
-        url: $(showButton).data('url'),
-        headers: {
-          'X-Csrf-Token': csrfToken,
-        },
-        contentType: 'application/json',
-      }).done(() => {
+      try {
+        await POST($(showButton).data('url'));
+      } catch (error) {
+        console.error(error);
+      } finally {
         window.location.reload();
-      });
+      }
     });
   });
 
@@ -167,19 +162,16 @@ export function initRepoProject() {
     const deleteColumnButton = deleteColumnModal.find('.actions > .ok.button');
     const deleteUrl = $(this).attr('data-url');
 
-    deleteColumnButton.on('click', (e) => {
+    deleteColumnButton.on('click', async (e) => {
       e.preventDefault();
 
-      $.ajax({
-        url: deleteUrl,
-        headers: {
-          'X-Csrf-Token': csrfToken,
-        },
-        contentType: 'application/json',
-        method: 'DELETE',
-      }).done(() => {
+      try {
+        await DELETE(deleteUrl);
+      } catch (error) {
+        console.error(error);
+      } finally {
         window.location.reload();
-      });
+      }
     });
   });
 
@@ -190,7 +182,7 @@ export function initRepoProject() {
     if (!columnTitle.val()) {
       return;
     }
-    const url = $(this).data('url');
+    const url = e.target.getAttribute('data-url');
     createNewColumn(url, columnTitle, projectColorInput);
   });
 }

From 3f1e4896b6d71dc061e23fa2dcac4c1b7d412540 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 15 Mar 2024 23:57:53 +0200
Subject: [PATCH 392/679] Remove the `time-since` class (#29826)

It serves no purpose.

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 modules/timeutil/datetime.go                 | 2 ++
 modules/timeutil/since.go                    | 4 ++--
 tests/integration/repo_test.go               | 2 +-
 web_src/js/components/DiffCommitSelector.vue | 2 +-
 4 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go
index 3ae44cb714..c089173560 100644
--- a/modules/timeutil/datetime.go
+++ b/modules/timeutil/datetime.go
@@ -13,6 +13,8 @@ import (
 
 // DateTime renders an absolute time HTML element by datetime.
 func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
+	// TODO: remove the extraAttrs argument, it's not used in any call to DateTime
+
 	if p, ok := datetime.(*time.Time); ok {
 		datetime = *p
 	}
diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go
index dfaa0e3e3a..dba42c793a 100644
--- a/modules/timeutil/since.go
+++ b/modules/timeutil/since.go
@@ -126,7 +126,7 @@ func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
 	}
 
 	// declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip
-	htm := fmt.Sprintf(`<relative-time class="time-since" prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`,
+	htm := fmt.Sprintf(`<relative-time prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`,
 		attrs, then.Format(time.RFC3339), friendlyText)
 	return template.HTML(htm)
 }
@@ -134,7 +134,7 @@ func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
 // TimeSince renders relative time HTML given a time.Time
 func TimeSince(then time.Time, lang translation.Locale) template.HTML {
 	if setting.UI.PreferredTimestampTense == "absolute" {
-		return DateTime("full", then, `class="time-since"`)
+		return DateTime("full", then)
 	}
 	return timeSinceUnix(then, time.Now(), lang)
 }
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index f141b6dcb1..06c55b1e8a 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -77,7 +77,7 @@ func testViewRepo(t *testing.T) {
 		})
 
 		// convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC"
-		htmlTimeString, _ := s.Find("relative-time.time-since").Attr("datetime")
+		htmlTimeString, _ := s.Find("relative-time").Attr("datetime")
 		htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString)
 		f.commitTime = htmlTime.In(time.Local).Format(time.RFC1123)
 		items = append(items, f)
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index 54877a18c0..780ba22f0c 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -248,7 +248,7 @@ export default {
               {{ commit.committer_or_author_name }}
               <span class="text right">
                 <!-- TODO: make this respect the PreferredTimestampTense setting -->
-                <relative-time class="time-since" prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
+                <relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
               </span>
             </div>
           </div>

From 83850cc4799285d766d0fb5751fff10a6e4d3353 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Fri, 15 Mar 2024 23:35:47 +0100
Subject: [PATCH 393/679] Better highlighting of archved labels (#29749)

as followup of the not jet finished discussion at
https://github.com/go-gitea/gitea/pull/29680#discussion_r1521867261

we enhance and chat about how best to highlight archived labels here :)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/css/repo.css | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index b7e8efd47e..848eb53327 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2410,7 +2410,8 @@
 }
 
 .archived-label {
-  filter: grayscale(0.8);
+  filter: grayscale(0.5);
+  opacity: 0.5;
 }
 
 .ui.label.scope-left {

From 68169133a3b0d29fe348ee065088d33f6dd1b087 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 16 Mar 2024 02:33:01 +0100
Subject: [PATCH 394/679] Light theme color enhancements (#29830)

Same as https://github.com/go-gitea/gitea/pull/29822 but for light
theme. Slight shift towards blue and made the themes match more, like on
header and footer background.

Before
<img width="1342" alt="Screenshot 2024-03-16 at 00 43 03"
src="https://github.com/go-gitea/gitea/assets/115237/b46021a1-241c-446a-b220-ca25cc90f3bf">


After
<img width="1343" alt="Screenshot 2024-03-16 at 00 45 21"
src="https://github.com/go-gitea/gitea/assets/115237/1c898875-a6bb-4bd3-b059-f82e1a145c99">


Before
<img width="1018" alt="Screenshot 2024-03-16 at 00 43 13"
src="https://github.com/go-gitea/gitea/assets/115237/d237ee7d-b4cc-4688-a074-1e96515ac475">

After
<img width="1022" alt="Screenshot 2024-03-16 at 00 43 50"
src="https://github.com/go-gitea/gitea/assets/115237/89b1da77-6bc9-4b38-9688-546e794aadfa">
---
 web_src/css/themes/theme-gitea-light.css | 134 +++++++++++------------
 1 file changed, 67 insertions(+), 67 deletions(-)

diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 4d0f7c6c13..eded03e371 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -30,33 +30,33 @@
   --color-primary-alpha-90: #4183c4e1;
   --color-primary-hover: var(--color-primary-dark-1);
   --color-primary-active: var(--color-primary-dark-2);
-  --color-secondary: #dedede;
-  --color-secondary-dark-1: #cecece;
-  --color-secondary-dark-2: #bfbfbf;
-  --color-secondary-dark-3: #a0a0a0;
-  --color-secondary-dark-4: #909090;
-  --color-secondary-dark-5: #818181;
-  --color-secondary-dark-6: #717171;
-  --color-secondary-dark-7: #626262;
-  --color-secondary-dark-8: #525252;
-  --color-secondary-dark-9: #434343;
-  --color-secondary-dark-10: #333333;
-  --color-secondary-dark-11: #242424;
-  --color-secondary-dark-12: #141414;
-  --color-secondary-dark-13: #040404;
-  --color-secondary-light-1: #e5e5e5;
-  --color-secondary-light-2: #ebebeb;
-  --color-secondary-light-3: #f2f2f2;
-  --color-secondary-light-4: #f8f8f8;
-  --color-secondary-alpha-10: #dedede19;
-  --color-secondary-alpha-20: #dedede33;
-  --color-secondary-alpha-30: #dedede4b;
-  --color-secondary-alpha-40: #dedede66;
-  --color-secondary-alpha-50: #dedede80;
-  --color-secondary-alpha-60: #dedede99;
-  --color-secondary-alpha-70: #dededeb3;
-  --color-secondary-alpha-80: #dededecc;
-  --color-secondary-alpha-90: #dededee1;
+  --color-secondary: #d0d7de;
+  --color-secondary-dark-1: #c7ced5;
+  --color-secondary-dark-2: #b9c0c7;
+  --color-secondary-dark-3: #99a0a7;
+  --color-secondary-dark-4: #899097;
+  --color-secondary-dark-5: #7a8188;
+  --color-secondary-dark-6: #6a7178;
+  --color-secondary-dark-7: #5b6269;
+  --color-secondary-dark-8: #4b5259;
+  --color-secondary-dark-9: #3c434a;
+  --color-secondary-dark-10: #2c333a;
+  --color-secondary-dark-11: #1d242b;
+  --color-secondary-dark-12: #0d141b;
+  --color-secondary-dark-13: #00040b;
+  --color-secondary-light-1: #dee5ec;
+  --color-secondary-light-2: #e4ebf2;
+  --color-secondary-light-3: #ebf2f9;
+  --color-secondary-light-4: #f1f8ff;
+  --color-secondary-alpha-10: #d0d7de19;
+  --color-secondary-alpha-20: #d0d7de33;
+  --color-secondary-alpha-30: #d0d7de4b;
+  --color-secondary-alpha-40: #d0d7de66;
+  --color-secondary-alpha-50: #d0d7de80;
+  --color-secondary-alpha-60: #d0d7de99;
+  --color-secondary-alpha-70: #d0d7deb3;
+  --color-secondary-alpha-80: #d0d7decc;
+  --color-secondary-alpha-90: #d0d7dee1;
   --color-secondary-button: var(--color-secondary-dark-4);
   --color-secondary-hover: var(--color-secondary-dark-5);
   --color-secondary-active: var(--color-secondary-dark-6);
@@ -81,7 +81,7 @@
   --color-purple: #a333c8;
   --color-pink: #e03997;
   --color-brown: #a5673f;
-  --color-black: #1b1c1d;
+  --color-black: #191c1d;
   /* light variants - produced via Sass scale-color(color, $lightness: +25%) */
   --color-red-light: #e45e5e;
   --color-orange-light: #f59555;
@@ -107,7 +107,7 @@
   --color-purple-dark-1: #932eb4;
   --color-pink-dark-1: #db228a;
   --color-brown-dark-1: #955d39;
-  --color-black-dark-1: #18191a;
+  --color-black-dark-1: #16191c;
   /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
   --color-red-dark-2: #b11e1e;
   --color-orange-dark-2: #cc580c;
@@ -120,7 +120,7 @@
   --color-purple-dark-2: #8229a0;
   --color-pink-dark-2: #c21e7b;
   --color-brown-dark-2: #845232;
-  --color-black-dark-2: #161617;
+  --color-black-dark-2: #131619;
   /* ansi colors used for actions console and console files */
   --color-ansi-black: #1f2326;
   --color-ansi-red: #cc4848;
@@ -139,8 +139,8 @@
   --color-ansi-bright-cyan: #00b6ad;
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
-  --color-grey: #707070;
-  --color-grey-light: #838383;
+  --color-grey: #697077;
+  --color-grey-light: #7c838a;
   --color-gold: #a1882b;
   --color-white: #ffffff;
   --color-diff-removed-word-bg: #fdb8c0;
@@ -151,7 +151,7 @@
   --color-diff-removed-row-border: #f1c0c0;
   --color-diff-moved-row-border: #d0e27f;
   --color-diff-added-row-border: #e6ffed;
-  --color-diff-inactive: #f2f2f2;
+  --color-diff-inactive: #f0f2f4;
   --color-error-border: #e0b4b4;
   --color-error-bg: #fff6f6;
   --color-error-bg-active: #fbb;
@@ -181,56 +181,56 @@
   --color-git: #f05133;
   /* target-based colors */
   --color-body: #ffffff;
-  --color-box-header: #f7f7f7;
+  --color-box-header: #f1f3f5;
   --color-box-body: #ffffff;
-  --color-box-body-highlight: #fafafa;
-  --color-text-dark: #080808;
-  --color-text: #212121;
-  --color-text-light: #555555;
-  --color-text-light-1: #6a6a6a;
-  --color-text-light-2: #808080;
-  --color-text-light-3: #a0a0a0;
+  --color-box-body-highlight: #f4faff;
+  --color-text-dark: #03080d;
+  --color-text: #1c2126;
+  --color-text-light: #3c434a;
+  --color-text-light-1: #4b5259;
+  --color-text-light-2: #6a7178;
+  --color-text-light-3: #899097;
   --color-footer: var(--color-nav-bg);
-  --color-timeline: #ececec;
+  --color-timeline: #d0d7de;
   --color-input-text: var(--color-text-dark);
-  --color-input-background: #fafafa;
-  --color-input-toggle-background: #dedede;
+  --color-input-background: #f8f9fb;
+  --color-input-toggle-background: #d0d7de;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: transparent;
-  --color-light: #00000006;
+  --color-header-wrapper: #fafbfc;
+  --color-light: #00001706;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
-  --color-light-border: #0000001d;
-  --color-hover: #00000014;
-  --color-active: #0000001b;
-  --color-menu: #fafafa;
-  --color-card: #fafafa;
-  --color-markup-table-row: #00000008;
-  --color-markup-code-block: #00000010;
-  --color-button: #fafafa;
-  --color-code-bg: #ffffff;
-  --color-code-sidebar-bg: #f5f5f5;
-  --color-shadow: #00000026;
-  --color-secondary-bg: #f4f4f4;
+  --color-light-border: #0000171d;
+  --color-hover: #00001708;
+  --color-active: #00001714;
+  --color-menu: #f8f9fb;
+  --color-card: #f8f9fb;
+  --color-markup-table-row: #00001708;
+  --color-markup-code-block: #00001710;
+  --color-button: #f8f9fb;
+  --color-code-bg: #fafdff;
+  --color-code-sidebar-bg: #f2f5f8;
+  --color-shadow: #00001726;
+  --color-secondary-bg: #f2f5f8;
   --color-expand-button: #d8efff;
   --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-6);
   --color-project-board-bg: var(--color-secondary-light-4);
-  --color-project-board-dark-label: #111111;
-  --color-project-board-light-label: #eeeeee;
+  --color-project-board-dark-label: #0e1114;
+  --color-project-board-light-label: #eaeef2;
   --color-caret: var(--color-text-dark);
-  --color-reaction-bg: #0000000a;
+  --color-reaction-bg: #0000170a;
   --color-reaction-hover-bg: var(--color-primary-light-5);
   --color-reaction-active-bg: var(--color-primary-light-6);
-  --color-tooltip-text: #ffffff;
-  --color-tooltip-bg: #000000f0;
-  --color-nav-bg: #ffffff;
+  --color-tooltip-text: #fbfdff;
+  --color-tooltip-bg: #000017f0;
+  --color-nav-bg: #f8f9fb;
   --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
   --color-label-text: var(--color-text);
-  --color-label-bg: #9d9d9d4b;
-  --color-label-hover-bg: #9d9d9da0;
-  --color-label-active-bg: #9d9d9dff;
+  --color-label-bg: #949da64b;
+  --color-label-hover-bg: #949da6a0;
+  --color-label-active-bg: #949da6ff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-6);
   --color-active-line: #fffbdd;

From 043f55fabfadd765125690052920ba31ebd3817b Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 16 Mar 2024 03:56:17 +0200
Subject: [PATCH 395/679] Remove jQuery AJAX from the notifications (#29817)

- Removed 2 jQuery AJAX calls and replaced with our fetch wrapper
- Deleted an AJAX call that wasn't attached to any element since #24989
- Tested the notification count and notification table functionality and
it works as before

# Demo using `fetch` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/ff862a9a-1c88-41cc-bd01-5a0711dbd6f8)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/notification.js | 106 +++++++++++-----------------
 1 file changed, 40 insertions(+), 66 deletions(-)

diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index a9236247c6..3d34f21fd4 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -1,6 +1,7 @@
 import $ from 'jquery';
+import {GET} from '../modules/fetch.js';
 
-const {appSubUrl, csrfToken, notificationSettings, assetVersionEncoded} = window.config;
+const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
 let notificationSequenceNumber = 0;
 
 export function initNotificationsTable() {
@@ -27,25 +28,6 @@ export function initNotificationsTable() {
       e.target.closest('.notifications-item').setAttribute('data-remove', 'true');
     });
   }
-
-  $('#notification_table .button').on('click', function () {
-    (async () => {
-      const data = await updateNotification(
-        $(this).data('url'),
-        $(this).data('status'),
-        $(this).data('page'),
-        $(this).data('q'),
-        $(this).data('notification-id'),
-      );
-
-      if ($(data).data('sequence-number') === notificationSequenceNumber) {
-        $('#notification_div').replaceWith(data);
-        initNotificationsTable();
-      }
-      await updateNotificationCount();
-    })();
-    return false;
-  });
 }
 
 async function receiveUpdateCount(event) {
@@ -163,58 +145,50 @@ async function updateNotificationCountWithCallback(callback, timeout, lastCount)
 async function updateNotificationTable() {
   const notificationDiv = $('#notification_div');
   if (notificationDiv.length > 0) {
-    const data = await $.ajax({
-      type: 'GET',
-      url: `${appSubUrl}/notifications${window.location.search}`,
-      data: {
-        'div-only': true,
-        'sequence-number': ++notificationSequenceNumber,
+    try {
+      const params = new URLSearchParams(window.location.search);
+      params.set('div-only', true);
+      params.set('sequence-number', ++notificationSequenceNumber);
+      const url = `${appSubUrl}/notifications?${params.toString()}`;
+      const response = await GET(url);
+
+      if (!response.ok) {
+        throw new Error('Failed to fetch notification table');
       }
-    });
-    if ($(data).data('sequence-number') === notificationSequenceNumber) {
-      notificationDiv.replaceWith(data);
-      initNotificationsTable();
+
+      const data = await response.text();
+      if ($(data).data('sequence-number') === notificationSequenceNumber) {
+        notificationDiv.replaceWith(data);
+        initNotificationsTable();
+      }
+    } catch (error) {
+      console.error(error);
     }
   }
 }
 
 async function updateNotificationCount() {
-  const data = await $.ajax({
-    type: 'GET',
-    url: `${appSubUrl}/notifications/new`,
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-  });
+  try {
+    const response = await GET(`${appSubUrl}/notifications/new`);
 
-  const notificationCount = $('.notification_count');
-  if (data.new === 0) {
-    notificationCount.addClass('gt-hidden');
-  } else {
-    notificationCount.removeClass('gt-hidden');
+    if (!response.ok) {
+      throw new Error('Failed to fetch notification count');
+    }
+
+    const data = await response.json();
+
+    const notificationCount = $('.notification_count');
+    if (data.new === 0) {
+      notificationCount.addClass('gt-hidden');
+    } else {
+      notificationCount.removeClass('gt-hidden');
+    }
+
+    notificationCount.text(`${data.new}`);
+
+    return `${data.new}`;
+  } catch (error) {
+    console.error(error);
+    return '0';
   }
-
-  notificationCount.text(`${data.new}`);
-
-  return `${data.new}`;
-}
-
-async function updateNotification(url, status, page, q, notificationID) {
-  if (status !== 'pinned') {
-    $(`#notification_${notificationID}`).remove();
-  }
-
-  return $.ajax({
-    type: 'POST',
-    url,
-    data: {
-      _csrf: csrfToken,
-      notification_id: notificationID,
-      status,
-      page,
-      q,
-      noredirect: true,
-      'sequence-number': ++notificationSequenceNumber,
-    },
-  });
 }

From 6ead30dbc469803d7e8361b13217e07d87d6f80d Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 16 Mar 2024 04:01:25 +0200
Subject: [PATCH 396/679] Forbid jQuery AJAX (#29818)

Please use the fetch wrapper instead, or even better `htmx`.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: 6543 <6543@obermui.de>
---
 .eslintrc.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index e9991c02ba..f84cb39a41 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -283,7 +283,7 @@ rules:
   i/unambiguous: [0]
   init-declarations: [0]
   jquery/no-ajax-events: [2]
-  jquery/no-ajax: [0]
+  jquery/no-ajax: [2]
   jquery/no-animate: [2]
   jquery/no-attr: [0]
   jquery/no-bind: [2]
@@ -396,7 +396,7 @@ rules:
   no-irregular-whitespace: [2]
   no-iterator: [2]
   no-jquery/no-ajax-events: [2]
-  no-jquery/no-ajax: [0]
+  no-jquery/no-ajax: [2]
   no-jquery/no-and-self: [2]
   no-jquery/no-animate-toggle: [2]
   no-jquery/no-animate: [2]

From e0ea3811c4178ffa30452b7ca4bd211e59326f91 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 16 Mar 2024 17:20:13 +0800
Subject: [PATCH 397/679] Refactor AddParam to AddParamIfExist (#29834)

When read the code: `pager.AddParam(ctx, "search", "search")`, the
question always comes: What is it doing? Where is the value from? Why
"search" / "search" ?

Now it is clear: `pager.AddParamIfExist("search", ctx.Data["search"])`
---
 routers/web/admin/repos.go       |  4 ++--
 routers/web/explore/code.go      |  2 +-
 routers/web/explore/repo.go      |  4 ++--
 routers/web/org/home.go          |  2 +-
 routers/web/org/projects.go      |  2 +-
 routers/web/repo/commit.go       |  4 ++--
 routers/web/repo/issue.go        | 20 ++++++++++----------
 routers/web/repo/milestone.go    |  4 ++--
 routers/web/repo/packages.go     |  4 ++--
 routers/web/repo/projects.go     |  2 +-
 routers/web/repo/search.go       |  2 +-
 routers/web/user/code.go         |  2 +-
 routers/web/user/home.go         | 24 ++++++++++++------------
 routers/web/user/notification.go |  4 ++--
 routers/web/user/package.go      |  4 ++--
 routers/web/user/profile.go      |  6 +++---
 services/context/pagination.go   | 17 ++++++++---------
 17 files changed, 53 insertions(+), 54 deletions(-)

diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index ddf4440167..5504037df0 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -84,7 +84,7 @@ func UnadoptedRepos(ctx *context.Context) {
 	if !doSearch {
 		pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
 		pager.SetDefaultParams(ctx)
-		pager.AddParam(ctx, "search", "search")
+		pager.AddParamIfExist("search", ctx.Data["search"])
 		ctx.Data["Page"] = pager
 		ctx.HTML(http.StatusOK, tplUnadoptedRepos)
 		return
@@ -98,7 +98,7 @@ func UnadoptedRepos(ctx *context.Context) {
 	ctx.Data["Dirs"] = repoNames
 	pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "search", "search")
+	pager.AddParamIfExist("search", ctx.Data["search"])
 	ctx.Data["Page"] = pager
 	ctx.HTML(http.StatusOK, tplUnadoptedRepos)
 }
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index 7ba5984002..bfc798a97d 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -127,7 +127,7 @@ func Code(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "l", "Language")
+	pager.AddParamIfExist("l", ctx.Data["Language"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplExploreCode)
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index cf7381512b..2cc22c50cf 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -169,8 +169,8 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 
 	pager := context.NewPagination(int(count), opts.PageSize, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "topic", "TopicOnly")
-	pager.AddParam(ctx, "language", "Language")
+	pager.AddParamIfExist("topic", ctx.Data["TopicOnly"])
+	pager.AddParamIfExist("language", ctx.Data["Language"])
 	pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant))
 	ctx.Data["Page"] = pager
 
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 71d10f3a43..947721dc41 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -154,7 +154,7 @@ func Home(ctx *context.Context) {
 
 	pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "language", "Language")
+	pager.AddParamIfExist("language", ctx.Data["Language"])
 	ctx.Data["Page"] = pager
 
 	ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index ad8bb90d9e..094d14d194 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -120,7 +120,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamIfExist("state", ctx.Data["State"])
 	ctx.Data["Page"] = pager
 
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 1b99f4183c..21fafd4901 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -163,8 +163,8 @@ func Graph(ctx *context.Context) {
 	ctx.Data["CommitCount"] = commitsCount
 
 	paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
-	paginator.AddParam(ctx, "mode", "Mode")
-	paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
+	paginator.AddParamIfExist("mode", ctx.Data["Mode"])
+	paginator.AddParamIfExist("hide-pr-refs", ctx.Data["HidePRRefs"])
 	for _, branch := range branches {
 		paginator.AddParamString("branch", branch)
 	}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index d5fd8439f3..69e09371b3 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -472,16 +472,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
 	}
 	ctx.Data["ShowArchivedLabels"] = archived
 
-	pager.AddParam(ctx, "q", "Keyword")
-	pager.AddParam(ctx, "type", "ViewType")
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
-	pager.AddParam(ctx, "labels", "SelectLabels")
-	pager.AddParam(ctx, "milestone", "MilestoneID")
-	pager.AddParam(ctx, "project", "ProjectID")
-	pager.AddParam(ctx, "assignee", "AssigneeID")
-	pager.AddParam(ctx, "poster", "PosterID")
-	pager.AddParam(ctx, "archived", "ShowArchivedLabels")
+	pager.AddParamIfExist("q", ctx.Data["Keyword"])
+	pager.AddParamIfExist("type", ctx.Data["ViewType"])
+	pager.AddParamIfExist("sort", ctx.Data["SortType"])
+	pager.AddParamIfExist("state", ctx.Data["State"])
+	pager.AddParamIfExist("labels", ctx.Data["SelectLabels"])
+	pager.AddParamIfExist("milestone", ctx.Data["MilestoneID"])
+	pager.AddParamIfExist("project", ctx.Data["ProjectID"])
+	pager.AddParamIfExist("assignee", ctx.Data["AssigneeID"])
+	pager.AddParamIfExist("poster", ctx.Data["PosterID"])
+	pager.AddParamIfExist("archived", ctx.Data["ShowArchivedLabels"])
 
 	ctx.Data["Page"] = pager
 }
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index c41b844ce4..2f7c2cb187 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -106,8 +106,8 @@ func Milestones(ctx *context.Context) {
 	ctx.Data["IsShowClosed"] = isShowClosed
 
 	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
-	pager.AddParam(ctx, "state", "State")
-	pager.AddParam(ctx, "q", "Keyword")
+	pager.AddParamIfExist("state", ctx.Data["State"])
+	pager.AddParamIfExist("q", ctx.Data["Keyword"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplMilestone)
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index 11874ab0d0..41fc38dedb 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -70,8 +70,8 @@ func Packages(ctx *context.Context) {
 	ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Query")
-	pager.AddParam(ctx, "type", "PackageType")
+	pager.AddParamIfExist("q", ctx.Data["Query"])
+	pager.AddParamIfExist("type", ctx.Data["PackageType"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplPackagesList)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 86909b5fd0..39f1b62ff7 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -118,7 +118,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamIfExist("state", ctx.Data["State"])
 	ctx.Data["Page"] = pager
 
 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 16e620f57d..73537e64bc 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -59,7 +59,7 @@ func Search(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "l", "Language")
+	pager.AddParamIfExist("l", ctx.Data["Language"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplSearch)
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index 92911edfe9..a8ac50639d 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -112,7 +112,7 @@ func CodeSearch(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "l", "Language")
+	pager.AddParamIfExist("l", ctx.Data["Language"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplUserCode)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 4ec6f6be3f..dddd03e21f 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -133,7 +133,7 @@ func Dashboard(ctx *context.Context) {
 	ctx.Data["Feeds"] = feeds
 
 	pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
-	pager.AddParam(ctx, "date", "Date")
+	pager.AddParamIfExist("date", ctx.Data["Date"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplDashboard)
@@ -329,10 +329,10 @@ func Milestones(ctx *context.Context) {
 	ctx.Data["IsShowClosed"] = isShowClosed
 
 	pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Keyword")
-	pager.AddParam(ctx, "repos", "RepoIDs")
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamIfExist("q", ctx.Data["Keyword"])
+	pager.AddParamIfExist("repos", ctx.Data["RepoIDs"])
+	pager.AddParamIfExist("sort", ctx.Data["SortType"])
+	pager.AddParamIfExist("state", ctx.Data["State"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplMilestones)
@@ -632,13 +632,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	}
 
 	pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Keyword")
-	pager.AddParam(ctx, "type", "ViewType")
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
-	pager.AddParam(ctx, "labels", "SelectLabels")
-	pager.AddParam(ctx, "milestone", "MilestoneID")
-	pager.AddParam(ctx, "assignee", "AssigneeID")
+	pager.AddParamIfExist("q", ctx.Data["Keyword"])
+	pager.AddParamIfExist("type", ctx.Data["ViewType"])
+	pager.AddParamIfExist("sort", ctx.Data["SortType"])
+	pager.AddParamIfExist("state", ctx.Data["State"])
+	pager.AddParamIfExist("labels", ctx.Data["SelectLabels"])
+	pager.AddParamIfExist("milestone", ctx.Data["MilestoneID"])
+	pager.AddParamIfExist("assignee", ctx.Data["AssigneeID"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplIssues)
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 324205ed91..81afeae043 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -344,8 +344,8 @@ func NotificationSubscriptions(ctx *context.Context) {
 		ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
 		return
 	}
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamIfExist("sort", ctx.Data["SortType"])
+	pager.AddParamIfExist("state", ctx.Data["State"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 3ecc59a2ab..911ca12bf0 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -125,8 +125,8 @@ func ListPackages(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Query")
-	pager.AddParam(ctx, "type", "PackageType")
+	pager.AddParamIfExist("q", ctx.Data["Query"])
+	pager.AddParamIfExist("type", ctx.Data["PackageType"])
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplPackagesList)
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 9851ea90a6..f9df511f60 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -324,12 +324,12 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 
 	pager := context.NewPagination(total, pagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "tab", "TabName")
+	pager.AddParamIfExist("tab", ctx.Data["TabName"])
 	if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
-		pager.AddParam(ctx, "language", "Language")
+		pager.AddParamIfExist("language", ctx.Data["Language"])
 	}
 	if tab == "activity" {
-		pager.AddParam(ctx, "date", "Date")
+		pager.AddParamIfExist("date", ctx.Data["Date"])
 	}
 	ctx.Data["Page"] = pager
 }
diff --git a/services/context/pagination.go b/services/context/pagination.go
index 655a278f9f..11d37283c9 100644
--- a/services/context/pagination.go
+++ b/services/context/pagination.go
@@ -26,14 +26,13 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination {
 	return p
 }
 
-// AddParam adds a value from context identified by ctxKey as link param under a given paramKey
-func (p *Pagination) AddParam(ctx *Context, paramKey, ctxKey string) {
-	_, exists := ctx.Data[ctxKey]
-	if !exists {
+// AddParamIfExist adds a value to the query parameters if the value is not nil
+func (p *Pagination) AddParamIfExist(key string, val any) {
+	if val == nil {
 		return
 	}
-	paramData := fmt.Sprintf("%v", ctx.Data[ctxKey]) // cast any to string
-	urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(paramKey), url.QueryEscape(paramData))
+	paramData := fmt.Sprint(val)
+	urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(paramData))
 	p.urlParams = append(p.urlParams, urlParam)
 }
 
@@ -50,8 +49,8 @@ func (p *Pagination) GetParams() template.URL {
 
 // SetDefaultParams sets common pagination params that are often used
 func (p *Pagination) SetDefaultParams(ctx *Context) {
-	p.AddParam(ctx, "sort", "SortType")
-	p.AddParam(ctx, "q", "Keyword")
+	p.AddParamIfExist("sort", ctx.Data["SortType"])
+	p.AddParamIfExist("q", ctx.Data["Keyword"])
 	// do not add any more uncommon params here!
-	p.AddParam(ctx, "fuzzy", "IsFuzzy")
+	p.AddParamIfExist("fuzzy", ctx.Data["IsFuzzy"])
 }

From 1262ff6734543b37d834e63a6a623648c77ee4f4 Mon Sep 17 00:00:00 2001
From: 6543 <m.huber@kithara.com>
Date: Sat, 16 Mar 2024 11:32:45 +0100
Subject: [PATCH 398/679] Refactor code_indexer to use an SearchOptions struct
 for PerformSearch (#29724)

similar to how it's already done for the issue_indexer


---
*Sponsored by Kithara Software GmbH*
---
 modules/indexer/code/bleve/bleve.go           | 26 +++++++++----------
 .../code/elasticsearch/elasticsearch.go       | 26 ++++++++-----------
 modules/indexer/code/git.go                   |  2 +-
 modules/indexer/code/indexer_test.go          | 11 +++++++-
 modules/indexer/code/internal/indexer.go      | 15 +++++++++--
 modules/indexer/code/search.go                |  8 +++---
 routers/web/explore/code.go                   | 12 ++++++++-
 routers/web/repo/search.go                    | 13 ++++++++--
 routers/web/user/code.go                      | 12 ++++++++-
 9 files changed, 86 insertions(+), 39 deletions(-)

diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go
index 107dd23598..d7f735e957 100644
--- a/modules/indexer/code/bleve/bleve.go
+++ b/modules/indexer/code/bleve/bleve.go
@@ -142,7 +142,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
 			return err
 		}
 		if size, err = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64); err != nil {
-			return fmt.Errorf("Misformatted git cat-file output: %w", err)
+			return fmt.Errorf("misformatted git cat-file output: %w", err)
 		}
 	}
 
@@ -233,26 +233,26 @@ func (b *Indexer) Delete(_ context.Context, repoID int64) error {
 
 // Search searches for files in the specified repo.
 // Returns the matching file-paths
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
 	var (
 		indexerQuery query.Query
 		keywordQuery query.Query
 	)
 
-	if isFuzzy {
-		phraseQuery := bleve.NewMatchPhraseQuery(keyword)
+	if opts.IsKeywordFuzzy {
+		phraseQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
 		phraseQuery.FieldVal = "Content"
 		phraseQuery.Analyzer = repoIndexerAnalyzer
 		keywordQuery = phraseQuery
 	} else {
-		prefixQuery := bleve.NewPrefixQuery(keyword)
+		prefixQuery := bleve.NewPrefixQuery(opts.Keyword)
 		prefixQuery.FieldVal = "Content"
 		keywordQuery = prefixQuery
 	}
 
-	if len(repoIDs) > 0 {
-		repoQueries := make([]query.Query, 0, len(repoIDs))
-		for _, repoID := range repoIDs {
+	if len(opts.RepoIDs) > 0 {
+		repoQueries := make([]query.Query, 0, len(opts.RepoIDs))
+		for _, repoID := range opts.RepoIDs {
 			repoQueries = append(repoQueries, inner_bleve.NumericEqualityQuery(repoID, "RepoID"))
 		}
 
@@ -266,8 +266,8 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 
 	// Save for reuse without language filter
 	facetQuery := indexerQuery
-	if len(language) > 0 {
-		languageQuery := bleve.NewMatchQuery(language)
+	if len(opts.Language) > 0 {
+		languageQuery := bleve.NewMatchQuery(opts.Language)
 		languageQuery.FieldVal = "Language"
 		languageQuery.Analyzer = analyzer_keyword.Name
 
@@ -277,12 +277,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 		)
 	}
 
-	from := (page - 1) * pageSize
+	from, pageSize := opts.GetSkipTake()
 	searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
 	searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
 	searchRequest.IncludeLocations = true
 
-	if len(language) == 0 {
+	if len(opts.Language) == 0 {
 		searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10))
 	}
 
@@ -326,7 +326,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 	}
 
 	searchResultLanguages := make([]*internal.SearchResultLanguages, 0, 10)
-	if len(language) > 0 {
+	if len(opts.Language) > 0 {
 		// Use separate query to go get all language counts
 		facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false)
 		facetRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go
index 065b0b2061..e4622fd66e 100644
--- a/modules/indexer/code/elasticsearch/elasticsearch.go
+++ b/modules/indexer/code/elasticsearch/elasticsearch.go
@@ -281,18 +281,18 @@ func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLan
 }
 
 // Search searches for codes and language stats by given conditions.
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
 	searchType := esMultiMatchTypePhrasePrefix
-	if isFuzzy {
+	if opts.IsKeywordFuzzy {
 		searchType = esMultiMatchTypeBestFields
 	}
 
-	kwQuery := elastic.NewMultiMatchQuery(keyword, "content").Type(searchType)
+	kwQuery := elastic.NewMultiMatchQuery(opts.Keyword, "content").Type(searchType)
 	query := elastic.NewBoolQuery()
 	query = query.Must(kwQuery)
-	if len(repoIDs) > 0 {
-		repoStrs := make([]any, 0, len(repoIDs))
-		for _, repoID := range repoIDs {
+	if len(opts.RepoIDs) > 0 {
+		repoStrs := make([]any, 0, len(opts.RepoIDs))
+		for _, repoID := range opts.RepoIDs {
 			repoStrs = append(repoStrs, repoID)
 		}
 		repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...)
@@ -300,16 +300,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 	}
 
 	var (
-		start       int
-		kw          = "<em>" + keyword + "</em>"
-		aggregation = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc()
+		start, pageSize = opts.GetSkipTake()
+		kw              = "<em>" + opts.Keyword + "</em>"
+		aggregation     = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc()
 	)
 
-	if page > 0 {
-		start = (page - 1) * pageSize
-	}
-
-	if len(language) == 0 {
+	if len(opts.Language) == 0 {
 		searchResult, err := b.inner.Client.Search().
 			Index(b.inner.VersionedIndexName()).
 			Aggregation("language", aggregation).
@@ -330,7 +326,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 		return convertResult(searchResult, kw, pageSize)
 	}
 
-	langQuery := elastic.NewMatchQuery("language", language)
+	langQuery := elastic.NewMatchQuery("language", opts.Language)
 	countResult, err := b.inner.Client.Search().
 		Index(b.inner.VersionedIndexName()).
 		Aggregation("language", aggregation).
diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go
index f105d032eb..2905a540e5 100644
--- a/modules/indexer/code/git.go
+++ b/modules/indexer/code/git.go
@@ -32,7 +32,7 @@ func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision s
 
 	needGenesis := len(status.CommitSha) == 0
 	if !needGenesis {
-		hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision)
+		hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(status.CommitSha, revision)
 		stdout, _, _ := hasAncestorCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
 		needGenesis = len(stdout) == 0
 	}
diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go
index 23dbd63410..8975c5ce40 100644
--- a/modules/indexer/code/indexer_test.go
+++ b/modules/indexer/code/indexer_test.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/indexer/code/bleve"
@@ -70,7 +71,15 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
 
 		for _, kw := range keywords {
 			t.Run(kw.Keyword, func(t *testing.T) {
-				total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, true)
+				total, res, langs, err := indexer.Search(context.TODO(), &internal.SearchOptions{
+					RepoIDs: kw.RepoIDs,
+					Keyword: kw.Keyword,
+					Paginator: &db.ListOptions{
+						Page:     1,
+						PageSize: 10,
+					},
+					IsKeywordFuzzy: true,
+				})
 				assert.NoError(t, err)
 				assert.Len(t, kw.IDs, int(total))
 				assert.Len(t, langs, kw.Langs)
diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go
index c92419deb2..c259fcd26e 100644
--- a/modules/indexer/code/internal/indexer.go
+++ b/modules/indexer/code/internal/indexer.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"fmt"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/indexer/internal"
 )
@@ -16,7 +17,17 @@ type Indexer interface {
 	internal.Indexer
 	Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error
 	Delete(ctx context.Context, repoID int64) error
-	Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error)
+	Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error)
+}
+
+type SearchOptions struct {
+	RepoIDs  []int64
+	Keyword  string
+	Language string
+
+	IsKeywordFuzzy bool
+
+	db.Paginator
 }
 
 // NewDummyIndexer returns a dummy indexer
@@ -38,6 +49,6 @@ func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error {
 	return fmt.Errorf("indexer is not ready")
 }
 
-func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error) {
+func (d *dummyIndexer) Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error) {
 	return 0, nil, nil, fmt.Errorf("indexer is not ready")
 }
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 89a62a8d3e..51c7595cf8 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -32,6 +32,8 @@ type ResultLine struct {
 
 type SearchResultLanguages = internal.SearchResultLanguages
 
+type SearchOptions = internal.SearchOptions
+
 func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) {
 	startIndex := selectionStartIndex
 	numLinesBefore := 0
@@ -125,12 +127,12 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 
 // PerformSearch perform a search on a repository
 // if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2
-func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int, []*Result, []*internal.SearchResultLanguages, error) {
-	if len(keyword) == 0 {
+func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) {
+	if opts == nil || len(opts.Keyword) == 0 {
 		return 0, nil, nil, nil
 	}
 
-	total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isFuzzy)
+	total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, opts)
 	if err != nil {
 		return 0, nil, nil, err
 	}
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index bfc798a97d..c90174f9c2 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -6,6 +6,7 @@ package explore
 import (
 	"net/http"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
@@ -76,7 +77,16 @@ func Code(ctx *context.Context) {
 	)
 
 	if (len(repoIDs) > 0) || isAdmin {
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+			RepoIDs:        repoIDs,
+			Keyword:        keyword,
+			IsKeywordFuzzy: isFuzzy,
+			Language:       language,
+			Paginator: &db.ListOptions{
+				Page:     page,
+				PageSize: setting.UI.RepoSearchPagingNum,
+			},
+		})
 		if err != nil {
 			if code_indexer.IsAvailable(ctx) {
 				ctx.ServerError("SearchResults", err)
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 73537e64bc..cf8666bea5 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -6,6 +6,7 @@ package repo
 import (
 	"net/http"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
@@ -41,8 +42,16 @@ func Search(ctx *context.Context) {
 		page = 1
 	}
 
-	total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
-		language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
+	total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+		RepoIDs:        []int64{ctx.Repo.Repository.ID},
+		Keyword:        keyword,
+		IsKeywordFuzzy: isFuzzy,
+		Language:       language,
+		Paginator: &db.ListOptions{
+			Page:     page,
+			PageSize: setting.UI.RepoSearchPagingNum,
+		},
+	})
 	if err != nil {
 		if code_indexer.IsAvailable(ctx) {
 			ctx.ServerError("SearchResults", err)
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index a8ac50639d..7ce3e12192 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -6,6 +6,7 @@ package user
 import (
 	"net/http"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
@@ -74,7 +75,16 @@ func CodeSearch(ctx *context.Context) {
 	)
 
 	if len(repoIDs) > 0 {
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+			RepoIDs:        repoIDs,
+			Keyword:        keyword,
+			IsKeywordFuzzy: isFuzzy,
+			Language:       language,
+			Paginator: &db.ListOptions{
+				Page:     page,
+				PageSize: setting.UI.RepoSearchPagingNum,
+			},
+		})
 		if err != nil {
 			if code_indexer.IsAvailable(ctx) {
 				ctx.ServerError("SearchResults", err)

From 66902d89e567ab1ae6dfb828636999c61ff0149e Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 16 Mar 2024 19:34:38 +0800
Subject: [PATCH 399/679] Refactor markdown attention render (#29833)

* Remove some deadcode
* Use 2-word name for CSS class names
* Remove "gt-*" rules for sanitizer

The UI doesn't change much.
---
 modules/markup/markdown/ast.go      |  7 -------
 modules/markup/markdown/goldmark.go | 24 +++++++++---------------
 modules/markup/sanitizer.go         | 13 +++----------
 web_src/css/base.css                | 17 +++++++++++------
 4 files changed, 23 insertions(+), 38 deletions(-)

diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go
index 77ce5cb359..624c35d945 100644
--- a/modules/markup/markdown/ast.go
+++ b/modules/markup/markdown/ast.go
@@ -175,13 +175,6 @@ func NewColorPreview(color []byte) *ColorPreview {
 	}
 }
 
-// IsColorPreview returns true if the given node implements the ColorPreview interface,
-// otherwise false.
-func IsColorPreview(node ast.Node) bool {
-	_, ok := node.(*ColorPreview)
-	return ok
-}
-
 // Attention is an inline for an attention
 type Attention struct {
 	ast.BaseInline
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 67817ce27b..bdb7748247 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -216,7 +216,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 			attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNode.Segment.Value(reader.Source())), "!"))
 
 			// color the blockquote
-			v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType))
+			v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
 
 			// create an emphasis to make it bold
 			attentionParagraph := ast.NewParagraph()
@@ -364,27 +364,21 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
 // renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
 func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	if entering {
-		_, _ = w.WriteString(`<span class="gt-mr-2 gt-vm attention-`)
 		n := node.(*Attention)
-		_, _ = w.WriteString(strings.ToLower(n.AttentionType))
-		_, _ = w.WriteString(`">`)
-
-		var octiconType string
+		var octiconName string
 		switch n.AttentionType {
-		case "note":
-			octiconType = "info"
 		case "tip":
-			octiconType = "light-bulb"
+			octiconName = "light-bulb"
 		case "important":
-			octiconType = "report"
+			octiconName = "report"
 		case "warning":
-			octiconType = "alert"
+			octiconName = "alert"
 		case "caution":
-			octiconType = "stop"
+			octiconName = "stop"
+		default: // including "note"
+			octiconName = "info"
 		}
-		_, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType)))
-	} else {
-		_, _ = w.WriteString("</span>\n")
+		_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
 	}
 	return ast.WalkContinue, nil
 }
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index ffc33c3b8e..79a2ba0dfb 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -64,10 +64,9 @@ func createDefaultPolicy() *bluemonday.Policy {
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
 
 	// For attention
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-py-3 attention attention-\w+$`)).OnElements("blockquote")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-mr-2 gt-vm attention-\w+$`)).OnElements("span", "strong")
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-(\w|-)+$`)).OnElements("svg")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
 	policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
 	policy.AllowAttrs("fill-rule", "d").OnElements("path")
 
@@ -105,18 +104,12 @@ func createDefaultPolicy() *bluemonday.Policy {
 	// Allow icons
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
 
-	// Allow unlabelled labels
-	policy.AllowNoAttrs().OnElements("label")
-
 	// Allow classes for emojis
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
 
 	// Allow icons, emojis, chroma syntax and keyword markup on span
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
 
-	// Allow 'style' attribute on text elements.
-	policy.AllowAttrs("style").OnElements("span", "p")
-
 	// Allow 'color' and 'background-color' properties for the style attribute on text elements.
 	policy.AllowStyles("color", "background-color").OnElements("span", "p")
 
@@ -144,7 +137,7 @@ func createDefaultPolicy() *bluemonday.Policy {
 
 	generalSafeElements := []string{
 		"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
-		"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote",
+		"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
 		"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
 		"details", "caption", "figure", "figcaption",
 		"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 2c055b7439..d995f51038 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1279,42 +1279,47 @@ input:-webkit-autofill:active,
   border-radius: var(--border-radius);
 }
 
-.attention {
+.attention-header {
+  padding: 0.5em 0.75em !important;
   color: var(--color-text) !important;
 }
 
+.attention-icon {
+  margin: 2px 6px 0 0;
+}
+
 blockquote.attention-note {
   border-left-color: var(--color-blue-dark-1);
 }
-strong.attention-note, span.attention-note {
+strong.attention-note, svg.attention-note {
   color: var(--color-blue-dark-1);
 }
 
 blockquote.attention-tip {
   border-left-color: var(--color-success-text);
 }
-strong.attention-tip, span.attention-tip {
+strong.attention-tip, svg.attention-tip {
   color: var(--color-success-text);
 }
 
 blockquote.attention-important {
   border-left-color: var(--color-violet-dark-1);
 }
-strong.attention-important, span.attention-important {
+strong.attention-important, svg.attention-important {
   color: var(--color-violet-dark-1);
 }
 
 blockquote.attention-warning {
   border-left-color: var(--color-warning-text);
 }
-strong.attention-warning, span.attention-warning {
+strong.attention-warning, svg.attention-warning {
   color: var(--color-warning-text);
 }
 
 blockquote.attention-caution {
   border-left-color: var(--color-red-dark-1);
 }
-strong.attention-caution, span.attention-caution {
+strong.attention-caution, svg.attention-caution {
   color: var(--color-red-dark-1);
 }
 

From a8893816647140526055acc1c4cfe2d130ce7c47 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 16 Mar 2024 20:07:56 +0800
Subject: [PATCH 400/679] Remove AddParamIfExist(AddParam) (#29841)

Follow #29834

Remove AddParamIfExist, use "AddParamString" instead, it should clearly
know what is being added into the parameters.
---
 routers/web/admin/repos.go       |  5 +++--
 routers/web/explore/code.go      |  2 +-
 routers/web/explore/repo.go      |  4 ++--
 routers/web/org/home.go          |  2 +-
 routers/web/org/projects.go      |  2 +-
 routers/web/repo/commit.go       |  4 ++--
 routers/web/repo/issue.go        | 20 ++++++++++----------
 routers/web/repo/milestone.go    |  4 ++--
 routers/web/repo/packages.go     |  4 ++--
 routers/web/repo/projects.go     |  2 +-
 routers/web/repo/search.go       |  2 +-
 routers/web/user/code.go         |  2 +-
 routers/web/user/home.go         | 22 ++++++++++------------
 routers/web/user/notification.go |  4 ++--
 routers/web/user/package.go      |  4 ++--
 routers/web/user/profile.go      |  8 +++++---
 services/context/pagination.go   | 22 +++++++++-------------
 17 files changed, 55 insertions(+), 58 deletions(-)

diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index 5504037df0..0815879bb3 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -4,6 +4,7 @@
 package admin
 
 import (
+	"fmt"
 	"net/http"
 	"net/url"
 	"strings"
@@ -84,7 +85,7 @@ func UnadoptedRepos(ctx *context.Context) {
 	if !doSearch {
 		pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
 		pager.SetDefaultParams(ctx)
-		pager.AddParamIfExist("search", ctx.Data["search"])
+		pager.AddParamString("search", fmt.Sprint(doSearch))
 		ctx.Data["Page"] = pager
 		ctx.HTML(http.StatusOK, tplUnadoptedRepos)
 		return
@@ -98,7 +99,7 @@ func UnadoptedRepos(ctx *context.Context) {
 	ctx.Data["Dirs"] = repoNames
 	pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamIfExist("search", ctx.Data["search"])
+	pager.AddParamString("search", fmt.Sprint(doSearch))
 	ctx.Data["Page"] = pager
 	ctx.HTML(http.StatusOK, tplUnadoptedRepos)
 }
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index c90174f9c2..ecd7c33e01 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -137,7 +137,7 @@ func Code(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamIfExist("l", ctx.Data["Language"])
+	pager.AddParamString("l", language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplExploreCode)
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index 2cc22c50cf..66477a255c 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -169,8 +169,8 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 
 	pager := context.NewPagination(int(count), opts.PageSize, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamIfExist("topic", ctx.Data["TopicOnly"])
-	pager.AddParamIfExist("language", ctx.Data["Language"])
+	pager.AddParamString("topic", fmt.Sprint(topicOnly))
+	pager.AddParamString("language", language)
 	pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant))
 	ctx.Data["Page"] = pager
 
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 947721dc41..846b1de18a 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -154,7 +154,7 @@ func Home(ctx *context.Context) {
 
 	pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamIfExist("language", ctx.Data["Language"])
+	pager.AddParamString("language", language)
 	ctx.Data["Page"] = pager
 
 	ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 094d14d194..928676a52b 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -120,7 +120,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
-	pager.AddParamIfExist("state", ctx.Data["State"])
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
 	ctx.Data["Page"] = pager
 
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 21fafd4901..d66de782f4 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -163,8 +163,8 @@ func Graph(ctx *context.Context) {
 	ctx.Data["CommitCount"] = commitsCount
 
 	paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
-	paginator.AddParamIfExist("mode", ctx.Data["Mode"])
-	paginator.AddParamIfExist("hide-pr-refs", ctx.Data["HidePRRefs"])
+	paginator.AddParamString("mode", mode)
+	paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs))
 	for _, branch := range branches {
 		paginator.AddParamString("branch", branch)
 	}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 69e09371b3..a0a500f0b2 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -472,16 +472,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
 	}
 	ctx.Data["ShowArchivedLabels"] = archived
 
-	pager.AddParamIfExist("q", ctx.Data["Keyword"])
-	pager.AddParamIfExist("type", ctx.Data["ViewType"])
-	pager.AddParamIfExist("sort", ctx.Data["SortType"])
-	pager.AddParamIfExist("state", ctx.Data["State"])
-	pager.AddParamIfExist("labels", ctx.Data["SelectLabels"])
-	pager.AddParamIfExist("milestone", ctx.Data["MilestoneID"])
-	pager.AddParamIfExist("project", ctx.Data["ProjectID"])
-	pager.AddParamIfExist("assignee", ctx.Data["AssigneeID"])
-	pager.AddParamIfExist("poster", ctx.Data["PosterID"])
-	pager.AddParamIfExist("archived", ctx.Data["ShowArchivedLabels"])
+	pager.AddParamString("q", keyword)
+	pager.AddParamString("type", viewType)
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+	pager.AddParamString("labels", fmt.Sprint(selectLabels))
+	pager.AddParamString("milestone", fmt.Sprint(milestoneID))
+	pager.AddParamString("project", fmt.Sprint(projectID))
+	pager.AddParamString("assignee", fmt.Sprint(assigneeID))
+	pager.AddParamString("poster", fmt.Sprint(posterID))
+	pager.AddParamString("archived", fmt.Sprint(archived))
 
 	ctx.Data["Page"] = pager
 }
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 2f7c2cb187..95a4fe60cc 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -106,8 +106,8 @@ func Milestones(ctx *context.Context) {
 	ctx.Data["IsShowClosed"] = isShowClosed
 
 	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
-	pager.AddParamIfExist("state", ctx.Data["State"])
-	pager.AddParamIfExist("q", ctx.Data["Keyword"])
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+	pager.AddParamString("q", keyword)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplMilestone)
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index 41fc38dedb..57e578da37 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -70,8 +70,8 @@ func Packages(ctx *context.Context) {
 	ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
-	pager.AddParamIfExist("q", ctx.Data["Query"])
-	pager.AddParamIfExist("type", ctx.Data["PackageType"])
+	pager.AddParamString("q", query)
+	pager.AddParamString("type", packageType)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplPackagesList)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 39f1b62ff7..2cba5c0970 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -118,7 +118,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
-	pager.AddParamIfExist("state", ctx.Data["State"])
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
 	ctx.Data["Page"] = pager
 
 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index cf8666bea5..0f377a97bb 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -68,7 +68,7 @@ func Search(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamIfExist("l", ctx.Data["Language"])
+	pager.AddParamString("l", language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplSearch)
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index 7ce3e12192..785c37b124 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -122,7 +122,7 @@ func CodeSearch(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamIfExist("l", ctx.Data["Language"])
+	pager.AddParamString("l", language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplUserCode)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index dddd03e21f..465de500a0 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -133,7 +133,7 @@ func Dashboard(ctx *context.Context) {
 	ctx.Data["Feeds"] = feeds
 
 	pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
-	pager.AddParamIfExist("date", ctx.Data["Date"])
+	pager.AddParamString("date", date)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplDashboard)
@@ -329,10 +329,10 @@ func Milestones(ctx *context.Context) {
 	ctx.Data["IsShowClosed"] = isShowClosed
 
 	pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
-	pager.AddParamIfExist("q", ctx.Data["Keyword"])
-	pager.AddParamIfExist("repos", ctx.Data["RepoIDs"])
-	pager.AddParamIfExist("sort", ctx.Data["SortType"])
-	pager.AddParamIfExist("state", ctx.Data["State"])
+	pager.AddParamString("q", keyword)
+	pager.AddParamString("repos", reposQuery)
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplMilestones)
@@ -632,13 +632,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	}
 
 	pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
-	pager.AddParamIfExist("q", ctx.Data["Keyword"])
-	pager.AddParamIfExist("type", ctx.Data["ViewType"])
-	pager.AddParamIfExist("sort", ctx.Data["SortType"])
-	pager.AddParamIfExist("state", ctx.Data["State"])
-	pager.AddParamIfExist("labels", ctx.Data["SelectLabels"])
-	pager.AddParamIfExist("milestone", ctx.Data["MilestoneID"])
-	pager.AddParamIfExist("assignee", ctx.Data["AssigneeID"])
+	pager.AddParamString("q", keyword)
+	pager.AddParamString("type", viewType)
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+	pager.AddParamString("labels", selectedLabels)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplIssues)
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 81afeae043..438462371b 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -344,8 +344,8 @@ func NotificationSubscriptions(ctx *context.Context) {
 		ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
 		return
 	}
-	pager.AddParamIfExist("sort", ctx.Data["SortType"])
-	pager.AddParamIfExist("state", ctx.Data["State"])
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", state)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 911ca12bf0..9af49406c4 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -125,8 +125,8 @@ func ListPackages(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
-	pager.AddParamIfExist("q", ctx.Data["Query"])
-	pager.AddParamIfExist("type", ctx.Data["PackageType"])
+	pager.AddParamString("q", query)
+	pager.AddParamString("type", packageType)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplPackagesList)
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index f9df511f60..f0749e1021 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -324,12 +324,14 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 
 	pager := context.NewPagination(total, pagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamIfExist("tab", ctx.Data["TabName"])
+	pager.AddParamString("tab", tab)
 	if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
-		pager.AddParamIfExist("language", ctx.Data["Language"])
+		pager.AddParamString("language", language)
 	}
 	if tab == "activity" {
-		pager.AddParamIfExist("date", ctx.Data["Date"])
+		if ctx.Data["Date"] != nil {
+			pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"]))
+		}
 	}
 	ctx.Data["Page"] = pager
 }
diff --git a/services/context/pagination.go b/services/context/pagination.go
index 11d37283c9..fb2ef699ce 100644
--- a/services/context/pagination.go
+++ b/services/context/pagination.go
@@ -26,16 +26,6 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination {
 	return p
 }
 
-// AddParamIfExist adds a value to the query parameters if the value is not nil
-func (p *Pagination) AddParamIfExist(key string, val any) {
-	if val == nil {
-		return
-	}
-	paramData := fmt.Sprint(val)
-	urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(paramData))
-	p.urlParams = append(p.urlParams, urlParam)
-}
-
 // AddParamString adds a string parameter directly
 func (p *Pagination) AddParamString(key, value string) {
 	urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value))
@@ -49,8 +39,14 @@ func (p *Pagination) GetParams() template.URL {
 
 // SetDefaultParams sets common pagination params that are often used
 func (p *Pagination) SetDefaultParams(ctx *Context) {
-	p.AddParamIfExist("sort", ctx.Data["SortType"])
-	p.AddParamIfExist("q", ctx.Data["Keyword"])
+	if v, ok := ctx.Data["SortType"].(string); ok {
+		p.AddParamString("sort", v)
+	}
+	if v, ok := ctx.Data["Keyword"].(string); ok {
+		p.AddParamString("q", v)
+	}
+	if v, ok := ctx.Data["IsFuzzy"].(bool); ok {
+		p.AddParamString("fuzzy", fmt.Sprint(v))
+	}
 	// do not add any more uncommon params here!
-	p.AddParamIfExist("fuzzy", ctx.Data["IsFuzzy"])
 }

From 3cd64949ae1402a4ff45fba0a27c4acca1c5aead Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 16 Mar 2024 14:22:16 +0200
Subject: [PATCH 401/679] Forbid variables containing jQuery collections not
 having the `$` prefix (#29839)

See
https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/variable-pattern.md

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 .eslintrc.yaml                                |  2 +-
 .../js/components/RepoBranchTagSelector.vue   |  2 +-
 web_src/js/features/common-global.js          | 16 ++--
 web_src/js/features/comp/LabelEdit.js         | 42 ++++-----
 web_src/js/features/comp/ReactionSelector.js  | 22 ++---
 web_src/js/features/notification.js           | 20 ++--
 web_src/js/features/repo-diff.js              | 12 +--
 web_src/js/features/repo-editor.js            | 20 ++--
 web_src/js/features/repo-graph.js             |  8 +-
 web_src/js/features/repo-home.js              | 72 +++++++--------
 web_src/js/features/repo-issue.js             | 92 +++++++++----------
 web_src/js/features/repo-legacy.js            | 22 ++---
 web_src/js/features/repo-projects.js          | 52 +++++------
 web_src/js/jquery.js                          |  2 +-
 14 files changed, 192 insertions(+), 192 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index f84cb39a41..5f291f13e7 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -487,7 +487,7 @@ rules:
   no-jquery/no-visibility: [2]
   no-jquery/no-when: [2]
   no-jquery/no-wrap: [2]
-  no-jquery/variable-pattern: [0]
+  no-jquery/variable-pattern: [2]
   no-label-var: [2]
   no-labels: [0] # handled by no-restricted-syntax
   no-lone-blocks: [2]
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index 70ce9bd6a0..83289c8852 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -123,7 +123,7 @@ const sfc = {
       return -1;
     },
     scrollToActive() {
-      let el = this.$refs[`listItem${this.active}`];
+      let el = this.$refs[`listItem${this.active}`]; // eslint-disable-line no-jquery/variable-pattern
       if (!el || !el.length) return;
       if (Array.isArray(el)) {
         el = el[0];
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index ee4ade1f04..d99f606c8a 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -231,8 +231,8 @@ export function initDropzone(el) {
     init() {
       this.on('success', (file, data) => {
         file.uuid = data.uuid;
-        const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
-        $dropzone.find('.files').append(input);
+        const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
+        $dropzone.find('.files').append($input);
         // Create a "Copy Link" element, to conveniently copy the image
         // or file link as Markdown to the clipboard
         const copyLinkElement = document.createElement('div');
@@ -305,15 +305,15 @@ export function initGlobalLinkActions() {
       filter += `#${$this.attr('data-modal-id')}`;
     }
 
-    const dialog = $(`.delete.modal${filter}`);
-    dialog.find('.name').text($this.data('name'));
+    const $dialog = $(`.delete.modal${filter}`);
+    $dialog.find('.name').text($this.data('name'));
     for (const [key, value] of Object.entries(dataArray)) {
       if (key && key.startsWith('data')) {
-        dialog.find(`.${key}`).text(value);
+        $dialog.find(`.${key}`).text(value);
       }
     }
 
-    dialog.modal({
+    $dialog.modal({
       closable: false,
       onApprove: async () => {
         if ($this.data('type') === 'form') {
@@ -380,8 +380,8 @@ function initGlobalShowModal() {
         $attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
       }
     }
-    const colorPickers = $modal.find('.color-picker');
-    if (colorPickers.length > 0) {
+    const $colorPickers = $modal.find('.color-picker');
+    if ($colorPickers.length > 0) {
       initCompColorPicker(); // FIXME: this might cause duplicate init
     }
     $modal.modal('setting', {
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index 2a22190e10..2e7e1df669 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -6,23 +6,23 @@ function isExclusiveScopeName(name) {
 }
 
 function updateExclusiveLabelEdit(form) {
-  const nameInput = $(`${form} .label-name-input`);
-  const exclusiveField = $(`${form} .label-exclusive-input-field`);
-  const exclusiveCheckbox = $(`${form} .label-exclusive-input`);
-  const exclusiveWarning = $(`${form} .label-exclusive-warning`);
+  const $nameInput = $(`${form} .label-name-input`);
+  const $exclusiveField = $(`${form} .label-exclusive-input-field`);
+  const $exclusiveCheckbox = $(`${form} .label-exclusive-input`);
+  const $exclusiveWarning = $(`${form} .label-exclusive-warning`);
 
-  if (isExclusiveScopeName(nameInput.val())) {
-    exclusiveField.removeClass('muted');
-    exclusiveField.removeAttr('aria-disabled');
-    if (exclusiveCheckbox.prop('checked') && exclusiveCheckbox.data('exclusive-warn')) {
-      exclusiveWarning.removeClass('gt-hidden');
+  if (isExclusiveScopeName($nameInput.val())) {
+    $exclusiveField.removeClass('muted');
+    $exclusiveField.removeAttr('aria-disabled');
+    if ($exclusiveCheckbox.prop('checked') && $exclusiveCheckbox.data('exclusive-warn')) {
+      $exclusiveWarning.removeClass('gt-hidden');
     } else {
-      exclusiveWarning.addClass('gt-hidden');
+      $exclusiveWarning.addClass('gt-hidden');
     }
   } else {
-    exclusiveField.addClass('muted');
-    exclusiveField.attr('aria-disabled', 'true');
-    exclusiveWarning.addClass('gt-hidden');
+    $exclusiveField.addClass('muted');
+    $exclusiveField.attr('aria-disabled', 'true');
+    $exclusiveWarning.addClass('gt-hidden');
   }
 }
 
@@ -46,18 +46,18 @@ export function initCompLabelEdit(selector) {
     $('.edit-label .color-picker').minicolors('value', $(this).data('color'));
     $('#label-modal-id').val($(this).data('id'));
 
-    const nameInput = $('.edit-label .label-name-input');
-    nameInput.val($(this).data('title'));
+    const $nameInput = $('.edit-label .label-name-input');
+    $nameInput.val($(this).data('title'));
 
-    const isArchivedCheckbox = $('.edit-label .label-is-archived-input');
-    isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived'));
+    const $isArchivedCheckbox = $('.edit-label .label-is-archived-input');
+    $isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived'));
 
-    const exclusiveCheckbox = $('.edit-label .label-exclusive-input');
-    exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive'));
+    const $exclusiveCheckbox = $('.edit-label .label-exclusive-input');
+    $exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive'));
     // Warn when label was previously not exclusive and used in issues
-    exclusiveCheckbox.data('exclusive-warn',
+    $exclusiveCheckbox.data('exclusive-warn',
       $(this).data('num-issues') > 0 &&
-      (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName(nameInput.val())));
+      (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val())));
     updateExclusiveLabelEdit('.edit-label');
 
     $('.edit-label .label-desc-input').val($(this).data('description'));
diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js
index 76834f8844..6df4bde069 100644
--- a/web_src/js/features/comp/ReactionSelector.js
+++ b/web_src/js/features/comp/ReactionSelector.js
@@ -17,21 +17,21 @@ export function initCompReactionSelector($parent) {
 
     const data = await res.json();
     if (data && (data.html || data.empty)) {
-      const content = $(this).closest('.content');
-      let react = content.find('.segment.reactions');
-      if ((!data.empty || data.html === '') && react.length > 0) {
-        react.remove();
+      const $content = $(this).closest('.content');
+      let $react = $content.find('.segment.reactions');
+      if ((!data.empty || data.html === '') && $react.length > 0) {
+        $react.remove();
       }
       if (!data.empty) {
-        const attachments = content.find('.segment.bottom:first');
-        react = $(data.html);
-        if (attachments.length > 0) {
-          react.insertBefore(attachments);
+        const $attachments = $content.find('.segment.bottom:first');
+        $react = $(data.html);
+        if ($attachments.length > 0) {
+          $react.insertBefore($attachments);
         } else {
-          react.appendTo(content);
+          $react.appendTo($content);
         }
-        react.find('.dropdown').dropdown();
-        initCompReactionSelector(react);
+        $react.find('.dropdown').dropdown();
+        initCompReactionSelector($react);
       }
     }
   });
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 3d34f21fd4..57b7bcb6e8 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -45,9 +45,9 @@ async function receiveUpdateCount(event) {
 }
 
 export function initNotificationCount() {
-  const notificationCount = $('.notification_count');
+  const $notificationCount = $('.notification_count');
 
-  if (!notificationCount.length) {
+  if (!$notificationCount.length) {
     return;
   }
 
@@ -55,7 +55,7 @@ export function initNotificationCount() {
   const startPeriodicPoller = (timeout, lastCount) => {
     if (timeout <= 0 || !Number.isFinite(timeout)) return;
     usingPeriodicPoller = true;
-    lastCount = lastCount ?? notificationCount.text();
+    lastCount = lastCount ?? $notificationCount.text();
     setTimeout(async () => {
       await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
     }, timeout);
@@ -143,8 +143,8 @@ async function updateNotificationCountWithCallback(callback, timeout, lastCount)
 }
 
 async function updateNotificationTable() {
-  const notificationDiv = $('#notification_div');
-  if (notificationDiv.length > 0) {
+  const $notificationDiv = $('#notification_div');
+  if ($notificationDiv.length > 0) {
     try {
       const params = new URLSearchParams(window.location.search);
       params.set('div-only', true);
@@ -158,7 +158,7 @@ async function updateNotificationTable() {
 
       const data = await response.text();
       if ($(data).data('sequence-number') === notificationSequenceNumber) {
-        notificationDiv.replaceWith(data);
+        $notificationDiv.replaceWith(data);
         initNotificationsTable();
       }
     } catch (error) {
@@ -177,14 +177,14 @@ async function updateNotificationCount() {
 
     const data = await response.json();
 
-    const notificationCount = $('.notification_count');
+    const $notificationCount = $('.notification_count');
     if (data.new === 0) {
-      notificationCount.addClass('gt-hidden');
+      $notificationCount.addClass('gt-hidden');
     } else {
-      notificationCount.removeClass('gt-hidden');
+      $notificationCount.removeClass('gt-hidden');
     }
 
-    notificationCount.text(`${data.new}`);
+    $notificationCount.text(`${data.new}`);
 
     return `${data.new}`;
   } catch (error) {
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index b341583c3e..20a6577932 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -97,10 +97,10 @@ function initRepoDiffConversationForm() {
       const data = await response.text();
 
       if ($(this).closest('.conversation-holder').length) {
-        const conversation = $(data);
-        $(this).closest('.conversation-holder').replaceWith(conversation);
-        conversation.find('.dropdown').dropdown();
-        initCompReactionSelector(conversation);
+        const $conversation = $(data);
+        $(this).closest('.conversation-holder').replaceWith($conversation);
+        $conversation.find('.dropdown').dropdown();
+        initCompReactionSelector($conversation);
       } else {
         window.location.reload();
       }
@@ -209,8 +209,8 @@ function initRepoDiffShowMore() {
 
 export function initRepoDiffView() {
   initRepoDiffConversationForm();
-  const diffFileList = $('#diff-file-list');
-  if (diffFileList.length === 0) return;
+  const $diffFileList = $('#diff-file-list');
+  if ($diffFileList.length === 0) return;
   initDiffFileTree();
   initDiffCommitSelect();
   initRepoDiffShowMore();
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index 4fe7ed8a4d..fea98e2df8 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -15,9 +15,9 @@ function initEditPreviewTab($form) {
       const $this = $(this);
       let context = `${$this.data('context')}/`;
       const mode = $this.data('markup-mode') || 'comment';
-      const treePathEl = $form.find('input#tree_path');
-      if (treePathEl.length > 0) {
-        context += treePathEl.val();
+      const $treePathEl = $form.find('input#tree_path');
+      if ($treePathEl.length > 0) {
+        context += $treePathEl.val();
       }
       context = context.substring(0, context.lastIndexOf('/'));
 
@@ -25,7 +25,7 @@ function initEditPreviewTab($form) {
       formData.append('mode', mode);
       formData.append('context', context);
       formData.append('text', $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val());
-      formData.append('file_path', treePathEl.val());
+      formData.append('file_path', $treePathEl.val());
       try {
         const response = await POST($this.data('url'), {data: formData});
         const data = await response.text();
@@ -78,11 +78,11 @@ export function initRepoEditor() {
   const joinTreePath = ($fileNameEl) => {
     const parts = [];
     $('.breadcrumb span.section').each(function () {
-      const element = $(this);
-      if (element.find('a').length) {
-        parts.push(element.find('a').text());
+      const $element = $(this);
+      if ($element.find('a').length) {
+        parts.push($element.find('a').text());
       } else {
-        parts.push(element.text());
+        parts.push($element.text());
       }
     });
     if ($fileNameEl.val()) parts.push($fileNameEl.val());
@@ -181,6 +181,6 @@ export function renderPreviewPanelContent($panelPreviewer, data) {
   $panelPreviewer.html(data);
   initMarkupContent();
 
-  const refIssues = $panelPreviewer.find('p .ref-issue');
-  attachRefIssueContextPopup(refIssues);
+  const $refIssues = $panelPreviewer.find('p .ref-issue');
+  attachRefIssueContextPopup($refIssues);
 }
diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
index c83f448b76..4fbf95b9d5 100644
--- a/web_src/js/features/repo-graph.js
+++ b/web_src/js/features/repo-graph.js
@@ -63,10 +63,10 @@ export function initRepoGraphGit() {
     (async () => {
       const response = await GET(String(ajaxUrl));
       const html = await response.text();
-      const div = $(html);
-      $('#pagination').html(div.find('#pagination').html());
-      $('#rel-container').html(div.find('#rel-container').html());
-      $('#rev-container').html(div.find('#rev-container').html());
+      const $div = $(html);
+      $('#pagination').html($div.find('#pagination').html());
+      $('#rel-container').html($div.find('#rel-container').html());
+      $('#rev-container').html($div.find('#rev-container').html());
       $('#loading-indicator').addClass('gt-hidden');
       $('#rel-container').removeClass('gt-hidden');
       $('#rev-container').removeClass('gt-hidden');
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index bbba7b103e..50f324d788 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -6,55 +6,55 @@ import {POST} from '../modules/fetch.js';
 const {appSubUrl} = window.config;
 
 export function initRepoTopicBar() {
-  const mgrBtn = $('#manage_topic');
-  if (!mgrBtn.length) return;
-  const editDiv = $('#topic_edit');
-  const viewDiv = $('#repo-topics');
-  const saveBtn = $('#save_topic');
-  const topicDropdown = $('#topic_edit .dropdown');
-  const topicForm = editDiv; // the old logic, editDiv is topicForm
-  const topicDropdownSearch = topicDropdown.find('input.search');
+  const $mgrBtn = $('#manage_topic');
+  if (!$mgrBtn.length) return;
+  const $editDiv = $('#topic_edit');
+  const $viewDiv = $('#repo-topics');
+  const $saveBtn = $('#save_topic');
+  const $topicDropdown = $('#topic_edit .dropdown');
+  const $topicForm = $editDiv; // the old logic, $editDiv is topicForm
+  const $topicDropdownSearch = $topicDropdown.find('input.search');
   const topicPrompts = {
-    countPrompt: topicDropdown.attr('data-text-count-prompt'),
-    formatPrompt: topicDropdown.attr('data-text-format-prompt'),
+    countPrompt: $topicDropdown.attr('data-text-count-prompt'),
+    formatPrompt: $topicDropdown.attr('data-text-format-prompt'),
   };
 
-  mgrBtn.on('click', () => {
-    hideElem(viewDiv);
-    showElem(editDiv);
-    topicDropdownSearch.focus();
+  $mgrBtn.on('click', () => {
+    hideElem($viewDiv);
+    showElem($editDiv);
+    $topicDropdownSearch.trigger('focus');
   });
 
   $('#cancel_topic_edit').on('click', () => {
-    hideElem(editDiv);
-    showElem(viewDiv);
-    mgrBtn.focus();
+    hideElem($editDiv);
+    showElem($viewDiv);
+    $mgrBtn.trigger('focus');
   });
 
-  saveBtn.on('click', async () => {
+  $saveBtn.on('click', async () => {
     const topics = $('input[name=topics]').val();
 
     const data = new FormData();
     data.append('topics', topics);
 
-    const response = await POST(saveBtn.attr('data-link'), {data});
+    const response = await POST($saveBtn.attr('data-link'), {data});
 
     if (response.ok) {
       const responseData = await response.json();
       if (responseData.status === 'ok') {
-        viewDiv.children('.topic').remove();
+        $viewDiv.children('.topic').remove();
         if (topics.length) {
           const topicArray = topics.split(',');
           topicArray.sort();
           for (const topic of topicArray) {
-            const link = $('<a class="ui repo-topic large label topic gt-m-0"></a>');
-            link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`);
-            link.text(topic);
-            link.insertBefore(mgrBtn); // insert all new topics before manage button
+            const $link = $('<a class="ui repo-topic large label topic gt-m-0"></a>');
+            $link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`);
+            $link.text(topic);
+            $link.insertBefore($mgrBtn); // insert all new topics before manage button
           }
         }
-        hideElem(editDiv);
-        showElem(viewDiv);
+        hideElem($editDiv);
+        showElem($viewDiv);
       }
     } else if (response.status === 422) {
       const responseData = await response.json();
@@ -62,10 +62,10 @@ export function initRepoTopicBar() {
         topicPrompts.formatPrompt = responseData.message;
 
         const {invalidTopics} = responseData;
-        const topicLabels = topicDropdown.children('a.ui.label');
+        const $topicLabels = $topicDropdown.children('a.ui.label');
         for (const [index, value] of topics.split(',').entries()) {
           if (invalidTopics.includes(value)) {
-            topicLabels.eq(index).removeClass('green').addClass('red');
+            $topicLabels.eq(index).removeClass('green').addClass('red');
           }
         }
       } else {
@@ -74,10 +74,10 @@ export function initRepoTopicBar() {
     }
 
     // Always validate the form
-    topicForm.form('validate form');
+    $topicForm.form('validate form');
   });
 
-  topicDropdown.dropdown({
+  $topicDropdown.dropdown({
     allowAdditions: true,
     forceSelection: false,
     fullTextSearch: 'exact',
@@ -100,7 +100,7 @@ export function initRepoTopicBar() {
         const query = stripTags(this.urlData.query.trim());
         let found_query = false;
         const current_topics = [];
-        topicDropdown.find('a.label.visible').each((_, el) => {
+        $topicDropdown.find('a.label.visible').each((_, el) => {
           current_topics.push(el.getAttribute('data-value'));
         });
 
@@ -150,15 +150,15 @@ export function initRepoTopicBar() {
   });
 
   $.fn.form.settings.rules.validateTopic = function (_values, regExp) {
-    const topics = topicDropdown.children('a.ui.label');
-    const status = topics.length === 0 || topics.last().attr('data-value').match(regExp);
+    const $topics = $topicDropdown.children('a.ui.label');
+    const status = $topics.length === 0 || $topics.last().attr('data-value').match(regExp);
     if (!status) {
-      topics.last().removeClass('green').addClass('red');
+      $topics.last().removeClass('green').addClass('red');
     }
-    return status && topicDropdown.children('a.ui.label.red').length === 0;
+    return status && $topicDropdown.children('a.ui.label.red').length === 0;
   };
 
-  topicForm.form({
+  $topicForm.form({
     on: 'change',
     inline: true,
     fields: {
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 01c30a9e53..bca062bcc7 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -144,9 +144,9 @@ export function initRepoIssueSidebarList() {
 
   $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
     if (e.altKey && e.keyCode === 13) {
-      const selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected');
-      if (selectedItems.length > 0) {
-        excludeLabel($(selectedItems[0]));
+      const $selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected');
+      if ($selectedItems.length > 0) {
+        excludeLabel($($selectedItems[0]));
       }
     }
   });
@@ -214,12 +214,12 @@ export function initRepoIssueDependencyDelete() {
 export function initRepoIssueCodeCommentCancel() {
   // Cancel inline code comment
   $(document).on('click', '.cancel-code-comment', (e) => {
-    const form = $(e.currentTarget).closest('form');
-    if (form.length > 0 && form.hasClass('comment-form')) {
-      form.addClass('gt-hidden');
-      showElem(form.closest('.comment-code-cloud').find('button.comment-form-reply'));
+    const $form = $(e.currentTarget).closest('form');
+    if ($form.length > 0 && $form.hasClass('comment-form')) {
+      $form.addClass('gt-hidden');
+      showElem($form.closest('.comment-code-cloud').find('button.comment-form-reply'));
     } else {
-      form.closest('.comment-code-cloud').remove();
+      $form.closest('.comment-code-cloud').remove();
     }
   });
 }
@@ -370,10 +370,10 @@ export function initRepoIssueComments() {
   });
 
   $(document).on('click', (event) => {
-    const urlTarget = $(':target');
-    if (urlTarget.length === 0) return;
+    const $urlTarget = $(':target');
+    if ($urlTarget.length === 0) return;
 
-    const urlTargetId = urlTarget.attr('id');
+    const urlTargetId = $urlTarget.attr('id');
     if (!urlTargetId) return;
     if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
 
@@ -390,18 +390,18 @@ export function initRepoIssueComments() {
 
 export async function handleReply($el) {
   hideElem($el);
-  const form = $el.closest('.comment-code-cloud').find('.comment-form');
-  form.removeClass('gt-hidden');
+  const $form = $el.closest('.comment-code-cloud').find('.comment-form');
+  $form.removeClass('gt-hidden');
 
-  const $textarea = form.find('textarea');
+  const $textarea = $form.find('textarea');
   let editor = getComboMarkdownEditor($textarea);
   if (!editor) {
     // FIXME: the initialization of the dropzone is not consistent.
     // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
     // When the form is submitted and partially reload, none of them is initialized.
-    const dropzone = form.find('.dropzone')[0];
+    const dropzone = $form.find('.dropzone')[0];
     if (!dropzone.dropzone) initDropzone(dropzone);
-    editor = await initComboMarkdownEditor(form.find('.combo-markdown-editor'));
+    editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor'));
   }
   editor.focus();
   return editor;
@@ -413,30 +413,30 @@ export function initRepoPullRequestReview() {
     if (window.history.scrollRestoration !== 'manual') {
       window.history.scrollRestoration = 'manual';
     }
-    const commentDiv = $(window.location.hash);
-    if (commentDiv) {
+    const $commentDiv = $(window.location.hash);
+    if ($commentDiv) {
       // get the name of the parent id
-      const groupID = commentDiv.closest('div[id^="code-comments-"]').attr('id');
+      const groupID = $commentDiv.closest('div[id^="code-comments-"]').attr('id');
       if (groupID && groupID.startsWith('code-comments-')) {
         const id = groupID.slice(14);
-        const ancestorDiffBox = commentDiv.closest('.diff-file-box');
+        const $ancestorDiffBox = $commentDiv.closest('.diff-file-box');
         // on pages like conversation, there is no diff header
-        const diffHeader = ancestorDiffBox.find('.diff-file-header');
+        const $diffHeader = $ancestorDiffBox.find('.diff-file-header');
         // offset is for scrolling
         let offset = 30;
-        if (diffHeader[0]) {
-          offset += $('.diff-detail-box').outerHeight() + diffHeader.outerHeight();
+        if ($diffHeader[0]) {
+          offset += $('.diff-detail-box').outerHeight() + $diffHeader.outerHeight();
         }
         $(`#show-outdated-${id}`).addClass('gt-hidden');
         $(`#code-comments-${id}`).removeClass('gt-hidden');
         $(`#code-preview-${id}`).removeClass('gt-hidden');
         $(`#hide-outdated-${id}`).removeClass('gt-hidden');
         // if the comment box is folded, expand it
-        if (ancestorDiffBox.attr('data-folded') && ancestorDiffBox.attr('data-folded') === 'true') {
-          setFileFolding(ancestorDiffBox[0], ancestorDiffBox.find('.fold-file')[0], false);
+        if ($ancestorDiffBox.attr('data-folded') && $ancestorDiffBox.attr('data-folded') === 'true') {
+          setFileFolding($ancestorDiffBox[0], $ancestorDiffBox.find('.fold-file')[0], false);
         }
         window.scrollTo({
-          top: commentDiv.offset().top - offset,
+          top: $commentDiv.offset().top - offset,
           behavior: 'instant'
         });
       }
@@ -504,12 +504,12 @@ export function initRepoPullRequestReview() {
     const side = $(this).data('side');
     const idx = $(this).data('idx');
     const path = $(this).closest('[data-path]').data('path');
-    const tr = $(this).closest('tr');
-    const lineType = tr.data('line-type');
+    const $tr = $(this).closest('tr');
+    const lineType = $tr.data('line-type');
 
-    let ntr = tr.next();
-    if (!ntr.hasClass('add-comment')) {
-      ntr = $(`
+    let $ntr = $tr.next();
+    if (!$ntr.hasClass('add-comment')) {
+      $ntr = $(`
         <tr class="add-comment" data-line-type="${lineType}">
           ${isSplit ? `
             <td class="add-comment-left" colspan="4"></td>
@@ -518,22 +518,22 @@ export function initRepoPullRequestReview() {
             <td class="add-comment-left add-comment-right" colspan="5"></td>
           `}
         </tr>`);
-      tr.after(ntr);
+      $tr.after($ntr);
     }
 
-    const td = ntr.find(`.add-comment-${side}`);
-    const commentCloud = td.find('.comment-code-cloud');
-    if (commentCloud.length === 0 && !ntr.find('button[name="pending_review"]').length) {
+    const $td = $ntr.find(`.add-comment-${side}`);
+    const $commentCloud = $td.find('.comment-code-cloud');
+    if ($commentCloud.length === 0 && !$ntr.find('button[name="pending_review"]').length) {
       try {
         const response = await GET($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
         const html = await response.text();
-        td.html(html);
-        td.find("input[name='line']").val(idx);
-        td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
-        td.find("input[name='path']").val(path);
+        $td.html(html);
+        $td.find("input[name='line']").val(idx);
+        $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
+        $td.find("input[name='path']").val(path);
 
-        initDropzone(td.find('.dropzone')[0]);
-        const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor'));
+        initDropzone($td.find('.dropzone')[0]);
+        const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
         editor.focus();
       } catch (error) {
         console.error(error);
@@ -646,18 +646,18 @@ export function initRepoIssueTitleEdit() {
 
 export function initRepoIssueBranchSelect() {
   const changeBranchSelect = function () {
-    const selectionTextField = $('#pull-target-branch');
+    const $selectionTextField = $('#pull-target-branch');
 
-    const baseName = selectionTextField.data('basename');
+    const baseName = $selectionTextField.data('basename');
     const branchNameNew = $(this).data('branch');
-    const branchNameOld = selectionTextField.data('branch');
+    const branchNameOld = $selectionTextField.data('branch');
 
     // Replace branch name to keep translation from HTML template
-    selectionTextField.html(selectionTextField.html().replace(
+    $selectionTextField.html($selectionTextField.html().replace(
       `${baseName}:${branchNameOld}`,
       `${baseName}:${branchNameNew}`
     ));
-    selectionTextField.data('branch', branchNameNew); // update branch name in setting
+    $selectionTextField.data('branch', branchNameNew); // update branch name in setting
   };
   $('#branch-select > .item').on('click', changeBranchSelect);
 }
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 24fcc7c223..1df409c79e 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -76,11 +76,11 @@ export function initRepoCommentForm() {
       }
 
       if (editMode === 'true') {
-        const form = $('#update_issueref_form');
+        const $form = $('#update_issueref_form');
         const params = new URLSearchParams();
         params.append('ref', selectedValue);
         try {
-          await POST(form.attr('action'), {data: params});
+          await POST($form.attr('action'), {data: params});
           window.location.reload();
         } catch (error) {
           console.error(error);
@@ -139,7 +139,7 @@ export function initRepoCommentForm() {
 
       hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
 
-      const clickedItem = $(this);
+      const $clickedItem = $(this);
       const scope = $(this).attr('data-scope');
 
       $(this).parent().find('.item').each(function () {
@@ -148,10 +148,10 @@ export function initRepoCommentForm() {
           if ($(this).attr('data-scope') !== scope) {
             return true;
           }
-          if (!$(this).is(clickedItem) && !$(this).hasClass('checked')) {
+          if (!$(this).is($clickedItem) && !$(this).hasClass('checked')) {
             return true;
           }
-        } else if (!$(this).is(clickedItem)) {
+        } else if (!$(this).is($clickedItem)) {
           // Toggle for other labels
           return true;
         }
@@ -352,8 +352,8 @@ async function onEditContent(event) {
         this.on('success', (file, data) => {
           file.uuid = data.uuid;
           fileUuidDict[file.uuid] = {submitted: false};
-          const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
-          $dropzone.find('.files').append(input);
+          const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
+          $dropzone.find('.files').append($input);
         });
         this.on('removedfile', async (file) => {
           if (disableRemovedfileEvent) return;
@@ -390,8 +390,8 @@ async function onEditContent(event) {
               dz.files.push(attachment);
               fileUuidDict[attachment.uuid] = {submitted: true};
               $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
-              const input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
-              $dropzone.find('.files').append(input);
+              const $input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
+              $dropzone.find('.files').append($input);
             }
           } catch (error) {
             console.error(error);
@@ -430,8 +430,8 @@ async function onEditContent(event) {
       } else {
         $renderContent.html(data.content);
         $rawContent.text(comboMarkdownEditor.value());
-        const refIssues = $renderContent.find('p .ref-issue');
-        attachRefIssueContextPopup(refIssues);
+        const $refIssues = $renderContent.find('p .ref-issue');
+        attachRefIssueContextPopup($refIssues);
       }
       const $content = $segment;
       if (!$content.find('.dropzone-attachments').length) {
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index 5dd80b29b9..1f86711ab1 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -105,14 +105,14 @@ export function initRepoProject() {
   const _promise = initRepoProjectSortable();
 
   $('.edit-project-column-modal').each(function () {
-    const projectHeader = $(this).closest('.project-column-header');
-    const projectTitleLabel = projectHeader.find('.project-column-title');
-    const projectTitleInput = $(this).find('.project-column-title-input');
-    const projectColorInput = $(this).find('#new_project_column_color');
-    const boardColumn = $(this).closest('.project-column');
+    const $projectHeader = $(this).closest('.project-column-header');
+    const $projectTitleLabel = $projectHeader.find('.project-column-title');
+    const $projectTitleInput = $(this).find('.project-column-title-input');
+    const $projectColorInput = $(this).find('#new_project_column_color');
+    const $boardColumn = $(this).closest('.project-column');
 
-    if (boardColumn.css('backgroundColor')) {
-      setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor')));
+    if ($boardColumn.css('backgroundColor')) {
+      setLabelColor($projectHeader, rgbToHex($boardColumn.css('backgroundColor')));
     }
 
     $(this).find('.edit-project-column-button').on('click', async function (e) {
@@ -121,34 +121,34 @@ export function initRepoProject() {
       try {
         await PUT($(this).data('url'), {
           data: {
-            title: projectTitleInput.val(),
-            color: projectColorInput.val(),
+            title: $projectTitleInput.val(),
+            color: $projectColorInput.val(),
           },
         });
       } catch (error) {
         console.error(error);
       } finally {
-        projectTitleLabel.text(projectTitleInput.val());
-        projectTitleInput.closest('form').removeClass('dirty');
-        if (projectColorInput.val()) {
-          setLabelColor(projectHeader, projectColorInput.val());
+        $projectTitleLabel.text($projectTitleInput.val());
+        $projectTitleInput.closest('form').removeClass('dirty');
+        if ($projectColorInput.val()) {
+          setLabelColor($projectHeader, $projectColorInput.val());
         }
-        boardColumn.attr('style', `background: ${projectColorInput.val()}!important`);
+        $boardColumn.attr('style', `background: ${$projectColorInput.val()}!important`);
         $('.ui.modal').modal('hide');
       }
     });
   });
 
   $('.default-project-column-modal').each(function () {
-    const boardColumn = $(this).closest('.project-column');
-    const showButton = $(boardColumn).find('.default-project-column-show');
-    const commitButton = $(this).find('.actions > .ok.button');
+    const $boardColumn = $(this).closest('.project-column');
+    const $showButton = $($boardColumn).find('.default-project-column-show');
+    const $commitButton = $(this).find('.actions > .ok.button');
 
-    $(commitButton).on('click', async (e) => {
+    $($commitButton).on('click', async (e) => {
       e.preventDefault();
 
       try {
-        await POST($(showButton).data('url'));
+        await POST($($showButton).data('url'));
       } catch (error) {
         console.error(error);
       } finally {
@@ -158,11 +158,11 @@ export function initRepoProject() {
   });
 
   $('.show-delete-project-column-modal').each(function () {
-    const deleteColumnModal = $(`${$(this).attr('data-modal')}`);
-    const deleteColumnButton = deleteColumnModal.find('.actions > .ok.button');
+    const $deleteColumnModal = $(`${$(this).attr('data-modal')}`);
+    const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button');
     const deleteUrl = $(this).attr('data-url');
 
-    deleteColumnButton.on('click', async (e) => {
+    $deleteColumnButton.on('click', async (e) => {
       e.preventDefault();
 
       try {
@@ -177,13 +177,13 @@ export function initRepoProject() {
 
   $('#new_project_column_submit').on('click', (e) => {
     e.preventDefault();
-    const columnTitle = $('#new_project_column');
-    const projectColorInput = $('#new_project_column_color_picker');
-    if (!columnTitle.val()) {
+    const $columnTitle = $('#new_project_column');
+    const $projectColorInput = $('#new_project_column_color_picker');
+    if (!$columnTitle.val()) {
       return;
     }
     const url = e.target.getAttribute('data-url');
-    createNewColumn(url, columnTitle, projectColorInput);
+    createNewColumn(url, $columnTitle, $projectColorInput);
   });
 }
 
diff --git a/web_src/js/jquery.js b/web_src/js/jquery.js
index 892e2763cb..6b2199896c 100644
--- a/web_src/js/jquery.js
+++ b/web_src/js/jquery.js
@@ -1,3 +1,3 @@
 import $ from 'jquery';
 
-window.$ = window.jQuery = $;
+window.$ = window.jQuery = $; // eslint-disable-line no-jquery/variable-pattern

From c6e5ec51bd5d2d3ede30b7506e7cc47f18a49ca8 Mon Sep 17 00:00:00 2001
From: 6543 <m.huber@kithara.com>
Date: Sat, 16 Mar 2024 14:19:41 +0100
Subject: [PATCH 402/679] Meilisearch double quote on "match" query (#29740)

make `nonFuzzyWorkaround` unessesary

cc @Kerollmops
---
 .../indexer/issues/meilisearch/meilisearch.go | 83 ++++++-------------
 .../issues/meilisearch/meilisearch_test.go    | 24 +++---
 2 files changed, 37 insertions(+), 70 deletions(-)

diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index 34066bf559..b735c26968 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -6,6 +6,7 @@ package meilisearch
 import (
 	"context"
 	"errors"
+	"fmt"
 	"strconv"
 	"strings"
 
@@ -217,7 +218,14 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 
 	skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxTotalHits)
 
-	searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(options.Keyword, &meilisearch.SearchRequest{
+	keyword := options.Keyword
+	if !options.IsFuzzyKeyword {
+		// to make it non fuzzy ("typo tolerance" in meilisearch terms), we have to quote the keyword(s)
+		// https://www.meilisearch.com/docs/reference/api/search#phrase-search
+		keyword = doubleQuoteKeyword(keyword)
+	}
+
+	searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{
 		Filter:           query.Statement(),
 		Limit:            int64(limit),
 		Offset:           int64(skip),
@@ -228,7 +236,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		return nil, err
 	}
 
-	hits, err := nonFuzzyWorkaround(searchRes, options.Keyword, options.IsFuzzyKeyword)
+	hits, err := convertHits(searchRes)
 	if err != nil {
 		return nil, err
 	}
@@ -247,11 +255,20 @@ func parseSortBy(sortBy internal.SortBy) string {
 	return field + ":asc"
 }
 
-// nonFuzzyWorkaround is needed as meilisearch does not have an exact search
-// and you can only change "typo tolerance" per index. So we have to post-filter the results
-// https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#configuring-typo-tolerance
-// TODO: remove once https://github.com/orgs/meilisearch/discussions/377 is addressed
-func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, isFuzzy bool) ([]internal.Match, error) {
+func doubleQuoteKeyword(k string) string {
+	kp := strings.Split(k, " ")
+	parts := 0
+	for i := range kp {
+		part := strings.Trim(kp[i], "\"")
+		if part != "" {
+			kp[parts] = fmt.Sprintf(`"%s"`, part)
+			parts++
+		}
+	}
+	return strings.Join(kp[:parts], " ")
+}
+
+func convertHits(searchRes *meilisearch.SearchResponse) ([]internal.Match, error) {
 	hits := make([]internal.Match, 0, len(searchRes.Hits))
 	for _, hit := range searchRes.Hits {
 		hit, ok := hit.(map[string]any)
@@ -259,61 +276,11 @@ func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, i
 			return nil, ErrMalformedResponse
 		}
 
-		if !isFuzzy {
-			keyword = strings.ToLower(keyword)
-
-			// declare a anon func to check if the title, content or at least one comment contains the keyword
-			found, err := func() (bool, error) {
-				// check if title match first
-				title, ok := hit["title"].(string)
-				if !ok {
-					return false, ErrMalformedResponse
-				} else if strings.Contains(strings.ToLower(title), keyword) {
-					return true, nil
-				}
-
-				// check if content has a match
-				content, ok := hit["content"].(string)
-				if !ok {
-					return false, ErrMalformedResponse
-				} else if strings.Contains(strings.ToLower(content), keyword) {
-					return true, nil
-				}
-
-				// now check for each comment if one has a match
-				// so we first try to cast and skip if there are no comments
-				comments, ok := hit["comments"].([]any)
-				if !ok {
-					return false, ErrMalformedResponse
-				} else if len(comments) == 0 {
-					return false, nil
-				}
-
-				// now we iterate over all and report as soon as we detect one match
-				for i := range comments {
-					comment, ok := comments[i].(string)
-					if !ok {
-						return false, ErrMalformedResponse
-					}
-					if strings.Contains(strings.ToLower(comment), keyword) {
-						return true, nil
-					}
-				}
-
-				// we got no match
-				return false, nil
-			}()
-
-			if err != nil {
-				return nil, err
-			} else if !found {
-				continue
-			}
-		}
 		issueID, ok := hit["id"].(float64)
 		if !ok {
 			return nil, ErrMalformedResponse
 		}
+
 		hits = append(hits, internal.Match{
 			ID: int64(issueID),
 		})
diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go
index ecce704236..4666df136a 100644
--- a/modules/indexer/issues/meilisearch/meilisearch_test.go
+++ b/modules/indexer/issues/meilisearch/meilisearch_test.go
@@ -53,11 +53,10 @@ func TestMeilisearchIndexer(t *testing.T) {
 	tests.TestIndexer(t, indexer)
 }
 
-func TestNonFuzzyWorkaround(t *testing.T) {
-	// get unexpected return
-	_, err := nonFuzzyWorkaround(&meilisearch.SearchResponse{
+func TestConvertHits(t *testing.T) {
+	_, err := convertHits(&meilisearch.SearchResponse{
 		Hits: []any{"aa", "bb", "cc", "dd"},
-	}, "bowling", false)
+	})
 	assert.ErrorIs(t, err, ErrMalformedResponse)
 
 	validResponse := &meilisearch.SearchResponse{
@@ -82,14 +81,15 @@ func TestNonFuzzyWorkaround(t *testing.T) {
 			},
 		},
 	}
-
-	// nonFuzzy
-	hits, err := nonFuzzyWorkaround(validResponse, "bowling", false)
-	assert.NoError(t, err)
-	assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}}, hits)
-
-	// fuzzy
-	hits, err = nonFuzzyWorkaround(validResponse, "bowling", true)
+	hits, err := convertHits(validResponse)
 	assert.NoError(t, err)
 	assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits)
 }
+
+func TestDoubleQuoteKeyword(t *testing.T) {
+	assert.EqualValues(t, "", doubleQuoteKeyword(""))
+	assert.EqualValues(t, `"a" "b" "c"`, doubleQuoteKeyword("a b c"))
+	assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a  d g"))
+	assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a  d g"))
+	assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword(`a  "" "d" """g`))
+}

From f9b4efd42c17d7f75b689142b17575a478fe903c Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 16 Mar 2024 15:25:27 +0200
Subject: [PATCH 403/679] Forbid HTML injection using jQuery (#29843)

See
https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/no-append-html.md

Tested the following components and they work as before:
- notification table
- issue author dropdown
- comment edit box attachments div

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 .eslintrc.yaml                          | 2 +-
 web_src/js/features/notification.js     | 6 +++---
 web_src/js/features/repo-issue-list.js  | 4 +++-
 web_src/js/features/repo-legacy.js      | 5 ++---
 web_src/js/modules/fomantic/dropdown.js | 4 +++-
 5 files changed, 12 insertions(+), 9 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 5f291f13e7..0003ba95e1 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -400,7 +400,7 @@ rules:
   no-jquery/no-and-self: [2]
   no-jquery/no-animate-toggle: [2]
   no-jquery/no-animate: [2]
-  no-jquery/no-append-html: [0]
+  no-jquery/no-append-html: [2]
   no-jquery/no-attr: [0]
   no-jquery/no-bind: [2]
   no-jquery/no-box-model: [2]
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 57b7bcb6e8..90e19b683b 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -143,8 +143,8 @@ async function updateNotificationCountWithCallback(callback, timeout, lastCount)
 }
 
 async function updateNotificationTable() {
-  const $notificationDiv = $('#notification_div');
-  if ($notificationDiv.length > 0) {
+  const notificationDiv = document.getElementById('notification_div');
+  if (notificationDiv) {
     try {
       const params = new URLSearchParams(window.location.search);
       params.set('div-only', true);
@@ -158,7 +158,7 @@ async function updateNotificationTable() {
 
       const data = await response.text();
       if ($(data).data('sequence-number') === notificationSequenceNumber) {
-        $notificationDiv.replaceWith(data);
+        notificationDiv.outerHTML = data;
         initNotificationsTable();
       }
     } catch (error) {
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index efc7671204..21f1865732 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -125,7 +125,9 @@ function initRepoIssueListAuthorDropdown() {
     if (newMenuHtml) {
       const $newMenuItems = $(newMenuHtml);
       $newMenuItems.addClass('dynamic-item');
-      $menu.append('<div class="divider dynamic-item"></div>', ...$newMenuItems);
+      const div = document.createElement('div');
+      div.classList.add('divider', 'dynamic-item');
+      $menu[0].append(div, ...$newMenuItems);
     }
     $searchDropdown.dropdown('refresh');
     // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 1df409c79e..10c25bf28b 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -436,13 +436,12 @@ async function onEditContent(event) {
       const $content = $segment;
       if (!$content.find('.dropzone-attachments').length) {
         if (data.attachments !== '') {
-          $content.append(`<div class="dropzone-attachments"></div>`);
-          $content.find('.dropzone-attachments').replaceWith(data.attachments);
+          $content[0].append(data.attachments);
         }
       } else if (data.attachments === '') {
         $content.find('.dropzone-attachments').remove();
       } else {
-        $content.find('.dropzone-attachments').replaceWith(data.attachments);
+        $content.find('.dropzone-attachments')[0].outerHTML = data.attachments;
       }
       if (dz) {
         dz.emit('submit');
diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
index caba8a2f28..7302078dbd 100644
--- a/web_src/js/modules/fomantic/dropdown.js
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -72,7 +72,9 @@ function delegateOne($dropdown) {
   dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
     // when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
     const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
-    const $wrapper = $('<div>').append(menuItems);
+    const div = document.createElement('div');
+    div.innerHTML = menuItems;
+    const $wrapper = $(div);
     const $items = $wrapper.find('> .item');
     $items.each((_, item) => updateMenuItem($dropdown[0], item));
     $dropdown[0][ariaPatchKey].deferredRefreshAriaActiveItem();

From cb78648ad91a9ed0acd0640c2e7b1f580290999a Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 16 Mar 2024 21:59:02 +0800
Subject: [PATCH 404/679] Fix wrong test for TestPullView_CodeOwner (#29838)

It's weird the previous test was PASS.
---
 tests/integration/pull_review_test.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 7166a740ab..459aa5a58b 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -119,7 +119,7 @@ func TestPullView_CodeOwner(t *testing.T) {
 			user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 			forkedRepo, err := repo_service.ForkRepository(db.DefaultContext, user2, user5, repo_service.ForkRepoOptions{
 				BaseRepo: repo,
-				Name:     "test_codeowner_fork",
+				Name:     "test_codeowner",
 			})
 			assert.NoError(t, err)
 
@@ -137,9 +137,9 @@ func TestPullView_CodeOwner(t *testing.T) {
 			assert.NoError(t, err)
 
 			session := loginUser(t, "user5")
-			testPullCreate(t, session, "user5", "test_codeowner_fork", false, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2")
+			testPullCreate(t, session, "user5", "test_codeowner", true, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2")
 
-			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch-forked"})
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"})
 			unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
 		})
 	})

From 21fe512aac42c9ce3440b8eaae6b2cb2116a0e50 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 16 Mar 2024 16:08:10 +0100
Subject: [PATCH 405/679] Forbid jQuery `.prop` and fix related issues (#29832)

The issue checkbox code received a few more cleanups and I specifically
tested it. The other changes are trivial. Also, I checked the cases for
how many elements match the jQuery selection to determine querySelector
vs. querySelectorAll.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 .eslintrc.yaml                         |  4 +--
 web_src/js/features/admin/common.js    |  2 +-
 web_src/js/features/comp/LabelEdit.js  |  6 ++---
 web_src/js/features/repo-editor.js     | 14 +++++-----
 web_src/js/features/repo-issue-list.js | 36 ++++++++++++++++----------
 web_src/js/features/repo-legacy.js     |  2 +-
 6 files changed, 37 insertions(+), 27 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 0003ba95e1..b62b13cefe 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -315,7 +315,7 @@ rules:
   jquery/no-parent: [0]
   jquery/no-parents: [0]
   jquery/no-parse-html: [2]
-  jquery/no-prop: [0]
+  jquery/no-prop: [2]
   jquery/no-proxy: [2]
   jquery/no-ready: [2]
   jquery/no-serialize: [2]
@@ -466,7 +466,7 @@ rules:
   no-jquery/no-parse-html: [2]
   no-jquery/no-parse-json: [2]
   no-jquery/no-parse-xml: [2]
-  no-jquery/no-prop: [0]
+  no-jquery/no-prop: [2]
   no-jquery/no-proxy: [2]
   no-jquery/no-ready-shorthand: [2]
   no-jquery/no-ready: [2]
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 5354216e3d..31d840c3e1 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -49,7 +49,7 @@ export function initAdminCommon() {
   }
 
   function onUsePagedSearchChange() {
-    if ($('#use_paged_search').prop('checked')) {
+    if (document.getElementById('use_paged_search').checked) {
       showElem('.search-page-size');
       $('.search-page-size').find('input').attr('required', 'required');
     } else {
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index 2e7e1df669..26800ae05c 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -14,7 +14,7 @@ function updateExclusiveLabelEdit(form) {
   if (isExclusiveScopeName($nameInput.val())) {
     $exclusiveField.removeClass('muted');
     $exclusiveField.removeAttr('aria-disabled');
-    if ($exclusiveCheckbox.prop('checked') && $exclusiveCheckbox.data('exclusive-warn')) {
+    if ($exclusiveCheckbox[0].checked && $exclusiveCheckbox.data('exclusive-warn')) {
       $exclusiveWarning.removeClass('gt-hidden');
     } else {
       $exclusiveWarning.addClass('gt-hidden');
@@ -50,10 +50,10 @@ export function initCompLabelEdit(selector) {
     $nameInput.val($(this).data('title'));
 
     const $isArchivedCheckbox = $('.edit-label .label-is-archived-input');
-    $isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived'));
+    $isArchivedCheckbox[0].checked = this.hasAttribute('data-is-archived');
 
     const $exclusiveCheckbox = $('.edit-label .label-exclusive-input');
-    $exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive'));
+    $exclusiveCheckbox[0].checked = this.hasAttribute('data-exclusive');
     // Warn when label was previously not exclusive and used in issues
     $exclusiveCheckbox.data('exclusive-warn',
       $(this).data('num-issues') > 0 &&
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index fea98e2df8..ba00573c07 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -67,10 +67,10 @@ export function initRepoEditor() {
   $('.js-quick-pull-choice-option').on('change', function () {
     if ($(this).val() === 'commit-to-new-branch') {
       showElem($('.quick-pull-branch-name'));
-      $('.quick-pull-branch-name input').prop('required', true);
+      document.querySelector('.quick-pull-branch-name input').required = true;
     } else {
       hideElem($('.quick-pull-branch-name'));
-      $('.quick-pull-branch-name input').prop('required', false);
+      document.querySelector('.quick-pull-branch-name input').required = false;
     }
     $('#commit-button').text($(this).attr('button_text'));
   });
@@ -135,13 +135,13 @@ export function initRepoEditor() {
 
     // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
     // to enable or disable the commit button
-    const $commitButton = $('#commit-button');
+    const commitButton = document.getElementById('commit-button');
     const $editForm = $('.ui.edit.form');
     const dirtyFileClass = 'dirty-file';
 
     // Disabling the button at the start
     if ($('input[name="page_has_posted"]').val() !== 'true') {
-      $commitButton.prop('disabled', true);
+      commitButton.disabled = true;
     }
 
     // Registering a custom listener for the file path and the file content
@@ -151,7 +151,7 @@ export function initRepoEditor() {
       fieldSelector: ':input:not(.commit-form-wrapper :input)',
       change() {
         const dirty = $(this).hasClass(dirtyFileClass);
-        $commitButton.prop('disabled', !dirty);
+        commitButton.disabled = !dirty;
       },
     });
 
@@ -163,7 +163,7 @@ export function initRepoEditor() {
       editor.setValue(value);
     }
 
-    $commitButton.on('click', (event) => {
+    commitButton?.addEventListener('click', (e) => {
       // A modal which asks if an empty file should be committed
       if ($editArea.val().length === 0) {
         $('#edit-empty-content-modal').modal({
@@ -171,7 +171,7 @@ export function initRepoEditor() {
             $('.edit.form').trigger('submit');
           },
         }).modal('show');
-        event.preventDefault();
+        e.preventDefault();
       }
     });
   })();
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 21f1865732..48b1555c89 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -1,6 +1,6 @@
 import $ from 'jquery';
 import {updateIssuesMeta} from './repo-issue.js';
-import {toggleElem, hideElem} from '../utils/dom.js';
+import {toggleElem, hideElem, isElemHidden} from '../utils/dom.js';
 import {htmlEscape} from 'escape-goat';
 import {confirmModal} from './comp/ConfirmModal.js';
 import {showErrorToast} from '../modules/toast.js';
@@ -8,32 +8,42 @@ import {createSortable} from '../modules/sortable.js';
 import {DELETE, POST} from '../modules/fetch.js';
 
 function initRepoIssueListCheckboxes() {
-  const $issueSelectAll = $('.issue-checkbox-all');
-  const $issueCheckboxes = $('.issue-checkbox');
+  const issueSelectAll = document.querySelector('.issue-checkbox-all');
+  const issueCheckboxes = document.querySelectorAll('.issue-checkbox');
 
   const syncIssueSelectionState = () => {
-    const $checked = $issueCheckboxes.filter(':checked');
-    const anyChecked = $checked.length !== 0;
-    const allChecked = anyChecked && $checked.length === $issueCheckboxes.length;
+    const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked);
+    const anyChecked = Boolean(checkedCheckboxes.length);
+    const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length;
 
     if (allChecked) {
-      $issueSelectAll.prop({'checked': true, 'indeterminate': false});
+      issueSelectAll.checked = true;
+      issueSelectAll.indeterminate = false;
     } else if (anyChecked) {
-      $issueSelectAll.prop({'checked': false, 'indeterminate': true});
+      issueSelectAll.checked = false;
+      issueSelectAll.indeterminate = true;
     } else {
-      $issueSelectAll.prop({'checked': false, 'indeterminate': false});
+      issueSelectAll.checked = false;
+      issueSelectAll.indeterminate = false;
     }
     // if any issue is selected, show the action panel, otherwise show the filter panel
     toggleElem($('#issue-filters'), !anyChecked);
     toggleElem($('#issue-actions'), anyChecked);
     // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
-    $('#issue-filters, #issue-actions').filter(':visible').find('.issue-list-toolbar-left').prepend($issueSelectAll);
+    const panels = document.querySelectorAll('#issue-filters, #issue-actions');
+    const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el));
+    const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left');
+    toolbarLeft.prepend(issueSelectAll);
   };
 
-  $issueCheckboxes.on('change', syncIssueSelectionState);
+  for (const el of issueCheckboxes) {
+    el.addEventListener('change', syncIssueSelectionState);
+  }
 
-  $issueSelectAll.on('change', () => {
-    $issueCheckboxes.prop('checked', $issueSelectAll.is(':checked'));
+  issueSelectAll.addEventListener('change', () => {
+    for (const el of issueCheckboxes) {
+      el.checked = issueSelectAll.checked;
+    }
     syncIssueSelectionState();
   });
 
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 10c25bf28b..96cfa78d0b 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -533,7 +533,7 @@ export function initRepository() {
       const gitignores = $('input[name="gitignores"]').val();
       const license = $('input[name="license"]').val();
       if (gitignores || license) {
-        $('input[name="auto_init"]').prop('checked', true);
+        document.querySelector('input[name="auto_init"]').checked = true;
       }
     });
   }

From ffeaf2d0bd6c99c486aa7869779bb9ceb0aedad6 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 16 Mar 2024 17:58:58 +0100
Subject: [PATCH 406/679] add `.suppressed` link class (#29847)

Extract from https://github.com/go-gitea/gitea/pull/29344. With this
class it's possible to have links that don't color on hover. It will be
useful for https://github.com/go-gitea/gitea/pull/29429.
---
 web_src/css/base.css | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index d995f51038..6a5f923281 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -149,24 +149,32 @@ a {
   text-decoration-skip-ink: all;
 }
 
-/* muted link = only colored when hovered */
-/* silenced link = never colored */
+/* a = always colored, underlined on hover */
+/* a.muted = colored on hover, underlined on hover */
+/* a.suppressed = never colored, underlined on hover */
+/* a.silenced = never colored, never underlined */
 
 a.muted,
+a.suppressed,
 a.silenced,
 .muted-links a {
   color: inherit;
 }
 
 a:hover,
+a.suppressed:hover,
 a.muted:hover,
 a.muted:hover [class*="color-text"],
 .muted-links a:hover {
   color: var(--color-primary);
 }
 
-a.silenced:hover {
+a.silenced:hover,
+a.suppressed:hover {
   color: inherit;
+}
+
+a.silenced:hover {
   text-decoration: none;
 }
 

From 43aa914b1750add631698a7fd57d76f743b0489b Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 16 Mar 2024 18:03:56 +0100
Subject: [PATCH 407/679] fix double border and border-radius on empty action
 steps (#29845)

Before, double border-bottom and incorrect border-radius:

<img width="914" alt="Screenshot 2024-03-16 at 14 46 31"
src="https://github.com/go-gitea/gitea/assets/115237/6ea63c42-754c-420c-a0f5-c889a8507d9f">

After, both fixed:

<img width="917" alt="Screenshot 2024-03-16 at 14 45 59"
src="https://github.com/go-gitea/gitea/assets/115237/9d3f2dba-6b22-441d-8e99-5809d5f1f1c0">
---
 web_src/js/components/RepoActionView.vue | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 484a3677c5..c1e2c2b2d5 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -457,7 +457,7 @@ export function initRepositoryActionView() {
             </div>
           </div>
         </div>
-        <div class="job-step-container" ref="steps">
+        <div class="job-step-container" ref="steps" v-if="currentJob.steps.length">
           <div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
             <div class="job-step-summary" @click.stop="toggleStepLogs(i)" :class="currentJobStepsStates[i].expanded ? 'selected' : ''">
               <!-- If the job is done and the job step log is loaded for the first time, show the loading icon
@@ -686,11 +686,15 @@ export function initRepositoryActionView() {
   background-color: var(--color-console-bg);
   position: sticky;
   top: 0;
-  border-radius: var(--border-radius) var(--border-radius) 0 0;
+  border-radius: var(--border-radius);
   height: 60px;
   z-index: 1;
 }
 
+.job-info-header:has(+ .job-step-container) {
+  border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
 .job-info-header .job-info-header-title {
   color: var(--color-console-fg);
   font-size: 16px;

From 4e547822f348c2963e0db33135b45d43dfc58df8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 17 Mar 2024 04:21:14 +0100
Subject: [PATCH 408/679] Remove fomantic message module (#29856)

Remove this CSS-only module, which gives a nice reduction in CSS size.
Should look exactly like before.
---
 web_src/css/base.css                |  67 ---
 web_src/css/index.css               |   1 +
 web_src/css/modules/message.css     |  99 ++++
 web_src/fomantic/build/semantic.css | 684 ----------------------------
 4 files changed, 100 insertions(+), 751 deletions(-)
 create mode 100644 web_src/css/modules/message.css

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 6a5f923281..ea5ba9ce87 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -510,73 +510,6 @@ ol.ui.list li,
   visibility: visible !important;
 }
 
-.ui.message {
-  background: var(--color-box-body);
-  color: var(--color-text);
-  box-shadow: none !important;
-  border: 1px solid var(--color-secondary);
-}
-
-.ui.info.message .header,
-.ui.blue.message .header {
-  color: var(--color-blue);
-}
-
-.ui.info.message,
-.ui.attached.info.message,
-.ui.blue.message,
-.ui.attached.blue.message {
-  background: var(--color-info-bg);
-  color: var(--color-info-text);
-  border-color: var(--color-info-border);
-}
-
-.ui.success.message .header,
-.ui.positive.message .header,
-.ui.green.message .header {
-  color: var(--color-green);
-}
-
-.ui.success.message,
-.ui.attached.success.message,
-.ui.positive.message,
-.ui.attached.positive.message {
-  background: var(--color-success-bg);
-  color: var(--color-success-text);
-  border-color: var(--color-success-border);
-}
-
-.ui.error.message .header,
-.ui.negative.message .header,
-.ui.red.message .header {
-  color: var(--color-red);
-}
-
-.ui.error.message,
-.ui.attached.error.message,
-.ui.red.message,
-.ui.attached.red.message,
-.ui.negative.message,
-.ui.attached.negative.message {
-  background: var(--color-error-bg);
-  color: var(--color-error-text);
-  border-color: var(--color-error-border);
-}
-
-.ui.warning.message .header,
-.ui.yellow.message .header {
-  color: var(--color-yellow);
-}
-
-.ui.warning.message,
-.ui.attached.warning.message,
-.ui.yellow.message,
-.ui.attached.yellow.message {
-  background: var(--color-warning-bg);
-  color: var(--color-warning-text);
-  border-color: var(--color-warning-border);
-}
-
 .ui.error.header {
   background: var(--color-error-bg) !important;
   color: var(--color-error-text) !important;
diff --git a/web_src/css/index.css b/web_src/css/index.css
index ab925a4aa0..f6e4c196e6 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -12,6 +12,7 @@
 @import "./modules/divider.css";
 @import "./modules/svg.css";
 @import "./modules/flexcontainer.css";
+@import "./modules/message.css";
 
 @import "./shared/flex-list.css";
 @import "./shared/milestone.css";
diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css
new file mode 100644
index 0000000000..22dd03232b
--- /dev/null
+++ b/web_src/css/modules/message.css
@@ -0,0 +1,99 @@
+.ui.message {
+  background: var(--color-box-body);
+  color: var(--color-text);
+  border: 1px solid var(--color-secondary);
+  position: relative;
+  min-height: 1em;
+  margin: 1em 0;
+  padding: 1em 1.5em;
+  border-radius: var(--border-radius);
+}
+
+.ui.message:first-child {
+  margin-top: 0;
+}
+
+.ui.message:last-child {
+  margin-bottom: 0;
+}
+
+.ui.attached.message {
+  margin-bottom: -1px;
+  border-radius: var(--border-radius) var(--border-radius) 0 0;
+  margin-left: -1px;
+  margin-right: -1px;
+}
+
+.ui.attached + .ui.attached.message:not(.top):not(.bottom) {
+  margin-top: -1px;
+  border-radius: 0;
+}
+
+.ui.bottom.attached.message {
+  margin-top: -1px;
+  border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+.ui.bottom.attached.message:not(:last-child) {
+  margin-bottom: 1em;
+}
+
+.ui.info.message .header,
+.ui.blue.message .header {
+  color: var(--color-blue);
+}
+
+.ui.info.message,
+.ui.attached.info.message,
+.ui.blue.message,
+.ui.attached.blue.message {
+  background: var(--color-info-bg);
+  color: var(--color-info-text);
+  border-color: var(--color-info-border);
+}
+
+.ui.success.message .header,
+.ui.positive.message .header,
+.ui.green.message .header {
+  color: var(--color-green);
+}
+
+.ui.success.message,
+.ui.attached.success.message,
+.ui.positive.message,
+.ui.attached.positive.message {
+  background: var(--color-success-bg);
+  color: var(--color-success-text);
+  border-color: var(--color-success-border);
+}
+
+.ui.error.message .header,
+.ui.negative.message .header,
+.ui.red.message .header {
+  color: var(--color-red);
+}
+
+.ui.error.message,
+.ui.attached.error.message,
+.ui.red.message,
+.ui.attached.red.message,
+.ui.negative.message,
+.ui.attached.negative.message {
+  background: var(--color-error-bg);
+  color: var(--color-error-text);
+  border-color: var(--color-error-border);
+}
+
+.ui.warning.message .header,
+.ui.yellow.message .header {
+  color: var(--color-yellow);
+}
+
+.ui.warning.message,
+.ui.attached.warning.message,
+.ui.yellow.message,
+.ui.attached.yellow.message {
+  background: var(--color-warning-bg);
+  color: var(--color-warning-text);
+  border-color: var(--color-warning-border);
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 476f7ebf11..6f45c1944c 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -14926,690 +14926,6 @@ Floated Menu / Item
 /*******************************
          Site Overrides
 *******************************/
-/*!
- * # Fomantic-UI - Message
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Message
-*******************************/
-
-.ui.message {
-  position: relative;
-  min-height: 1em;
-  margin: 1em 0;
-  background: #F8F8F9;
-  padding: 1em 1.5em;
-  line-height: 1.4285em;
-  color: rgba(0, 0, 0, 0.87);
-  transition: opacity 0.1s ease, color 0.1s ease, background 0.1s ease, box-shadow 0.1s ease;
-  border-radius: 0.28571429rem;
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.message:first-child {
-  margin-top: 0;
-}
-
-.ui.message:last-child {
-  margin-bottom: 0;
-}
-
-/*--------------
-     Content
----------------*/
-
-/* Header */
-
-.ui.message .header {
-  display: block;
-  font-family: var(--fonts-regular);
-  font-weight: 500;
-  margin: -0.14285714em 0 0 0;
-}
-
-/* Default font size */
-
-.ui.message .header:not(.ui) {
-  font-size: 1.14285714em;
-}
-
-/* Paragraph */
-
-.ui.message p {
-  opacity: 0.85;
-  margin: 0.75em 0;
-}
-
-.ui.message p:first-child {
-  margin-top: 0;
-}
-
-.ui.message p:last-child {
-  margin-bottom: 0;
-}
-
-.ui.message .header + p {
-  margin-top: 0.25em;
-}
-
-/* List */
-
-.ui.message .list:not(.ui) {
-  text-align: left;
-  padding: 0;
-  opacity: 0.85;
-  list-style-position: inside;
-  margin: 0.5em 0 0;
-}
-
-.ui.message .list:not(.ui):first-child {
-  margin-top: 0;
-}
-
-.ui.message .list:not(.ui):last-child {
-  margin-bottom: 0;
-}
-
-.ui.message .list:not(.ui) li {
-  position: relative;
-  list-style-type: none;
-  margin: 0 0 0.3em 1em;
-  padding: 0;
-}
-
-.ui.message .list:not(.ui) li:before {
-  position: absolute;
-  content: '•';
-  left: -1em;
-  height: 100%;
-  vertical-align: baseline;
-}
-
-.ui.message .list:not(.ui) li:last-child {
-  margin-bottom: 0;
-}
-
-/* Icon */
-
-.ui.message > i.icon {
-  margin-right: 0.6em;
-}
-
-/* Close Icon */
-
-.ui.message > .close.icon {
-  cursor: pointer;
-  position: absolute;
-  margin: 0;
-  top: 0.78575em;
-  right: 0.5em;
-  opacity: 0.7;
-  transition: opacity 0.1s ease;
-}
-
-.ui.message > .close.icon:hover {
-  opacity: 1;
-}
-
-/* First / Last Element */
-
-.ui.message > :first-child {
-  margin-top: 0;
-}
-
-.ui.message > :last-child {
-  margin-bottom: 0;
-}
-
-/*******************************
-            Coupling
-*******************************/
-
-.ui.dropdown .menu > .message {
-  margin: 0 -1px;
-}
-
-/*******************************
-            States
-*******************************/
-
-/*--------------
-    Visible
----------------*/
-
-.ui.visible.visible.visible.visible.message {
-  display: block;
-}
-
-.ui.icon.visible.visible.visible.visible.message {
-  display: flex;
-}
-
-/*--------------
-     Hidden
----------------*/
-
-.ui.hidden.hidden.hidden.hidden.message {
-  display: none;
-}
-
-/*******************************
-            Variations
-*******************************/
-
-/*--------------
-      Compact
-  ---------------*/
-
-.ui.compact.message {
-  display: inline-block;
-}
-
-.ui.compact.icon.message {
-  display: inline-flex;
-  width: auto;
-}
-
-/*--------------
-      Attached
-  ---------------*/
-
-.ui.attached.message {
-  margin-bottom: -1px;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-  margin-left: -1px;
-  margin-right: -1px;
-}
-
-.ui.attached + .ui.attached.message:not(.top):not(.bottom) {
-  margin-top: -1px;
-  border-radius: 0;
-}
-
-.ui.bottom.attached.message {
-  margin-top: -1px;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset, 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.bottom.attached.message:not(:last-child) {
-  margin-bottom: 1em;
-}
-
-.ui.attached.icon.message {
-  width: auto;
-}
-
-/*--------------
-        Icon
-  ---------------*/
-
-.ui.icon.message {
-  display: flex;
-  width: 100%;
-  align-items: center;
-}
-
-.ui.icon.message > i.icon:not(.close) {
-  display: block;
-  flex: 0 0 auto;
-  width: auto;
-  line-height: 1;
-  vertical-align: middle;
-  font-size: 3em;
-  opacity: 0.8;
-}
-
-.ui.icon.message > .content {
-  display: block;
-  flex: 1 1 auto;
-  vertical-align: middle;
-}
-
-.ui.icon.message > i.icon:not(.close) + .content {
-  padding-left: 0;
-}
-
-.ui.icon.message > i.circular.icon {
-  width: 1em;
-}
-
-/*--------------
-      Floating
-  ---------------*/
-
-.ui.floating.message {
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-/*--------------
-     Colors
----------------*/
-
-/*--------------
-     Types
----------------*/
-
-.ui.positive.message {
-  background-color: #FCFFF5;
-  color: #2C662D;
-}
-
-.ui.positive.message,
-.ui.attached.positive.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.positive.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.positive.message .header {
-  color: #1A531B;
-}
-
-.ui.negative.message {
-  background-color: #FFF6F6;
-  color: #9F3A38;
-}
-
-.ui.negative.message,
-.ui.attached.negative.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.negative.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.negative.message .header {
-  color: #912D2B;
-}
-
-.ui.info.message {
-  background-color: #F8FFFF;
-  color: #276F86;
-}
-
-.ui.info.message,
-.ui.attached.info.message {
-  box-shadow: 0 0 0 1px #A9D5DE inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.info.message {
-  box-shadow: 0 0 0 1px #A9D5DE inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.info.message .header {
-  color: #0E566C;
-}
-
-.ui.warning.message {
-  background-color: #FFFAF3;
-  color: #573A08;
-}
-
-.ui.warning.message,
-.ui.attached.warning.message {
-  box-shadow: 0 0 0 1px #C9BA9B inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.warning.message {
-  box-shadow: 0 0 0 1px #C9BA9B inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.warning.message .header {
-  color: #794B02;
-}
-
-.ui.error.message {
-  background-color: #FFF6F6;
-  color: #9F3A38;
-}
-
-.ui.error.message,
-.ui.attached.error.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.error.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.error.message .header {
-  color: #912D2B;
-}
-
-.ui.success.message {
-  background-color: #FCFFF5;
-  color: #2C662D;
-}
-
-.ui.success.message,
-.ui.attached.success.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.success.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.success.message .header {
-  color: #1A531B;
-}
-
-.ui.primary.message {
-  background-color: #DFF0FF;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.primary.message,
-.ui.attached.primary.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.primary.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.primary.message .header {
-  color: rgba(242, 242, 242, 0.9);
-}
-
-.ui.secondary.message {
-  background-color: #F4F4F4;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.secondary.message,
-.ui.attached.secondary.message {
-  box-shadow: 0 0 0 1px #1B1C1D inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.secondary.message {
-  box-shadow: 0 0 0 1px #1B1C1D inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.secondary.message .header {
-  color: rgba(242, 242, 242, 0.9);
-}
-
-.ui.red.message {
-  background-color: #FFE8E6;
-  color: #DB2828;
-}
-
-.ui.red.message,
-.ui.attached.red.message {
-  box-shadow: 0 0 0 1px #DB2828 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.red.message {
-  box-shadow: 0 0 0 1px #DB2828 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.red.message .header {
-  color: #c82121;
-}
-
-.ui.orange.message {
-  background-color: #FFEDDE;
-  color: #F2711C;
-}
-
-.ui.orange.message,
-.ui.attached.orange.message {
-  box-shadow: 0 0 0 1px #F2711C inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.orange.message {
-  box-shadow: 0 0 0 1px #F2711C inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.orange.message .header {
-  color: #e7640d;
-}
-
-.ui.yellow.message {
-  background-color: #FFF8DB;
-  color: #B58105;
-}
-
-.ui.yellow.message,
-.ui.attached.yellow.message {
-  box-shadow: 0 0 0 1px #B58105 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.yellow.message {
-  box-shadow: 0 0 0 1px #B58105 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.yellow.message .header {
-  color: #9c6f04;
-}
-
-.ui.olive.message {
-  background-color: #FBFDEF;
-  color: #8ABC1E;
-}
-
-.ui.olive.message,
-.ui.attached.olive.message {
-  box-shadow: 0 0 0 1px #8ABC1E inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.olive.message {
-  box-shadow: 0 0 0 1px #8ABC1E inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.olive.message .header {
-  color: #7aa61a;
-}
-
-.ui.green.message {
-  background-color: #E5F9E7;
-  color: #1EBC30;
-}
-
-.ui.green.message,
-.ui.attached.green.message {
-  box-shadow: 0 0 0 1px #1EBC30 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.green.message {
-  box-shadow: 0 0 0 1px #1EBC30 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.green.message .header {
-  color: #1aa62a;
-}
-
-.ui.teal.message {
-  background-color: #E1F7F7;
-  color: #10A3A3;
-}
-
-.ui.teal.message,
-.ui.attached.teal.message {
-  box-shadow: 0 0 0 1px #10A3A3 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.teal.message {
-  box-shadow: 0 0 0 1px #10A3A3 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.teal.message .header {
-  color: #0e8c8c;
-}
-
-.ui.blue.message {
-  background-color: #DFF0FF;
-  color: #2185D0;
-}
-
-.ui.blue.message,
-.ui.attached.blue.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.blue.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.blue.message .header {
-  color: #1e77ba;
-}
-
-.ui.violet.message {
-  background-color: #EAE7FF;
-  color: #6435C9;
-}
-
-.ui.violet.message,
-.ui.attached.violet.message {
-  box-shadow: 0 0 0 1px #6435C9 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.violet.message {
-  box-shadow: 0 0 0 1px #6435C9 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.violet.message .header {
-  color: #5a30b5;
-}
-
-.ui.purple.message {
-  background-color: #F6E7FF;
-  color: #A333C8;
-}
-
-.ui.purple.message,
-.ui.attached.purple.message {
-  box-shadow: 0 0 0 1px #A333C8 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.purple.message {
-  box-shadow: 0 0 0 1px #A333C8 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.purple.message .header {
-  color: #922eb4;
-}
-
-.ui.pink.message {
-  background-color: #FFE3FB;
-  color: #E03997;
-}
-
-.ui.pink.message,
-.ui.attached.pink.message {
-  box-shadow: 0 0 0 1px #E03997 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.pink.message {
-  box-shadow: 0 0 0 1px #E03997 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.pink.message .header {
-  color: #dd238b;
-}
-
-.ui.brown.message {
-  background-color: #F1E2D3;
-  color: #A5673F;
-}
-
-.ui.brown.message,
-.ui.attached.brown.message {
-  box-shadow: 0 0 0 1px #A5673F inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.brown.message {
-  box-shadow: 0 0 0 1px #A5673F inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.brown.message .header {
-  color: #935b38;
-}
-
-.ui.grey.message {
-  background-color: #F4F4F4;
-  color: #767676;
-}
-
-.ui.grey.message,
-.ui.attached.grey.message {
-  box-shadow: 0 0 0 1px #767676 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.grey.message {
-  box-shadow: 0 0 0 1px #767676 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.grey.message .header {
-  color: #696969;
-}
-
-.ui.black.message {
-  background-color: #1B1C1D;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.black.message .header {
-  color: rgba(255, 255, 255, 0.9);
-}
-
-/*--------------
-     Sizes
----------------*/
-
-.ui.message {
-  font-size: 1em;
-}
-
-.ui.mini.message {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.message {
-  font-size: 0.85714286em;
-}
-
-.ui.small.message {
-  font-size: 0.92857143em;
-}
-
-.ui.large.message {
-  font-size: 1.14285714em;
-}
-
-.ui.big.message {
-  font-size: 1.28571429em;
-}
-
-.ui.huge.message {
-  font-size: 1.42857143em;
-}
-
-.ui.massive.message {
-  font-size: 1.71428571em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-        Site Overrides
-*******************************/
 /*!
  * # Fomantic-UI - Modal
  * http://github.com/fomantic/Fomantic-UI/

From ed02d1fab85c9b8206c0af84dcfc3792e61609cf Mon Sep 17 00:00:00 2001
From: norohind <60548839+norohind@users.noreply.github.com>
Date: Sun, 17 Mar 2024 06:56:49 +0300
Subject: [PATCH 409/679] Fix PR creation via api between branches of same repo
 with head field namespaced (#26986)

Fix #20175

Current implementation of API does not allow creating pull requests
between branches of the same
repo when you specify *namespace* (owner of the repo) in `head` field in
http request body.

---

Although GitHub implementation of API allows performing such action and
since Gitea targeting
compatibility with GitHub API I see it as an appropriate change.

I'm proposing a fix to the described problem and test case which covers
this logic.

My use-case just in case:
https://github.com/go-gitea/gitea/issues/20175#issuecomment-1711283022
---
 routers/api/v1/repo/pull.go        |  2 ++
 tests/integration/api_pull_test.go | 17 +++++++++++++++++
 2 files changed, 19 insertions(+)

diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 4cb94b11a2..fc47656072 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -1068,6 +1068,8 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 			return nil, nil, nil, nil, "", ""
 		}
 		headBranch = headInfos[1]
+		// The head repository can also point to the same repo
+		isSameRepo = ctx.Repo.Owner.ID == headUser.ID
 
 	} else {
 		ctx.NotFound()
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
index 92931c5699..bb479caf89 100644
--- a/tests/integration/api_pull_test.go
+++ b/tests/integration/api_pull_test.go
@@ -126,6 +126,23 @@ func TestAPICreatePullSuccess(t *testing.T) {
 	MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
 }
 
+func TestAPICreatePullSameRepoSuccess(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+	session := loginUser(t, owner.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner.Name, repo.Name), &api.CreatePullRequestOption{
+		Head:  fmt.Sprintf("%s:pr-to-update", owner.Name),
+		Base:  "master",
+		Title: "successfully create a PR between branches of the same repository",
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusCreated)
+	MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
+}
+
 func TestAPICreatePullWithFieldsSuccess(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	// repo10 have code, pulls units.

From c20b56815d4f27ffd1b457ec238e494adc5fba81 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 17 Mar 2024 10:02:22 +0100
Subject: [PATCH 410/679] Fix semantic.json (#29860)

Followup https://github.com/go-gitea/gitea/pull/29856
---
 web_src/fomantic/semantic.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 43d0b412b3..6e2facf822 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -34,7 +34,6 @@
     "label",
     "list",
     "menu",
-    "message",
     "modal",
     "search",
     "segment",

From 4b1c88628a6856e533ff10d346ca5bd73ce952b3 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 17 Mar 2024 11:04:59 +0100
Subject: [PATCH 411/679] Load citation JS only when needed (#29855)

Previously, the citation js would load every time when opening a citable
repo. Now it only loads when the user clicks the button for it. The
loading state is representend with a spinner on the button:

<img width="83" alt="Screenshot 2024-03-17 at 00 25 13"
src="https://github.com/go-gitea/gitea/assets/115237/29649089-13f3-4974-ab81-e12c0f8e651f">

Diff ist best viewed with whitespace hidden.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/modules/animations.css |  4 +++
 web_src/js/features/citation.js    | 45 +++++++++++++++++-------------
 2 files changed, 30 insertions(+), 19 deletions(-)

diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index d5ddc772f6..5bfc090773 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -13,6 +13,10 @@
   opacity: 0.3;
 }
 
+.button.is-loading > * {
+  opacity: 0;
+}
+
 .is-loading::after {
   content: "";
   position: absolute;
diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js
index 61f378f0f2..49992b225f 100644
--- a/web_src/js/features/citation.js
+++ b/web_src/js/features/citation.js
@@ -40,28 +40,35 @@ export async function initCitationFileCopyContent() {
     $citationCopyApa.toggleClass('primary', !isBibtex);
   };
 
-  try {
-    await initInputCitationValue($citationCopyApa, $citationCopyBibtex);
-  } catch (e) {
-    console.error(`initCitationFileCopyContent error: ${e}`, e);
-    return;
-  }
-  updateUi();
+  $('#cite-repo-button').on('click', async (e) => {
+    const dropdownBtn = e.target.closest('.ui.dropdown.button');
+    dropdownBtn.classList.add('is-loading');
 
-  $citationCopyApa.on('click', () => {
-    localStorage.setItem('citation-copy-format', 'apa');
-    updateUi();
-  });
-  $citationCopyBibtex.on('click', () => {
-    localStorage.setItem('citation-copy-format', 'bibtex');
-    updateUi();
-  });
+    try {
+      try {
+        await initInputCitationValue($citationCopyApa, $citationCopyBibtex);
+      } catch (e) {
+        console.error(`initCitationFileCopyContent error: ${e}`, e);
+        return;
+      }
+      updateUi();
 
-  $inputContent.on('click', () => {
-    $inputContent.trigger('select');
-  });
+      $citationCopyApa.on('click', () => {
+        localStorage.setItem('citation-copy-format', 'apa');
+        updateUi();
+      });
+      $citationCopyBibtex.on('click', () => {
+        localStorage.setItem('citation-copy-format', 'bibtex');
+        updateUi();
+      });
+
+      $inputContent.on('click', () => {
+        $inputContent.trigger('select');
+      });
+    } finally {
+      dropdownBtn.classList.remove('is-loading');
+    }
 
-  $('#cite-repo-button').on('click', () => {
     $('#cite-repo-modal').modal('show');
   });
 }

From a228656e3d318ffd871b414578f0b83aa5a65878 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 17 Mar 2024 13:14:14 +0100
Subject: [PATCH 412/679] Simplify README (#29827)

Came to the conclusion that a simple format Readme is easier to read
than the previous fancy centered stuff.

---------

Co-authored-by: Yarden Shoham <git@yardenshoham.com>
---
 README.md    | 64 ++++++++++++----------------------------------------
 README_ZH.md | 64 ++++++++++++----------------------------------------
 2 files changed, 28 insertions(+), 100 deletions(-)

diff --git a/README.md b/README.md
index 94d7284c7c..90474cbd94 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,19 @@
-<p align="center">
-  <a href="https://gitea.io/">
-    <img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/>
-  </a>
-</p>
-<h1 align="center">Gitea - Git with a cup of tea</h1>
+# Gitea
 
-<p align="center">
-  <a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly">
-    <img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main">
-  </a>
-  <a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea">
-    <img src="https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2">
-  </a>
-  <a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov">
-    <img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg">
-  </a>
-  <a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card">
-    <img src="https://goreportcard.com/badge/code.gitea.io/gitea">
-  </a>
-  <a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc">
-    <img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg">
-  </a>
-  <a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release">
-    <img src="https://img.shields.io/github/release/go-gitea/gitea.svg">
-  </a>
-  <a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source">
-    <img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg">
-  </a>
-  <a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea">
-    <img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen">
-  </a>
-  <a href="https://opensource.org/licenses/MIT" title="License: MIT">
-    <img src="https://img.shields.io/badge/License-MIT-blue.svg">
-  </a>
-  <a href="https://gitpod.io/#https://github.com/go-gitea/gitea">
-  <img
-    src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod"
-    alt="Contribute with Gitpod"
-  />
-  </a>
-  <a href="https://crowdin.com/project/gitea" title="Crowdin">
-    <img src="https://badges.crowdin.net/gitea/localized.svg">
-  </a>
-  <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs">
-    <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main">
-  </a>
-</p>
+[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
+[![](https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg)](https://app.codecov.io/gh/go-gitea/gitea "Codecov")
+[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
+[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
+[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
+[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
+[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
+[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
+[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/go-gitea/gitea)
+[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin")
+[![](https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main)](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs")
 
-<p align="center">
-  <a href="README_ZH.md">View this document in Chinese</a>
-</p>
+[View this document in Chinese](./README_ZH.md)
 
 ## Purpose
 
diff --git a/README_ZH.md b/README_ZH.md
index adfeb9a8df..deebb40cfb 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -1,55 +1,19 @@
-<p align="center">
-  <a href="https://gitea.io/">
-    <img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/>
-  </a>
-</p>
-<h1 align="center">Gitea - Git with a cup of tea</h1>
+# Gitea
 
-<p align="center">
-  <a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly">
-    <img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main">
-  </a>
-  <a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea">
-    <img src="https://img.shields.io/discord/322538954119184384.svg">
-  </a>
-  <a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov">
-    <img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg">
-  </a>
-  <a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card">
-    <img src="https://goreportcard.com/badge/code.gitea.io/gitea">
-  </a>
-  <a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc">
-    <img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg">
-  </a>
-  <a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release">
-    <img src="https://img.shields.io/github/release/go-gitea/gitea.svg">
-  </a>
-  <a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source">
-    <img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg">
-  </a>
-  <a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea">
-    <img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen">
-  </a>
-  <a href="https://opensource.org/licenses/MIT" title="License: MIT">
-    <img src="https://img.shields.io/badge/License-MIT-blue.svg">
-  </a>
-  <a href="https://gitpod.io/#https://github.com/go-gitea/gitea">
-  <img
-    src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod"
-    alt="Contribute with Gitpod"
-  />
-  </a>
-  <a href="https://crowdin.com/project/gitea" title="Crowdin">
-    <img src="https://badges.crowdin.net/gitea/localized.svg">
-  </a>
-  <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs">
-    <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main">
-  </a>
-</p>
+[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
+[![](https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg)](https://app.codecov.io/gh/go-gitea/gitea "Codecov")
+[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
+[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
+[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
+[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
+[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
+[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
+[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/go-gitea/gitea)
+[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin")
+[![](https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main)](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs")
 
-<p align="center">
-  <a href="README.md">View this document in English</a>
-</p>
+[View this document in English](./README.md)
 
 ## 目标
 

From 673286d8c8a00bf7240a93187d767fb5a5e32a31 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 17 Mar 2024 20:40:42 +0800
Subject: [PATCH 413/679] Refactor clone-panel styles (#29861)

1. The borders were doubled on the "empty" page, fix it.
2. Remove unnecessary CSS classes like "clone", "compact", etc
3. Use CSS class "clone-panel" instead of ID "clone-panel"
4. Use `tw-flex-1` instead of `gt-f1`
5. Remove unnecessary ID "more-btn"
---
 templates/repo/clone_buttons.tmpl |  6 +++---
 templates/repo/empty.tmpl         |  2 +-
 templates/repo/home.tmpl          |  4 ++--
 templates/repo/wiki/revision.tmpl |  2 +-
 templates/repo/wiki/view.tmpl     |  2 +-
 web_src/css/repo.css              | 32 +++++++++----------------------
 web_src/css/repo/wiki.css         |  2 +-
 7 files changed, 18 insertions(+), 32 deletions(-)

diff --git a/templates/repo/clone_buttons.tmpl b/templates/repo/clone_buttons.tmpl
index a664c4bda8..89daba9dc9 100644
--- a/templates/repo/clone_buttons.tmpl
+++ b/templates/repo/clone_buttons.tmpl
@@ -1,15 +1,15 @@
 <!-- there is always at least one button (by context/repo.go) -->
 {{if $.CloneButtonShowHTTPS}}
-	<button class="ui small compact clone button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">
+	<button class="ui small button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">
 		HTTPS
 	</button>
 {{end}}
 {{if $.CloneButtonShowSSH}}
-	<button class="ui small compact clone button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">
+	<button class="ui small button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">
 		SSH
 	</button>
 {{end}}
 <input id="repo-clone-url" size="20" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
-<button class="ui basic small compact icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}">
+<button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}">
 	{{svg "octicon-copy" 14}}
 </button>
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index a858a728e9..d3665a9f8b 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -37,7 +37,7 @@
 									</a>
 									{{end}}
 								{{end}}
-								<div class="ui action small input gt-df gt-f1">
+								<div class="clone-panel ui action small input tw-flex-1">
 									{{template "repo/clone_buttons" .}}
 								</div>
 							</div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index ef10904bcc..24bafb8d9d 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -124,9 +124,9 @@
 			<div class="gt-df gt-ac">
 				<!-- Only show clone panel in repository home page -->
 				{{if eq $n 0}}
-					<div class="ui action tiny input" id="clone-panel">
+					<div class="clone-panel ui action tiny input">
 						{{template "repo/clone_buttons" .}}
-						<button id="more-btn" class="ui basic small compact jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
+						<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
 							{{svg "octicon-kebab-horizontal"}}
 							<div class="menu">
 								{{if not $.DisableDownloadSourceArchives}}
diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
index 647c331d55..182635e011 100644
--- a/templates/repo/wiki/revision.tmpl
+++ b/templates/repo/wiki/revision.tmpl
@@ -15,7 +15,7 @@
 				</div>
 			</div>
 			<div class="ui eight wide column text right">
-				<div class="ui action small input" id="clone-panel">
+				<div class="clone-panel ui action small input">
 					{{template "repo/clone_buttons" .}}
 					{{template "repo/clone_script" .}}
 				</div>
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 19da3fd199..fefa9c589e 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -28,7 +28,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="ui action small input gt-df gt-ac" id="clone-panel">
+			<div class="clone-panel ui action small input">
 				{{template "repo/clone_buttons" .}}
 				{{template "repo/clone_script" .}}
 			</div>
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 848eb53327..4503bd69e3 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -143,41 +143,31 @@
   margin-bottom: 12px;
 }
 
-.repository #clone-panel #repo-clone-url {
+.repository .clone-panel #repo-clone-url {
   width: 320px;
   border-radius: 0;
 }
 
-@media (min-width: 768px) and (max-width: 991.98px) {
-  .repository #clone-panel #repo-clone-url {
+@media (max-width: 991.98px) {
+  .repository .clone-panel #repo-clone-url {
     width: 200px;
   }
 }
 
-@media (max-width: 767.98px) {
-  .repository #clone-panel #repo-clone-url {
-    width: 200px;
-  }
+.repository .ui.action.input.clone-panel > button + button,
+.repository .ui.action.input.clone-panel > button + input {
+  margin-left: -1px; /* make the borders overlap to avoid double borders */
 }
 
-.repository #clone-panel #repo-clone-https,
-.repository #clone-panel #repo-clone-ssh {
-  border-right: none;
-}
-
-.repository #clone-panel #more-btn {
-  border-left: none;
-}
-
-.repository #clone-panel button:first-of-type {
+.repository .clone-panel > button:first-of-type {
   border-radius: var(--border-radius) 0 0 var(--border-radius) !important;
 }
 
-.repository #clone-panel button:last-of-type {
+.repository .clone-panel > button:last-of-type {
   border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
 }
 
-.repository #clone-panel .dropdown .menu {
+.repository .clone-panel .dropdown .menu {
   right: 0 !important;
   left: auto !important;
 }
@@ -1759,10 +1749,6 @@
   font-weight: var(--font-weight-normal);
 }
 
-.repository.quickstart .guide .clone.button:first-child {
-  border-radius: var(--border-radius) 0 0 var(--border-radius);
-}
-
 .repository.quickstart .guide #repo-clone-url {
   border-radius: 0;
   padding: 5px 10px;
diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css
index 1302e9cb5c..bb6f364557 100644
--- a/web_src/css/repo/wiki.css
+++ b/web_src/css/repo/wiki.css
@@ -58,7 +58,7 @@
 }
 
 @media (max-width: 767.98px) {
-  .repository.wiki #clone-panel #repo-clone-url {
+  .repository.wiki .clone-panel #repo-clone-url {
     width: 160px;
   }
   .repository.wiki .wiki-content-main.with-sidebar,

From 33973ac567d6681bda26d82f26b7294a297c693f Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 17 Mar 2024 13:50:32 +0100
Subject: [PATCH 414/679] Avoid JS error on issue/pr list when logged out
 (#29854)

When logged out, the checkboxes are not there on the issue/pr lists,
which would cause an error here.

Fixes: https://github.com/go-gitea/gitea/issues/29862

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 web_src/js/features/repo-issue-list.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 48b1555c89..880ecf9489 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -9,6 +9,7 @@ import {DELETE, POST} from '../modules/fetch.js';
 
 function initRepoIssueListCheckboxes() {
   const issueSelectAll = document.querySelector('.issue-checkbox-all');
+  if (!issueSelectAll) return; // logged out state
   const issueCheckboxes = document.querySelectorAll('.issue-checkbox');
 
   const syncIssueSelectionState = () => {

From df05c558da704f0c9c9f11d32bba2a9c1cb2f8a8 Mon Sep 17 00:00:00 2001
From: Nanguan Lin <nanguanlin6@gmail.com>
Date: Sun, 17 Mar 2024 21:24:45 +0800
Subject: [PATCH 415/679] Fix user id column case (#29863)

Sometimes the column name is case-sensitive and it may cause 500.
---
 models/user/email_address.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/models/user/email_address.go b/models/user/email_address.go
index a9dbb8e891..d26549f383 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -434,7 +434,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
 		cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()})
 	}
 
-	count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid").
+	count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.id = email_address.uid").
 		Where(cond).Count(new(EmailAddress))
 	if err != nil {
 		return nil, 0, fmt.Errorf("Count: %w", err)
@@ -450,7 +450,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
 	emails := make([]*SearchEmailResult, 0, opts.PageSize)
 	err = db.GetEngine(ctx).Table("email_address").
 		Select("email_address.*, `user`.name, `user`.full_name").
-		Join("INNER", "`user`", "`user`.ID = email_address.uid").
+		Join("INNER", "`user`", "`user`.id = email_address.uid").
 		Where(cond).
 		OrderBy(orderby).
 		Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).

From 0285b04f4ca981d7467097dbca3b281011b7798c Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Sun, 17 Mar 2024 15:11:28 +0100
Subject: [PATCH 416/679] fix telegram webhook (#29864)

Fix #29837 which is a regression caused by

https://github.com/go-gitea/gitea/pull/29145/files#diff-731445ee00f0f1bf2ff731f4f96ddcf51cdc53fd2faaf406eb3536fc292ea748L48.

The line was probably removed by accident.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 services/webhook/telegram.go      | 4 +++-
 services/webhook/telegram_test.go | 9 +++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index e4a5b5a424..c2b4820032 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -181,7 +181,9 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro
 
 func createTelegramPayload(message string) TelegramPayload {
 	return TelegramPayload{
-		Message: strings.TrimSpace(message),
+		Message:           strings.TrimSpace(message),
+		ParseMode:         "HTML",
+		DisableWebPreview: true,
 	}
 }
 
diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go
index 27ab96cd09..2fe5161b22 100644
--- a/services/webhook/telegram_test.go
+++ b/services/webhook/telegram_test.go
@@ -18,6 +18,15 @@ import (
 
 func TestTelegramPayload(t *testing.T) {
 	tc := telegramConvertor{}
+
+	t.Run("Correct webhook params", func(t *testing.T) {
+		p := createTelegramPayload("testMsg ")
+
+		assert.Equal(t, "HTML", p.ParseMode)
+		assert.Equal(t, true, p.DisableWebPreview)
+		assert.Equal(t, "testMsg", p.Message)
+	})
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 

From 099052aba5bfa3a0a700d88d2e2891ae99f151ce Mon Sep 17 00:00:00 2001
From: Nanguan Lin <nanguanlin6@gmail.com>
Date: Sun, 17 Mar 2024 22:50:32 +0800
Subject: [PATCH 417/679] Fix the wrong locale key of searching users (#29868)

regression of #29530
I guess it's because the user-blocking feature is committed after that
locale clean PR.
---
 templates/shared/user/blocked_users.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/shared/user/blocked_users.tmpl b/templates/shared/user/blocked_users.tmpl
index b2f0957691..071c7da11c 100644
--- a/templates/shared/user/blocked_users.tmpl
+++ b/templates/shared/user/blocked_users.tmpl
@@ -17,7 +17,7 @@
 		{{.CsrfTokenHtml}}
 		<input type="hidden" name="action" value="block" />
 		<div id="search-user-box" class="field ui fluid search input">
-			<input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
+			<input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
 			<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
 		</div>
 		<div class="field">

From abb330e6139c37f3c1d84b30625c1f5495a4eb49 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 17 Mar 2024 23:40:05 +0800
Subject: [PATCH 418/679] Upgrade Go 1.22 and upgrade dependency (#29869)

---
 assets/go-licenses.json                   |  21 +-
 go.mod                                    | 153 ++---
 go.sum                                    | 663 ++++++----------------
 routers/api/actions/ping/ping.go          |   6 +-
 routers/api/actions/ping/ping_test.go     |   2 +-
 routers/api/actions/runner/interceptor.go |   2 +-
 routers/api/actions/runner/runner.go      |   6 +-
 services/webhook/deliver_test.go          |   1 -
 8 files changed, 265 insertions(+), 589 deletions(-)

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 2aa60780c4..be9022b694 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -24,11 +24,21 @@
     "path": "codeberg.org/gusted/mcaptcha/LICENSE",
     "licenseText": "Copyright © 2022 William Zijl\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
   },
+  {
+    "name": "connectrpc.com/connect",
+    "path": "connectrpc.com/connect/LICENSE",
+    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2021-2024 The Connect Authors\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
+  },
   {
     "name": "dario.cat/mergo",
     "path": "dario.cat/mergo/LICENSE",
     "licenseText": "Copyright (c) 2013 Dario Castañé. All rights reserved.\nCopyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
   },
+  {
+    "name": "filippo.io/edwards25519",
+    "path": "filippo.io/edwards25519/LICENSE",
+    "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
   {
     "name": "git.sr.ht/~mariusor/go-xsd-duration",
     "path": "git.sr.ht/~mariusor/go-xsd-duration/LICENSE",
@@ -234,11 +244,6 @@
     "path": "github.com/bradfitz/gomemcache/memcache/LICENSE",
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
-  {
-    "name": "github.com/bufbuild/connect-go",
-    "path": "github.com/bufbuild/connect-go/LICENSE",
-    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2021-2022 Buf Technologies, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
   {
     "name": "github.com/buildkite/terminal-to-html/v3",
     "path": "github.com/buildkite/terminal-to-html/v3/LICENSE",
@@ -535,8 +540,8 @@
     "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
   {
-    "name": "github.com/golang/protobuf",
-    "path": "github.com/golang/protobuf/LICENSE",
+    "name": "github.com/golang/protobuf/proto",
+    "path": "github.com/golang/protobuf/proto/LICENSE",
     "licenseText": "Copyright 2010 The Go Authors.  All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n    * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
   },
   {
@@ -1066,7 +1071,7 @@
   },
   {
     "name": "go.uber.org/zap",
-    "path": "go.uber.org/zap/LICENSE.txt",
+    "path": "go.uber.org/zap/LICENSE",
     "licenseText": "Copyright (c) 2016-2017 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
   },
   {
diff --git a/go.mod b/go.mod
index 97f429aadb..b76eb74876 100644
--- a/go.mod
+++ b/go.mod
@@ -1,27 +1,27 @@
 module code.gitea.io/gitea
 
-go 1.21
+go 1.22
 
 require (
-	code.gitea.io/actions-proto-go v0.3.1
+	code.gitea.io/actions-proto-go v0.4.0
 	code.gitea.io/gitea-vet v0.2.3
 	code.gitea.io/sdk/gitea v0.17.1
 	codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
-	gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669
+	connectrpc.com/connect v1.15.0
+	gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028
 	gitea.com/go-chi/cache v0.2.0
-	gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384
-	gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3
+	gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
+	gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96
 	gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
 	gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
 	github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
 	github.com/NYTimes/gziphandler v1.1.1
-	github.com/PuerkitoBio/goquery v1.8.1
+	github.com/PuerkitoBio/goquery v1.9.1
 	github.com/alecthomas/chroma/v2 v2.13.0
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
 	github.com/blevesearch/bleve/v2 v2.3.10
-	github.com/bufbuild/connect-go v1.10.0
-	github.com/buildkite/terminal-to-html/v3 v3.10.1
+	github.com/buildkite/terminal-to-html/v3 v3.11.0
 	github.com/caddyserver/certmagic v0.20.0
 	github.com/chi-middleware/proxy v1.1.1
 	github.com/denisenkom/go-mssqldb v0.12.3
@@ -30,33 +30,33 @@ require (
 	github.com/djherbis/nio/v3 v3.0.1
 	github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
 	github.com/dustin/go-humanize v1.0.1
-	github.com/editorconfig/editorconfig-core-go/v2 v2.6.0
+	github.com/editorconfig/editorconfig-core-go/v2 v2.6.1
 	github.com/emersion/go-imap v1.2.1
 	github.com/emirpasic/gods v1.18.1
 	github.com/ethantkoenig/rupture v1.0.1
-	github.com/felixge/fgprof v0.9.3
+	github.com/felixge/fgprof v0.9.4
 	github.com/fsnotify/fsnotify v1.7.0
 	github.com/gliderlabs/ssh v0.3.6
-	github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
+	github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225
 	github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
-	github.com/go-chi/chi/v5 v5.0.11
+	github.com/go-chi/chi/v5 v5.0.12
 	github.com/go-chi/cors v1.2.1
 	github.com/go-co-op/gocron v1.37.0
-	github.com/go-enry/go-enry/v2 v2.8.6
+	github.com/go-enry/go-enry/v2 v2.8.7
 	github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
 	github.com/go-git/go-billy/v5 v5.5.0
 	github.com/go-git/go-git/v5 v5.11.0
 	github.com/go-ldap/ldap/v3 v3.4.6
-	github.com/go-sql-driver/mysql v1.7.1
+	github.com/go-sql-driver/mysql v1.8.0
 	github.com/go-swagger/go-swagger v0.30.5
 	github.com/go-testfixtures/testfixtures/v3 v3.10.0
-	github.com/go-webauthn/webauthn v0.10.0
+	github.com/go-webauthn/webauthn v0.10.2
 	github.com/gobwas/glob v0.2.3
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
 	github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
-	github.com/golang-jwt/jwt/v5 v5.2.0
+	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/google/go-github/v57 v57.0.0
-	github.com/google/pprof v0.0.0-20240117000934-35fc243c5815
+	github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/feeds v1.1.2
 	github.com/gorilla/sessions v1.2.2
@@ -64,55 +64,55 @@ require (
 	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/huandu/xstrings v1.4.0
 	github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
-	github.com/jhillyerd/enmime v1.1.0
+	github.com/jhillyerd/enmime v1.2.0
 	github.com/json-iterator/go v1.1.12
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
 	github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
-	github.com/klauspost/compress v1.17.4
-	github.com/klauspost/cpuid/v2 v2.2.6
+	github.com/klauspost/compress v1.17.7
+	github.com/klauspost/cpuid/v2 v2.2.7
 	github.com/lib/pq v1.10.9
-	github.com/markbates/goth v1.78.0
+	github.com/markbates/goth v1.79.0
 	github.com/mattn/go-isatty v0.0.20
 	github.com/mattn/go-sqlite3 v1.14.22
-	github.com/meilisearch/meilisearch-go v0.26.1
+	github.com/meilisearch/meilisearch-go v0.26.2
 	github.com/mholt/archiver/v3 v3.5.1
 	github.com/microcosm-cc/bluemonday v1.0.26
-	github.com/minio/minio-go/v7 v7.0.66
+	github.com/minio/minio-go/v7 v7.0.69
 	github.com/msteinert/pam v1.2.0
 	github.com/nektos/act v0.2.52
 	github.com/niklasfasching/go-org v1.7.0
 	github.com/olivere/elastic/v7 v7.0.32
 	github.com/opencontainers/go-digest v1.0.0
-	github.com/opencontainers/image-spec v1.1.0-rc6
+	github.com/opencontainers/image-spec v1.1.0
 	github.com/pkg/errors v0.9.1
 	github.com/pquerna/otp v1.4.0
-	github.com/prometheus/client_golang v1.18.0
+	github.com/prometheus/client_golang v1.19.0
 	github.com/quasoft/websspi v1.1.2
-	github.com/redis/go-redis/v9 v9.4.0
+	github.com/redis/go-redis/v9 v9.5.1
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
-	github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd
+	github.com/sassoftware/go-rpmutils v0.3.0
 	github.com/sergi/go-diff v1.3.1
 	github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	github.com/syndtr/goleveldb v1.0.0
 	github.com/tstranex/u2f v1.0.0
 	github.com/ulikunitz/xz v0.5.11
 	github.com/urfave/cli/v2 v2.27.1
-	github.com/xanzy/go-gitlab v0.96.0
+	github.com/xanzy/go-gitlab v0.100.0
 	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/yohcop/openid-go v1.0.1
-	github.com/yuin/goldmark v1.6.0
+	github.com/yuin/goldmark v1.7.0
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
 	github.com/yuin/goldmark-meta v1.1.0
-	golang.org/x/crypto v0.18.0
+	golang.org/x/crypto v0.21.0
 	golang.org/x/image v0.15.0
-	golang.org/x/net v0.20.0
-	golang.org/x/oauth2 v0.16.0
-	golang.org/x/sys v0.16.0
+	golang.org/x/net v0.22.0
+	golang.org/x/oauth2 v0.18.0
+	golang.org/x/sys v0.18.0
 	golang.org/x/text v0.14.0
-	golang.org/x/tools v0.17.0
-	google.golang.org/grpc v1.60.1
+	golang.org/x/tools v0.19.0
+	google.golang.org/grpc v1.62.1
 	google.golang.org/protobuf v1.33.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 	gopkg.in/ini.v1 v1.67.0
@@ -120,23 +120,24 @@ require (
 	mvdan.cc/xurls/v2 v2.5.0
 	strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
 	xorm.io/builder v0.3.13
-	xorm.io/xorm v1.3.7
+	xorm.io/xorm v1.3.8
 )
 
 require (
-	cloud.google.com/go/compute v1.23.3 // indirect
+	cloud.google.com/go/compute v1.25.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	dario.cat/mergo v1.0.0 // indirect
+	filippo.io/edwards25519 v1.1.0 // indirect
 	git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
-	github.com/ClickHouse/ch-go v0.61.1 // indirect
-	github.com/ClickHouse/clickhouse-go/v2 v2.18.0 // indirect
+	github.com/ClickHouse/ch-go v0.61.5 // indirect
+	github.com/ClickHouse/clickhouse-go/v2 v2.22.0 // indirect
 	github.com/DataDog/zstd v1.5.5 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.2.1 // indirect
 	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
 	github.com/Microsoft/go-winio v0.6.1 // indirect
 	github.com/ProtonMail/go-crypto v1.0.0 // indirect
-	github.com/RoaringBitmap/roaring v1.7.0 // indirect
+	github.com/RoaringBitmap/roaring v1.9.0 // indirect
 	github.com/andybalholm/brotli v1.1.0 // indirect
 	github.com/andybalholm/cascadia v1.3.2 // indirect
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
@@ -144,12 +145,12 @@ require (
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bits-and-blooms/bitset v1.13.0 // indirect
-	github.com/blevesearch/bleve_index_api v1.1.5 // indirect
-	github.com/blevesearch/geo v0.1.19 // indirect
+	github.com/blevesearch/bleve_index_api v1.1.6 // indirect
+	github.com/blevesearch/geo v0.1.20 // indirect
 	github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
 	github.com/blevesearch/gtreap v0.1.1 // indirect
 	github.com/blevesearch/mmap-go v1.0.4 // indirect
-	github.com/blevesearch/scorch_segment_api/v2 v2.2.6 // indirect
+	github.com/blevesearch/scorch_segment_api/v2 v2.2.8 // indirect
 	github.com/blevesearch/segment v0.9.1 // indirect
 	github.com/blevesearch/snowballstem v0.9.0 // indirect
 	github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
@@ -165,7 +166,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/cloudflare/circl v1.3.7 // indirect
 	github.com/couchbase/go-couchbase v0.1.1 // indirect
-	github.com/couchbase/gomemcached v0.3.0 // indirect
+	github.com/couchbase/gomemcached v0.3.1 // indirect
 	github.com/couchbase/goutils v0.1.2 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
@@ -176,32 +177,32 @@ require (
 	github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
 	github.com/fatih/color v1.16.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/fxamacker/cbor/v2 v2.5.0 // indirect
-	github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
+	github.com/fxamacker/cbor/v2 v2.6.0 // indirect
+	github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect
 	github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
 	github.com/go-enry/go-oniguruma v1.2.1 // indirect
 	github.com/go-faster/city v1.0.1 // indirect
 	github.com/go-faster/errors v0.7.1 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
-	github.com/go-openapi/analysis v0.22.2 // indirect
-	github.com/go-openapi/errors v0.21.0 // indirect
-	github.com/go-openapi/inflect v0.19.0 // indirect
-	github.com/go-openapi/jsonpointer v0.20.2 // indirect
-	github.com/go-openapi/jsonreference v0.20.4 // indirect
-	github.com/go-openapi/loads v0.21.5 // indirect
-	github.com/go-openapi/runtime v0.26.2 // indirect
-	github.com/go-openapi/spec v0.20.14 // indirect
-	github.com/go-openapi/strfmt v0.22.0 // indirect
-	github.com/go-openapi/swag v0.22.7 // indirect
-	github.com/go-openapi/validate v0.22.6 // indirect
-	github.com/go-webauthn/x v0.1.6 // indirect
+	github.com/go-openapi/analysis v0.23.0 // indirect
+	github.com/go-openapi/errors v0.22.0 // indirect
+	github.com/go-openapi/inflect v0.21.0 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/go-openapi/loads v0.22.0 // indirect
+	github.com/go-openapi/runtime v0.28.0 // indirect
+	github.com/go-openapi/spec v0.21.0 // indirect
+	github.com/go-openapi/strfmt v0.23.0 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-openapi/validate v0.24.0 // indirect
+	github.com/go-webauthn/x v0.1.9 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
 	github.com/golang-sql/sqlexp v0.1.0 // indirect
 	github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/go-tpm v0.9.0 // indirect
@@ -246,11 +247,11 @@ require (
 	github.com/pierrec/lz4/v4 v4.1.21 // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/client_model v0.5.0 // indirect
-	github.com/prometheus/common v0.46.0 // indirect
-	github.com/prometheus/procfs v0.12.0 // indirect
-	github.com/rhysd/actionlint v1.6.26 // indirect
-	github.com/rivo/uniseg v0.4.4 // indirect
+	github.com/prometheus/client_model v0.6.0 // indirect
+	github.com/prometheus/common v0.50.0 // indirect
+	github.com/prometheus/procfs v0.13.0 // indirect
+	github.com/rhysd/actionlint v1.6.27 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/rogpeppe/go-internal v1.12.0 // indirect
 	github.com/rs/xid v1.5.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -260,7 +261,7 @@ require (
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
-	github.com/skeema/knownhosts v1.2.1 // indirect
+	github.com/skeema/knownhosts v1.2.2 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.11.0 // indirect
 	github.com/spf13/cast v1.6.0 // indirect
@@ -271,28 +272,28 @@ require (
 	github.com/toqueteos/webbrowser v1.2.0 // indirect
 	github.com/unknwon/com v1.0.1 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasthttp v1.51.0 // indirect
+	github.com/valyala/fasthttp v1.52.0 // indirect
 	github.com/valyala/fastjson v1.6.4 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
-	github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
+	github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
-	go.etcd.io/bbolt v1.3.8 // indirect
-	go.mongodb.org/mongo-driver v1.13.1 // indirect
-	go.opentelemetry.io/otel v1.22.0 // indirect
-	go.opentelemetry.io/otel/trace v1.22.0 // indirect
+	go.etcd.io/bbolt v1.3.9 // indirect
+	go.mongodb.org/mongo-driver v1.14.0 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	go.uber.org/zap v1.26.0 // indirect
-	golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
-	golang.org/x/mod v0.14.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
+	golang.org/x/mod v0.16.0 // indirect
 	golang.org/x/sync v0.6.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 916378d759..d82110177c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,64 +1,33 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
-cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
+cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
+cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-code.gitea.io/actions-proto-go v0.3.1 h1:PMyiQtBKb8dNnpEO2R5rcZdXSis+UQZVo/SciMtR1aU=
-code.gitea.io/actions-proto-go v0.3.1/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A=
+code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU=
+code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
 code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
 code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
 code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8=
 code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM=
 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
+connectrpc.com/connect v1.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo=
+connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA=
 dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
 dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
 git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
 gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw=
 gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8=
-gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo=
-gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc=
-gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0=
+gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028 h1:6/QAx4+s0dyRwdaTFPTnhGppuiuu0OqxIH9szyTpvKw=
+gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
 gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w=
 gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE=
-gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 h1:klh0LjhH7l4CuJkxlCM//o3rWLvWqxUpFxEtoYg5TNY=
-gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384/go.mod h1:hQ9SYHKdOX968wJglb/NMQ+UqpOKwW4L+EYdvkWjHSo=
-gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 h1:4FuO+MahrkDjdjVIS8ExmY9FEHTZS8TPheEm4uU5xLI=
-gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3/go.mod h1:fc/pjt5EqNKgqQXYzcas1Z5L5whkZHyOvTA7OzWVJck=
+gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
+gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
+gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q=
+gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
@@ -75,12 +44,10 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2
 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/ClickHouse/ch-go v0.61.1 h1:j5rx3qnvcnYjhnP1IdXE/vdIRQiqgwAzyqOaasA6QCw=
-github.com/ClickHouse/ch-go v0.61.1/go.mod h1:myxt/JZgy2BYHFGQqzmaIpbfr5CMbs3YHVULaWQj5YU=
-github.com/ClickHouse/clickhouse-go/v2 v2.18.0 h1:O1LicIeg2JS2V29fKRH4+yT3f6jvvcJBm506dpVQ4mQ=
-github.com/ClickHouse/clickhouse-go/v2 v2.18.0/go.mod h1:ztQvX6wm7kAbhJslS87EXEhOVNY/TObXwyURnGju5FQ=
-github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
+github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
+github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
+github.com/ClickHouse/clickhouse-go/v2 v2.22.0 h1:LAdk0qT125PpSPnYepFQs5X5z1EwpAtIX10SUELPgi0=
+github.com/ClickHouse/clickhouse-go/v2 v2.22.0/go.mod h1:tBhdF3f3RdP7sS59+oBAtTyhWpy0024ZxDMhgxra0QE=
 github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
 github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
 github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
@@ -98,12 +65,12 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
 github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
 github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
-github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
-github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
+github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
+github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
 github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
-github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
-github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
+github.com/RoaringBitmap/roaring v1.9.0 h1:lwKhr90/j0jVXJyh5X+vQN1VVn77rQFfYnh6RDRGCcE=
+github.com/RoaringBitmap/roaring v1.9.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
 github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
 github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
 github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
@@ -118,7 +85,6 @@ github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
-github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
 github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
 github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -143,10 +109,10 @@ github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6
 github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
 github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
 github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
-github.com/blevesearch/bleve_index_api v1.1.5 h1:0q05mzu6GT/kebzqKywCpou/eUea9wTKa7kfqX7QX+k=
-github.com/blevesearch/bleve_index_api v1.1.5/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
-github.com/blevesearch/geo v0.1.19 h1:hlX1YpBZ+X+xfjS8hEpmM/tdPUFbqBME3mdAWKHo2s0=
-github.com/blevesearch/geo v0.1.19/go.mod h1:EPyr3iJCcESYa830PnkFhqzJkOP7/daHT/ocun43WRY=
+github.com/blevesearch/bleve_index_api v1.1.6 h1:orkqDFCBuNU2oHW9hN2YEJmet+TE9orml3FCGbl1cKk=
+github.com/blevesearch/bleve_index_api v1.1.6/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
+github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
+github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
 github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
 github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
 github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
@@ -155,8 +121,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+
 github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
 github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
 github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU=
-github.com/blevesearch/scorch_segment_api/v2 v2.2.6 h1:rewrzgFaCEjjfWovAB9NubMAd4+aCLxD3RaQcPDaoNo=
-github.com/blevesearch/scorch_segment_api/v2 v2.2.6/go.mod h1:0rv+k/OIjtYCT/g7Z45pCOVweFyta+0AdXO8keKfZxo=
+github.com/blevesearch/scorch_segment_api/v2 v2.2.8 h1:+OLW38LuRKio6N6V0gIk1srwFz79FJ5v2sNqHz2HVAA=
+github.com/blevesearch/scorch_segment_api/v2 v2.2.8/go.mod h1:ckbeb7knyOOvAdZinn/ASbB7EA3HoagnJkmEV3J7+sg=
 github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
 github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
 github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
@@ -194,40 +160,34 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
 github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
 github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
-github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
-github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
-github.com/buildkite/terminal-to-html/v3 v3.10.1 h1:znT9eD26LQ59dDJJEpMCwkP4wEptEAPi74hsTBuHdEo=
-github.com/buildkite/terminal-to-html/v3 v3.10.1/go.mod h1:qtuRyYs6/Sw3FS9jUyVEaANHgHGqZsGqMknPLyau5cQ=
+github.com/buildkite/terminal-to-html/v3 v3.11.0 h1:wMTpKgR61lqmxMz1FKjCaW5mq6DqeEgFZdJ+SU4hP30=
+github.com/buildkite/terminal-to-html/v3 v3.11.0/go.mod h1:8JACDet3vmvWLsL4IBobweQYtf19W5J+EKM3LEE1c+4=
 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
 github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
 github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ=
 github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
 github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
-github.com/couchbase/go-couchbase v0.0.0-20201026062457-7b3be89bbd89/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
 github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk=
 github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
-github.com/couchbase/gomemcached v0.1.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
-github.com/couchbase/gomemcached v0.3.0 h1:XkMDdP6w7rtvLijDE0/RhcccX+XvAk5cboyBv1YcI0U=
-github.com/couchbase/gomemcached v0.3.0/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
-github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
+github.com/couchbase/gomemcached v0.3.1 h1:jfspNuQIXgWy+5GUPQrsQ6yC5uJCfMmd/JKvK6C26r8=
+github.com/couchbase/gomemcached v0.3.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
 github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs=
 github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
 github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
@@ -244,8 +204,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
 github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
-github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
 github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
 github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
@@ -269,8 +227,8 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
-github.com/editorconfig/editorconfig-core-go/v2 v2.6.0 h1:5O8paxMLmi/5ONoKXzWNYxoSZU7+ITVbGcPga0IrzfE=
-github.com/editorconfig/editorconfig-core-go/v2 v2.6.0/go.mod h1:hdTKe+hwa3mMnMn4JUQziT+yc3pF+6EVmK2LPbLZthE=
+github.com/editorconfig/editorconfig-core-go/v2 v2.6.1 h1:iPCqofzMO41WVbcS/B5Ym7AwHQg9cyQ7Ie/R2XU5L3A=
+github.com/editorconfig/editorconfig-core-go/v2 v2.6.1/go.mod h1:VY4oyqUnpULFB3SCRpl24GFDIN1PmfiQIvN/G4ScSNg=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
@@ -284,16 +242,12 @@ github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTe
 github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/ethantkoenig/rupture v1.0.1 h1:6aAXghmvtnngMgQzy7SMGdicMvkV86V4n9fT0meE5E4=
 github.com/ethantkoenig/rupture v1.0.1/go.mod h1:Sjqo/nbffZp1pVVXNGhpugIjsWmuS9KiIB4GtpEBur4=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
-github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
-github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88=
+github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@@ -304,30 +258,29 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
-github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
+github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8=
 github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
 github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
 github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
-github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 h1:j2TrkUG/NATGi/EQS+MvEoF79CxiRUmT16ErFroNcKI=
-github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI=
-github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 h1:I2nuhyVI/48VXoRCCZR2hYBgnSXa+EuDJf/VyX06TC0=
-github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
+github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225 h1:OoM81OclgRX7CUch4M7MmsH0NcmLWpFiSn7rhs6Y5ZU=
+github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225/go.mod h1:yRUfFCoZY6C1CWalauqEQ5xYgSckzEBEO/2MBC6BOME=
+github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 h1:H9MGShwybHLSln6K8RxHPMHiLcD86Lru+5TVW2TcXHY=
+github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
 github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
 github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
-github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
+github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
 github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
 github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
-github.com/go-enry/go-enry/v2 v2.8.6 h1:T6ljs5+qNiUTDqpfK5GUD5EvLNdDbf804u8iC30vw7U=
-github.com/go-enry/go-enry/v2 v2.8.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
+github.com/go-enry/go-enry/v2 v2.8.7 h1:vbab0pcf5Yo1cHQLzbWZ+QomUh3EfEU8EiR5n7W0lnQ=
+github.com/go-enry/go-enry/v2 v2.8.7/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
 github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
 github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
 github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
@@ -345,38 +298,34 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
 github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
 github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
 github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc=
-github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0=
-github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo=
-github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY=
-github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
-github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
-github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
-github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
-github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
-github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
-github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
-github.com/go-openapi/loads v0.21.5 h1:jDzF4dSoHw6ZFADCGltDb2lE4F6De7aWSpe+IcsRzT0=
-github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8=
-github.com/go-openapi/runtime v0.26.2 h1:elWyB9MacRzvIVgAZCBJmqTi7hBzU0hlKD4IvfX0Zl0=
-github.com/go-openapi/runtime v0.26.2/go.mod h1:O034jyRZ557uJKzngbMDJXkcKJVzXJiymdSfgejrcRw=
-github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
-github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
-github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI=
-github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
-github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
-github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
-github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo=
-github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM=
+github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
+github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
+github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
+github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
+github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk=
+github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
+github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
+github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
+github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
+github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
+github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
+github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
+github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
+github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
 github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
-github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
-github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
+github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-swagger/go-swagger v0.30.5 h1:SQ2+xSonWjjoEMOV5tcOnZJVlfyUfCBhGQGArS1b9+U=
 github.com/go-swagger/go-swagger v0.30.5/go.mod h1:cWUhSyCNqV7J1wkkxfr5QmbcnCewetCdvEXqgPvbc/Q=
 github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0=
@@ -386,14 +335,15 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
 github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc=
 github.com/go-testfixtures/testfixtures/v3 v3.10.0/go.mod h1:z8RoleoNtibi6Ar8ziCW7e6PQ+jWiqbUWvuv8AMe4lo=
-github.com/go-webauthn/webauthn v0.10.0 h1:yuW2e1tXnRAwAvKrR4q4LQmc6XtCMH639/ypZGhZCwk=
-github.com/go-webauthn/webauthn v0.10.0/go.mod h1:l0NiauXhL6usIKqNLCUM3Qir43GK7ORg8ggold0Uv/Y=
-github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ=
-github.com/go-webauthn/x v0.1.6/go.mod h1:W8dFVZ79o4f+nY1eOUICy/uq5dhrRl7mxQkYhXTo0FA=
+github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
+github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
+github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
+github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
-github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -401,11 +351,10 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w
 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
-github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
-github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -413,52 +362,30 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
 github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
 github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
 github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -471,27 +398,13 @@ github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
-github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 h1:WzfWbQz/Ze8v6l++GGbGNFZnUShVpP/0xffCPLL+ax8=
-github.com/google/pprof v0.0.0-20240117000934-35fc243c5815/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
@@ -502,7 +415,6 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
-github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY=
@@ -510,7 +422,6 @@ github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
 github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
-github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
 github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
@@ -521,8 +432,6 @@ github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+
 github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
 github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -533,8 +442,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
 github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
 github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
@@ -555,23 +463,20 @@ github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
 github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
 github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
 github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
-github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
-github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZOw=
-github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
+github.com/jhillyerd/enmime v1.2.0 h1:dIu1IPEymQgoT2dzuB//ttA/xcV40NMPpQtmd4wslHk=
+github.com/jhillyerd/enmime v1.2.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@@ -585,17 +490,16 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
-github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
-github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
+github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
-github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
-github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -609,12 +513,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
-github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
-github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
-github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
-github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
-github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -629,11 +528,10 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
 github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE=
 github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
-github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
-github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
+github.com/markbates/goth v1.79.0 h1:fUYi9R6VubVEK2bpmXvIUp7xRcxA68i8ovfUQx/i5Qc=
+github.com/markbates/goth v1.79.0/go.mod h1:RBD+tcFnXul2NnYuODhnIweOcuVPkBohLfEvutPekcU=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -645,8 +543,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
 github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A=
-github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
+github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyqQ1hNveVsU2OE=
+github.com/meilisearch/meilisearch-go v0.26.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
 github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
 github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
 github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
@@ -657,8 +555,8 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
 github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
-github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs=
+github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
+github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -678,7 +576,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
-github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
 github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@@ -704,20 +601,19 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
 github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
 github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
-github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
 github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
 github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
 github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
@@ -740,31 +636,29 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
 github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
-github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
-github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
-github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
+github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
+github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
+github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
+github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
+github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
 github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
 github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
-github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
+github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
+github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-github.com/rhysd/actionlint v1.6.26 h1:zi7jPZf3Ks14gCXYAAL47uBziyFlX7+Xwilqhexct9g=
-github.com/rhysd/actionlint v1.6.26/go.mod h1:TIj1DlCgtYLOv5CH9wCK+WJTOr1qAdnFzkGi0IgSCO4=
+github.com/rhysd/actionlint v1.6.27 h1:xxwe8YmveBcC8lydW6GoHMGmB6H/MTqUU60F2p10wjw=
+github.com/rhysd/actionlint v1.6.27/go.mod h1:m2nFUjAnOrxCMXuOMz9evYBRCLUsMnKY2IJl/N5umbk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
-github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -781,8 +675,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
-github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd h1:KpbqRPDwcAQTyaP+L+YudTRb3CnJlQ64Hfn1SF/zHBA=
-github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI=
+github.com/sassoftware/go-rpmutils v0.3.0 h1:tE4TZ8KcOXay5iIP64P291s6Qxd9MQCYhI7DU+f3gFA=
+github.com/sassoftware/go-rpmutils v0.3.0/go.mod h1:hM9wdxFsjUFR/tJ6SMsLrJuChcucCa0DsCzE9RMfwMo=
 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
@@ -800,8 +694,8 @@ github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
-github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
+github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck=
@@ -843,8 +737,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@@ -868,23 +763,21 @@ github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6S
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
-github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
-github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
+github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
+github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
 github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
 github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
 github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
-github.com/xanzy/go-gitlab v0.96.0 h1:LGkZ+wSNMRtHIBaYE4Hq3dZVjprwHv3Y1+rhKU3WETs=
-github.com/xanzy/go-gitlab v0.96.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
+github.com/xanzy/go-gitlab v0.100.0 h1:jaOtYj5nWI19+9oVVmgy233pax2oYqucwetogYU46ks=
+github.com/xanzy/go-gitlab v0.100.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
 github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
-github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
-github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -895,8 +788,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
-github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
+github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
 github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5 h1:3seWKGVhGoc66Ht5QlhQsr4xT2caDnFegsnh2NqvENU=
 github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
 github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
@@ -904,12 +797,11 @@ github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBz
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
-github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
+github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
 github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
@@ -921,39 +813,30 @@ github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvv
 github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
-go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
-go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
 go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
-go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
-go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw=
-go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
-go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
-go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
-go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
+go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
+go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
-go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
-go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
-go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -963,80 +846,32 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
-golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
+golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
 golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
 golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -1046,23 +881,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
-golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1070,43 +895,21 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1114,9 +917,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1129,9 +932,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -1141,11 +943,9 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1158,153 +958,39 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
 golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
-golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
-golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
 google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
-google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
@@ -1322,7 +1008,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg=
-gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -1340,13 +1025,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@@ -1369,12 +1047,9 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
 modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
 mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs=
 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
 xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
 xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
-xorm.io/xorm v1.3.7 h1:mLceAGu0b87r9pD4qXyxGHxifOXIIrAdVcA6k95/osw=
-xorm.io/xorm v1.3.7/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
+xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
+xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
diff --git a/routers/api/actions/ping/ping.go b/routers/api/actions/ping/ping.go
index 55219fe12b..828350407a 100644
--- a/routers/api/actions/ping/ping.go
+++ b/routers/api/actions/ping/ping.go
@@ -12,7 +12,7 @@ import (
 
 	pingv1 "code.gitea.io/actions-proto-go/ping/v1"
 	"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 )
 
 func NewPingServiceHandler() (string, http.Handler) {
@@ -21,9 +21,7 @@ func NewPingServiceHandler() (string, http.Handler) {
 
 var _ pingv1connect.PingServiceHandler = (*Service)(nil)
 
-type Service struct {
-	pingv1connect.UnimplementedPingServiceHandler
-}
+type Service struct{}
 
 func (s *Service) Ping(
 	ctx context.Context,
diff --git a/routers/api/actions/ping/ping_test.go b/routers/api/actions/ping/ping_test.go
index f39e94a1f3..098b003ea2 100644
--- a/routers/api/actions/ping/ping_test.go
+++ b/routers/api/actions/ping/ping_test.go
@@ -11,7 +11,7 @@ import (
 
 	pingv1 "code.gitea.io/actions-proto-go/ping/v1"
 	"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go
index ddc754dbc7..c2f4ade174 100644
--- a/routers/api/actions/runner/interceptor.go
+++ b/routers/api/actions/runner/interceptor.go
@@ -15,7 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 )
diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index 8df6f297ce..1d07be3aec 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -16,7 +16,7 @@ import (
 
 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
 	"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 	gouuid "github.com/google/uuid"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
@@ -32,9 +32,7 @@ func NewRunnerServiceHandler() (string, http.Handler) {
 
 var _ runnerv1connect.RunnerServiceClient = (*Service)(nil)
 
-type Service struct {
-	runnerv1connect.UnimplementedRunnerServiceHandler
-}
+type Service struct{}
 
 // Register for new runner.
 func (s *Service) Register(
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index 85de1f9904..bb8092831f 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -259,7 +259,6 @@ func TestWebhookDeliverSpecificTypes(t *testing.T) {
 
 	for typ := range cases {
 		cases[typ].gotBody = make(chan []byte, 1)
-		typ := typ // TODO: remove this workaround when Go >= 1.22
 		t.Run(typ, func(t *testing.T) {
 			t.Parallel()
 			hook := &webhook_model.Webhook{

From 5ca65d33906ebbca1e502536ffef18942b541c1d Mon Sep 17 00:00:00 2001
From: Nanguan Lin <nanguanlin6@gmail.com>
Date: Mon, 18 Mar 2024 04:28:11 +0800
Subject: [PATCH 419/679] Fix missing code in the user profile (#29865)

fix #29820
deleted by
https://github.com/go-gitea/gitea/pull/29248/files#diff-2b0b591787f16325539485e648a09ab6d3177f47dc129cfe84a35ffe141dfd19L39-L62,
which causing malfunction of follow/unfollow and missing description in
the user profile page.
---
 routers/web/shared/user/header.go | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 2d6d9ad98d..7531e1ba26 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -16,6 +16,8 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/services/context"
@@ -34,6 +36,7 @@ func prepareContextForCommonProfile(ctx *context.Context) {
 func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 	prepareContextForCommonProfile(ctx)
 
+	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
 	if setting.Service.UserLocationMapURL != "" {
 		ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
@@ -45,6 +48,17 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 		return
 	}
 	ctx.Data["OpenIDs"] = openIDs
+	if len(ctx.ContextUser.Description) != 0 {
+		content, err := markdown.RenderString(&markup.RenderContext{
+			Metas: map[string]string{"mode": "document"},
+			Ctx:   ctx,
+		}, ctx.ContextUser.Description)
+		if err != nil {
+			ctx.ServerError("RenderString", err)
+			return
+		}
+		ctx.Data["RenderedDescription"] = content
+	}
 
 	showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
 	orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{

From 095fdd691dd1a7d7748372cc73e7708278c80933 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 17 Mar 2024 23:12:36 +0100
Subject: [PATCH 420/679] move some scripts from 'build' to 'tools' directory,
 misc refactors (#29844)

- Move some scripts from `build` to new `tools` dir. Eventually i would
like to move all but let's do it step-by-step.
- Add dir to eslint and move the files into vars.
- Update docs accordingly.
- While updating docs I noticed we were incorrectly having `public/img`
path still in a few places. Replace those with the current
`public/assets/img`.

---------

Co-authored-by: Nanguan Lin <nanguanlin6@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 .dockerignore                                    |  2 +-
 .eslintrc.yaml                                   |  4 ----
 .gitignore                                       |  2 +-
 Makefile                                         | 16 +++++++++-------
 .../content/administration/cmd-embedded.zh-cn.md |  6 +++---
 .../development/hacking-on-gitea.en-us.md        |  2 +-
 .../development/hacking-on-gitea.zh-cn.md        |  2 +-
 {build => tools}/generate-images.js              |  4 ++--
 {build => tools}/generate-svg.js                 |  0
 {build => tools}/watch.sh                        |  0
 10 files changed, 18 insertions(+), 20 deletions(-)
 rename {build => tools}/generate-images.js (95%)
 rename {build => tools}/generate-svg.js (100%)
 rename {build => tools}/watch.sh (100%)

diff --git a/.dockerignore b/.dockerignore
index 80cbeb040c..5cec84c9a3 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -62,7 +62,6 @@ cpu.out
 /data
 /indexers
 /log
-/public/img/avatar
 /tests/integration/gitea-integration-*
 /tests/integration/indexers-*
 /tests/e2e/gitea-e2e-*
@@ -78,6 +77,7 @@ cpu.out
 /public/assets/js
 /public/assets/css
 /public/assets/fonts
+/public/assets/img/avatar
 /public/assets/img/webpack
 /vendor
 /web_src/fomantic/node_modules
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index b62b13cefe..b65fe56cf2 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -42,10 +42,6 @@ overrides:
       worker: true
     rules:
       no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
-  - files: ["build/generate-images.js"]
-    rules:
-      i/no-unresolved: [0]
-      i/no-extraneous-dependencies: [0]
   - files: ["*.config.*"]
     rules:
       i/no-unused-modules: [0]
diff --git a/.gitignore b/.gitignore
index 8f2544866a..abf9565cff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,7 +58,7 @@ cpu.out
 /data
 /indexers
 /log
-/public/img/avatar
+/public/assets/img/avatar
 /tests/integration/gitea-integration-*
 /tests/integration/indexers-*
 /tests/e2e/gitea-e2e-*
diff --git a/Makefile b/Makefile
index 88bcf0e17c..1cddad1e93 100644
--- a/Makefile
+++ b/Makefile
@@ -147,6 +147,8 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN
 GO_DIRS := build cmd models modules routers services tests
 WEB_DIRS := web_src/js web_src/css
 
+ESLINT_FILES := web_src/js tools *.config.js tests/e2e
+STYLELINT_FILES := web_src/css web_src/js/components/*.vue
 SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
 EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
 
@@ -375,19 +377,19 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
 
 .PHONY: lint-js
 lint-js: node_modules
-	npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e
+	npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES)
 
 .PHONY: lint-js-fix
 lint-js-fix: node_modules
-	npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e --fix
+	npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) --fix
 
 .PHONY: lint-css
 lint-css: node_modules
-	npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue
+	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES)
 
 .PHONY: lint-css-fix
 lint-css-fix: node_modules
-	npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue --fix
+	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
 
 .PHONY: lint-swagger
 lint-swagger: node_modules
@@ -444,7 +446,7 @@ lint-yaml: .venv
 
 .PHONY: watch
 watch:
-	@bash build/watch.sh
+	@bash tools/watch.sh
 
 .PHONY: watch-frontend
 watch-frontend: node-check node_modules
@@ -916,7 +918,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
 .PHONY: svg
 svg: node-check | node_modules
 	rm -rf $(SVG_DEST_DIR)
-	node build/generate-svg.js
+	node tools/generate-svg.js
 
 .PHONY: svg-check
 svg-check: svg
@@ -960,7 +962,7 @@ generate-gitignore:
 .PHONY: generate-images
 generate-images: | node_modules
 	npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7
-	node build/generate-images.js $(TAGS)
+	node tools/generate-images.js $(TAGS)
 
 .PHONY: generate-manpage
 generate-manpage:
diff --git a/docs/content/administration/cmd-embedded.zh-cn.md b/docs/content/administration/cmd-embedded.zh-cn.md
index 4570bb58a3..a2df1aa2f5 100644
--- a/docs/content/administration/cmd-embedded.zh-cn.md
+++ b/docs/content/administration/cmd-embedded.zh-cn.md
@@ -37,7 +37,7 @@ gitea embedded list [--include-vendored] [patterns...]
 
 - 列出所有模板文件,无论在哪个虚拟目录下:`**.tmpl`
 - 列出所有邮件模板文件:`templates/mail/**.tmpl`
-- 列出 `public/img` 目录下的所有文件:`public/img/**`
+列出 `public/assets/img` 目录下的所有文件:`public/assets/img/**`
 
 不要忘记为模式使用引号,因为空格、`*` 和其他字符可能对命令行解释器有特殊含义。
 
@@ -49,8 +49,8 @@ gitea embedded list [--include-vendored] [patterns...]
 
 ```sh
 $ gitea embedded list '**openid**'
-public/img/auth/openid_connect.svg
-public/img/openid-16x16.png
+public/assets/img/auth/openid_connect.svg
+public/assets/img/openid-16x16.png
 templates/user/auth/finalize_openid.tmpl
 templates/user/auth/signin_openid.tmpl
 templates/user/auth/signup_openid_connect.tmpl
diff --git a/docs/content/development/hacking-on-gitea.en-us.md b/docs/content/development/hacking-on-gitea.en-us.md
index 982dbcf6ea..004e803827 100644
--- a/docs/content/development/hacking-on-gitea.en-us.md
+++ b/docs/content/development/hacking-on-gitea.en-us.md
@@ -214,7 +214,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
 
 ### Building and adding SVGs
 
-SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
+SVG icons are built using the `make svg` target which compiles the icon sources into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
 
 ### Building the Logo
 
diff --git a/docs/content/development/hacking-on-gitea.zh-cn.md b/docs/content/development/hacking-on-gitea.zh-cn.md
index a31e1dc511..7dfea30538 100644
--- a/docs/content/development/hacking-on-gitea.zh-cn.md
+++ b/docs/content/development/hacking-on-gitea.zh-cn.md
@@ -201,7 +201,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
 
 ### 构建和添加 SVGs
 
-SVG 图标是使用 `make svg` 目标构建的,该目标将 `build/generate-svg.js` 中定义的图标源编译到输出目录 `public/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。
+SVG 图标是使用 `make svg` 命令构建的,该命令将图标资源编译到输出目录 `public/assets/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。
 
 ### 构建 Logo
 
diff --git a/build/generate-images.js b/tools/generate-images.js
similarity index 95%
rename from build/generate-images.js
rename to tools/generate-images.js
index db31d19e2a..cc2855c18e 100755
--- a/build/generate-images.js
+++ b/tools/generate-images.js
@@ -1,7 +1,7 @@
 #!/usr/bin/env node
-import imageminZopfli from 'imagemin-zopfli';
+import imageminZopfli from 'imagemin-zopfli'; // eslint-disable-line i/no-unresolved
+import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line i/no-unresolved
 import {optimize} from 'svgo';
-import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node';
 import {readFile, writeFile} from 'node:fs/promises';
 import {argv, exit} from 'node:process';
 
diff --git a/build/generate-svg.js b/tools/generate-svg.js
similarity index 100%
rename from build/generate-svg.js
rename to tools/generate-svg.js
diff --git a/build/watch.sh b/tools/watch.sh
similarity index 100%
rename from build/watch.sh
rename to tools/watch.sh

From a4a766f4a205d507d157f2f17a0e761e00939cb8 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 18 Mar 2024 00:24:59 +0000
Subject: [PATCH 421/679] [skip ci] Updated licenses and gitignores

---
 options/license/threeparttable | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 options/license/threeparttable

diff --git a/options/license/threeparttable b/options/license/threeparttable
new file mode 100644
index 0000000000..498b728226
--- /dev/null
+++ b/options/license/threeparttable
@@ -0,0 +1,3 @@
+This file may be distributed, modified, and used in other works with just
+one restriction: modified versions must clearly indicate the modification
+(a name change, or a displayed message, or ?).

From 16e360099d0a515d429538ec88cff1f3ede23fb4 Mon Sep 17 00:00:00 2001
From: buckybytes <158571971+buckybytes@users.noreply.github.com>
Date: Sun, 17 Mar 2024 21:23:08 -0500
Subject: [PATCH 422/679] Editor error message misleading due to re-used key.
 (#29859)

The error message:

`editor.file_changed_while_editing = The file contents have changed
since you started editing. <a target="_blank" rel="noopener noreferrer"
href="%s">Click here</a> to see them or <strong>Commit Changes
again</strong> to overwrite them.`

Is re-used in inappropriate contexts. The link in the key goes to a 404
when the key is used in a situation where the file contents have not
changed.

Added two new keys to differentiate commit id mismatch and push out of
date conditions.
---
 options/locale/locale_en-US.ini | 2 ++
 routers/web/repo/editor.go      | 4 ++--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index dc16d78fc7..6622a25efd 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1312,6 +1312,8 @@ editor.file_editing_no_longer_exists = The file being edited, "%s", no longer ex
 editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository.
 editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
 editor.file_already_exists = A file named "%s" already exists in this repository.
+editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing.  Commit into a patch branch and then merge.
+editor.push_out_of_date = The push appears to be out of date.
 editor.commit_empty_file_header = Commit an empty file
 editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
 editor.no_changes_to_show = There are no changes to show.
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 6146ce4ce4..29395b4013 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -333,9 +333,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 				ctx.Error(http.StatusInternalServerError, err.Error())
 			}
 		} else if models.IsErrCommitIDDoesNotMatch(err) {
-			ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form)
+			ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form)
 		} else if git.IsErrPushOutOfDate(err) {
-			ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
+			ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
 		} else if git.IsErrPushRejected(err) {
 			errPushRej := err.(*git.ErrPushRejected)
 			if len(errPushRej.Message) == 0 {

From b251e608c01392c947f84be387f956541bfea25c Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 18 Mar 2024 19:05:17 +0800
Subject: [PATCH 423/679] Only do counting when count_only=true for repo
 dashboard (#29884)

Ref: #29878
---
 routers/web/repo/repo.go                    | 25 ++++++++++++---------
 web_src/js/components/DashboardRepoList.vue |  2 +-
 2 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 7a626a7065..7f9bf3210a 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -622,26 +622,31 @@ func SearchRepo(ctx *context.Context) {
 		}
 	}
 
-	var err error
+	// To improve performance when only the count is requested
+	if ctx.FormBool("count_only") {
+		if count, err := repo_model.CountRepository(ctx, opts); err != nil {
+			log.Error("CountRepository: %v", err)
+			ctx.JSON(http.StatusInternalServerError, nil) // frontend JS doesn't handle error response (same as below)
+		} else {
+			ctx.SetTotalCountHeader(count)
+			ctx.JSONOK()
+		}
+		return
+	}
+
 	repos, count, err := repo_model.SearchRepository(ctx, opts)
 	if err != nil {
-		ctx.JSON(http.StatusInternalServerError, api.SearchError{
-			OK:    false,
-			Error: err.Error(),
-		})
+		log.Error("SearchRepository: %v", err)
+		ctx.JSON(http.StatusInternalServerError, nil)
 		return
 	}
 
 	ctx.SetTotalCountHeader(count)
 
-	// To improve performance when only the count is requested
-	if ctx.FormBool("count_only") {
-		return
-	}
-
 	latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
 	if err != nil {
 		log.Error("FindReposLastestCommitStatuses: %v", err)
+		ctx.JSON(http.StatusInternalServerError, nil)
 		return
 	}
 
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index b9ee531d2a..e039ed016b 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -235,7 +235,7 @@ const sfc = {
         if (!this.reposTotalCount) {
           const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
           response = await GET(totalCountSearchURL);
-          this.reposTotalCount = response.headers.get('X-Total-Count');
+          this.reposTotalCount = response.headers.get('X-Total-Count') ?? '?';
         }
 
         response = await GET(searchedURL);

From 34290a00c4501ffeba26db267be71ab68e3ec97f Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 18 Mar 2024 15:47:05 +0100
Subject: [PATCH 424/679] Migrate border and margin classes to Tailwind
 (#29828)

Used all existing css vars, other migrations are 1:1.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 tailwind.config.js                           | 11 ++++++++++
 templates/devtest/flex-list.tmpl             |  2 +-
 templates/devtest/gitea-ui.tmpl              |  4 ++--
 templates/org/header.tmpl                    |  2 +-
 templates/repo/issue/filter_list.tmpl        |  4 ++--
 templates/repo/issue/labels/label_list.tmpl  |  2 +-
 templates/repo/pulls/tab_menu.tmpl           |  2 +-
 templates/repo/settings/options.tmpl         |  6 +++---
 templates/status/500.tmpl                    |  3 ++-
 templates/user/dashboard/navbar.tmpl         |  2 +-
 web_src/css/helpers.css                      | 22 --------------------
 web_src/js/components/DashboardRepoList.vue  |  9 ++++----
 web_src/js/components/DiffCommitSelector.vue | 14 ++++++++-----
 web_src/js/components/RepoCodeFrequency.vue  |  2 +-
 web_src/js/components/RepoContributors.vue   |  2 +-
 web_src/js/components/RepoRecentCommits.vue  |  2 +-
 web_src/js/features/repo-diff-commit.js      |  2 +-
 17 files changed, 43 insertions(+), 48 deletions(-)

diff --git a/tailwind.config.js b/tailwind.config.js
index d783268bd7..e2e8f23656 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -55,5 +55,16 @@ export default {
       current: 'currentcolor',
       transparent: 'transparent',
     },
+    borderRadius: {
+      'none': '0',
+      'sm': '2px',
+      'DEFAULT': 'var(--border-radius)', // 4px
+      'md': 'var(--border-radius-medium)', // 6px
+      'lg': '8px',
+      'xl': '12px',
+      '2xl': '16px',
+      '3xl': '24px',
+      'full': 'var(--border-radius-circle)', // 50%
+    },
   },
 };
diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl
index c8584c110b..0c7b27cd84 100644
--- a/templates/devtest/flex-list.tmpl
+++ b/templates/devtest/flex-list.tmpl
@@ -104,7 +104,7 @@
 		</div>
 
 		<h1>If parent provides the padding/margin space:</h1>
-		<div class="gt-border-secondary gt-py-4">
+		<div class="tw-border tw-border-secondary gt-py-4">
 			<div class="flex-list flex-space-fitted">
 				<div class="flex-item">item 1 (no padding top)</div>
 				<div class="flex-item">item 2 (no padding bottom)</div>
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 284566046d..f71b6611c5 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -95,8 +95,8 @@
 
 	<div>
 		<h1>Loading</h1>
-		<div class="is-loading small-loading-icon gt-border-secondary gt-py-2"><span>loading ...</span></div>
-		<div class="is-loading gt-border-secondary gt-py-4">
+		<div class="is-loading small-loading-icon tw-border tw-border-secondary gt-py-2"><span>loading ...</span></div>
+		<div class="is-loading tw-border tw-border-secondary gt-py-4">
 			<p>loading ...</p>
 			<p>loading ...</p>
 			<p>loading ...</p>
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 943557b1ca..1a55101c2e 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -7,7 +7,7 @@
 				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
 				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
 			</span>
-			<span class="gt-df gt-ac gt-gap-2 gt-ml-auto gt-font-16 tw-whitespace-nowrap">
+			<span class="gt-df gt-ac gt-gap-2 tw-ml-auto gt-font-16 tw-whitespace-nowrap">
 				{{if .EnableFeed}}
 					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
 						{{svg "octicon-rss" 24}}
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index d0086fdf8c..bb13f63b98 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -43,7 +43,7 @@
 					{{end}}
 				{{end}}
 				{{RenderLabel $.Context ctx.Locale .}}
-				<p class="gt-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
+				<p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
 			</a>
 		{{end}}
 	</div>
@@ -108,7 +108,7 @@
 			</div>
 			{{range .OpenProjects}}
 				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item gt-df" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
-					{{svg .IconName 18 "gt-mr-3 gt-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
+					{{svg .IconName 18 "gt-mr-3 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
 				</a>
 			{{end}}
 		{{end}}
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index 428a4919aa..ca8d40f1e0 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -44,7 +44,7 @@
 			</div>
 			<div class="label-operation gt-df">
 				{{template "repo/issue/labels/label_archived" .}}
-				<div class="gt-df gt-ml-auto">
+				<div class="gt-df tw-ml-auto">
 					{{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}}
 						<a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a>
 						<a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a>
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index 10bdfdb3de..340b1bb397 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -15,7 +15,7 @@
 			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
-		<span class="item gt-ml-auto gt-pr-0 gt-font-bold gt-df gt-ac gt-gap-3">
+		<span class="item tw-ml-auto gt-pr-0 gt-font-bold gt-df gt-ac gt-gap-3">
 			<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
 			<span class="diff-stats-bar">
 				<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index a699396a84..604e5d4152 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -116,13 +116,12 @@
 								<th></th>
 							</tr>
 						</thead>
-						{{end}}
 						{{if $modifyBrokenPullMirror}}
 							{{/* even if a repo is a pull mirror (IsMirror=true), the PullMirror might still be nil if the mirror migration is broken */}}
 							<tbody>
 								<tr>
 									<td colspan="4">
-										<div class="text red gt-py-4 gt-border-secondary-bottom">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div>
+										<div class="text red gt-py-4">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div>
 									</td>
 								</tr>
 							</tbody>
@@ -201,8 +200,9 @@
 								</td>
 							</tr>
 						</tbody>
+						{{end}}{{/* end if: $modifyBrokenPullMirror / $isWorkingPullMirror */}}
 					</table>
-					{{end}}{{/* end if: IsMirror */}}
+					{{end}}{{/* end if .Repository.IsMirror */}}
 
 					<table class="ui table">
 						<thead>
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index a821fe55da..29de861a25 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -16,7 +16,7 @@
 </head>
 <body>
 	<div class="full height">
-		<nav class="ui secondary menu gt-border-secondary-bottom">
+		<nav class="ui secondary menu">
 			<div class="ui container gt-df">
 				<div class="item gt-f1">
 					<a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home"}}">
@@ -28,6 +28,7 @@
 				</div>
 			</div>
 		</nav>
+		<div class="divider gt-my-0"></div>
 		<div role="main" class="page-content status-page-500">
 			<div class="ui container" >
 				<style> .ui.message.flash-message { text-align: left; } </style>
diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl
index af07897e2c..3e9442d6fc 100644
--- a/templates/user/dashboard/navbar.tmpl
+++ b/templates/user/dashboard/navbar.tmpl
@@ -78,7 +78,7 @@
 
 	{{if .ContextUser.IsOrganization}}
 		<div class="right menu">
-			<a class="{{if .PageIsNews}}active {{end}}item gt-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}">
+			<a class="{{if .PageIsNews}}active {{end}}item tw-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}">
 				{{svg "octicon-rss"}}&nbsp;{{ctx.Locale.Tr "activities"}}
 			</a>
 			{{if not .UnitIssuesGlobalDisabled}}
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 860722823a..6fc84d743c 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -52,18 +52,6 @@ Gitea's private styles use `g-` prefix.
 .gt-font-semibold { font-weight: var(--font-weight-semibold) !important; }
 .gt-font-bold { font-weight: var(--font-weight-bold) !important; }
 
-.gt-rounded { border-radius: var(--border-radius) !important; }
-.gt-rounded-top { border-radius: var(--border-radius) var(--border-radius) 0 0 !important; }
-.gt-rounded-bottom { border-radius: 0 0 var(--border-radius) var(--border-radius) !important; }
-.gt-rounded-left { border-radius: var(--border-radius) 0 0 var(--border-radius) !important; }
-.gt-rounded-right { border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; }
-
-.gt-border-secondary { border: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-top { border-top: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-bottom { border-bottom: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-left { border-left: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-right { border-right: 1px solid var(--color-secondary) !important; }
-
 .interact-fg { color: inherit !important; }
 .interact-fg:hover { color: var(--color-primary) !important; }
 .interact-fg:active { color: var(--color-primary-active) !important; }
@@ -121,14 +109,6 @@ Gitea's private styles use `g-` prefix.
 .gt-my-4 { margin-top: 1rem !important; margin-bottom: 1rem !important; }
 .gt-my-5 { margin-top: 2rem !important; margin-bottom: 2rem !important; }
 
-.gt-m-auto  { margin: auto !important; }
-.gt-mx-auto { margin-left: auto !important; margin-right: auto !important; }
-.gt-my-auto { margin-top: auto !important; margin-bottom: auto !important; }
-.gt-mt-auto { margin-top: auto !important; }
-.gt-mr-auto { margin-right: auto !important; }
-.gt-mb-auto { margin-bottom: auto !important; }
-.gt-ml-auto { margin-left: auto !important; }
-
 .gt-p-0 { padding: 0 !important; }
 .gt-p-1 { padding: .125rem !important; }
 .gt-p-2 { padding: .25rem !important; }
@@ -199,8 +179,6 @@ Gitea's private styles use `g-` prefix.
 .gt-gap-y-4 { row-gap: 1rem !important; }
 .gt-gap-y-5 { row-gap: 2rem !important; }
 
-.gt-shrink-0 { flex-shrink: 0 !important; }
-
 .gt-font-12 { font-size: 12px !important }
 .gt-font-13 { font-size: 13px !important }
 .gt-font-14 { font-size: 14px !important }
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index e039ed016b..177ae04855 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -409,7 +409,7 @@ export default sfc; // activate the IDE's Vue plugin
           </div>
         </overflow-menu>
       </div>
-      <div v-if="repos.length" class="ui attached table segment gt-rounded-bottom">
+      <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
           <li class="gt-df gt-ac gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
             <a class="repo-list-link muted" :href="repo.link">
@@ -425,8 +425,9 @@ export default sfc; // activate the IDE's Vue plugin
             </a>
           </li>
         </ul>
-        <div v-if="showMoreReposLink" class="center gt-py-3 gt-border-secondary-top">
-          <div class="ui borderless pagination menu narrow">
+        <div v-if="showMoreReposLink" class="tw-text-center">
+          <div class="divider gt-my-0"/>
+          <div class="ui borderless pagination menu narrow gt-my-3">
             <a
               class="item navigation gt-py-2" :class="{'disabled': page === 1}"
               @click="changePage(1)" :title="textFirstPage"
@@ -466,7 +467,7 @@ export default sfc; // activate the IDE's Vue plugin
           <svg-icon name="octicon-plus"/>
         </a>
       </h4>
-      <div v-if="organizations.length" class="ui attached table segment gt-rounded-bottom">
+      <div v-if="organizations.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
           <li class="gt-df gt-ac gt-py-3" v-for="org in organizations" :key="org.name">
             <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index 780ba22f0c..b465127aca 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -204,7 +204,7 @@ export default {
     </button>
     <div class="menu left transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
       <div class="loading-indicator is-loading" v-if="isLoading"/>
-      <div v-if="!isLoading" class="vertical item gt-df gt-fc gt-gap-2" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
+      <div v-if="!isLoading" class="vertical item" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
         <div class="gt-ellipsis">
           {{ locale.show_all_commits }}
         </div>
@@ -215,7 +215,7 @@ export default {
       <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review -->
       <div
         v-if="lastReviewCommitSha != null" role="menuitem"
-        class="vertical item gt-df gt-fc gt-gap-2 gt-border-secondary-top"
+        class="vertical item"
         :class="{disabled: commitsSinceLastReview === 0}"
         @keydown.enter="changesSinceLastReviewClick()"
         @click="changesSinceLastReviewClick()"
@@ -227,10 +227,10 @@ export default {
           {{ commitsSinceLastReview }} commits
         </div>
       </div>
-      <span v-if="!isLoading" class="info gt-border-secondary-top text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>
+      <span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>
       <template v-for="commit in commits" :key="commit.id">
         <div
-          class="vertical item gt-df gt-gap-2 gt-border-secondary-top" role="menuitem"
+          class="vertical item" role="menuitem"
           :class="{selection: commit.selected, hovered: commit.hovered}"
           @keydown.enter.exact="commitClicked(commit.id)"
           @keydown.enter.shift.exact="commitClickedShift(commit)"
@@ -285,10 +285,14 @@ export default {
     width: 350px;
   }
 
-  #diff-commit-selector-menu .item {
+  #diff-commit-selector-menu .item,
+  #diff-commit-selector-menu .info {
+    display: flex !important;
     flex-direction: row;
     line-height: 1.4;
     padding: 7px 14px !important;
+    border-top: 1px solid var(--color-secondary) !important;
+    gap: 0.25em;
   }
 
   #diff-commit-selector-menu .item:focus {
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
index ad607a041a..c55bbff9cd 100644
--- a/web_src/js/components/RepoCodeFrequency.vue
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -148,7 +148,7 @@ export default {
       {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
     </div>
     <div class="gt-df ui segment main-graph">
-      <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
           <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
           {{ locale.loadingInfo }}
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 22c247ae32..6093c762cb 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -353,7 +353,7 @@ export default {
       </div>
     </div>
     <div class="gt-df ui segment main-graph">
-      <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
           <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
           {{ locale.loadingInfo }}
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
index 77697cd413..c1fd40f506 100644
--- a/web_src/js/components/RepoRecentCommits.vue
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -125,7 +125,7 @@ export default {
       {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
     </div>
     <div class="gt-df ui segment main-graph">
-      <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
           <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
           {{ locale.loadingInfo }}
diff --git a/web_src/js/features/repo-diff-commit.js b/web_src/js/features/repo-diff-commit.js
index 3d4f0f677a..f0466f9320 100644
--- a/web_src/js/features/repo-diff-commit.js
+++ b/web_src/js/features/repo-diff-commit.js
@@ -39,7 +39,7 @@ function addLink(parent, href, text, tooltip) {
   link.href = href;
   link.textContent = text;
   if (tooltip) {
-    link.classList.add('gt-border-secondary', 'gt-rounded');
+    link.classList.add('tw-border', 'tw-border-secondary', 'tw-rounded');
     link.setAttribute('data-tooltip-content', tooltip);
   }
   parent.append(link);

From 1f0d31ce8fdfc8c32f84e4e0801c2d04b727bbd8 Mon Sep 17 00:00:00 2001
From: Nanguan Lin <nanguanlin6@gmail.com>
Date: Tue, 19 Mar 2024 05:14:51 +0800
Subject: [PATCH 425/679] Remove unused error in graceful manager (#29871)

As title.
---
 modules/graceful/manager_unix.go |  4 ++--
 modules/graceful/net_unix.go     | 12 ++----------
 2 files changed, 4 insertions(+), 12 deletions(-)

diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go
index f49c42650c..d03fff9b5b 100644
--- a/modules/graceful/manager_unix.go
+++ b/modules/graceful/manager_unix.go
@@ -59,8 +59,8 @@ func (g *Manager) start() {
 	go func() {
 		defer func() {
 			close(startupDone)
-			// Close the unused listeners and ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
-			_ = CloseProvidedListeners()
+			// Close the unused listeners
+			closeProvidedListeners()
 		}()
 		// Wait for all servers to be created
 		g.createServerCond.L.Lock()
diff --git a/modules/graceful/net_unix.go b/modules/graceful/net_unix.go
index 4f8c036a69..796e00507c 100644
--- a/modules/graceful/net_unix.go
+++ b/modules/graceful/net_unix.go
@@ -129,25 +129,17 @@ func getProvidedFDs() (savedErr error) {
 	return savedErr
 }
 
-// CloseProvidedListeners closes all unused provided listeners.
-func CloseProvidedListeners() error {
+// closeProvidedListeners closes all unused provided listeners.
+func closeProvidedListeners() {
 	mutex.Lock()
 	defer mutex.Unlock()
-	var returnableError error
 	for _, l := range providedListeners {
 		err := l.Close()
 		if err != nil {
 			log.Error("Error in closing unused provided listener: %v", err)
-			if returnableError != nil {
-				returnableError = fmt.Errorf("%v & %w", returnableError, err)
-			} else {
-				returnableError = err
-			}
 		}
 	}
 	providedListeners = []net.Listener{}
-
-	return returnableError
 }
 
 // DefaultGetListener obtains a listener for the stream-oriented local network address:

From 0e183d81fc5283f9d2047472de580e4f04a046c1 Mon Sep 17 00:00:00 2001
From: coldWater <forsaken628@gmail.com>
Date: Tue, 19 Mar 2024 10:20:36 +0800
Subject: [PATCH 426/679] Fix missing error check of bufio.Scanner (#29882)

maybe more
---
 models/asymkey/ssh_key_authorized_keys.go         | 4 ++++
 modules/git/commit.go                             | 5 +++++
 modules/git/repo_stats.go                         | 4 ++++
 modules/markup/csv/csv.go                         | 5 +++++
 routers/web/repo/compare.go                       | 4 ++++
 services/asymkey/ssh_key_authorized_principals.go | 4 ++++
 services/doctor/authorizedkeys.go                 | 4 ++++
 7 files changed, 30 insertions(+)

diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go
index 9279db2020..7621994866 100644
--- a/models/asymkey/ssh_key_authorized_keys.go
+++ b/models/asymkey/ssh_key_authorized_keys.go
@@ -152,6 +152,10 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
 				return err
 			}
 		}
+		err = scanner.Err()
+		if err != nil {
+			return fmt.Errorf("scan: %w", err)
+		}
 		f.Close()
 	}
 	return nil
diff --git a/modules/git/commit.go b/modules/git/commit.go
index facb632bd9..789a2e8f69 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -9,6 +9,7 @@ import (
 	"bytes"
 	"context"
 	"errors"
+	"fmt"
 	"io"
 	"os/exec"
 	"strconv"
@@ -396,6 +397,10 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) {
 			}
 		}
 	}
+	err = scanner.Err()
+	if err != nil {
+		return nil, fmt.Errorf("scan: %w", err)
+	}
 
 	return c.submoduleCache, nil
 }
diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go
index 41f94e24f9..ce82946873 100644
--- a/modules/git/repo_stats.go
+++ b/modules/git/repo_stats.go
@@ -124,6 +124,10 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
 					}
 				}
 			}
+			err = scanner.Err()
+			if err != nil {
+				return fmt.Errorf("scan: %w", err)
+			}
 			a := make([]*CodeActivityAuthor, 0, len(authors))
 			for _, v := range authors {
 				a = append(a, v)
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 570c4f4704..50bb918442 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -6,6 +6,7 @@ package markup
 import (
 	"bufio"
 	"bytes"
+	"fmt"
 	"html"
 	"io"
 	"regexp"
@@ -123,6 +124,10 @@ func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
 			return err
 		}
 	}
+	err = scan.Err()
+	if err != nil {
+		return fmt.Errorf("scan: %w", err)
+	}
 
 	_, err = tmpBlock.WriteString("</pre>")
 	if err != nil {
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index b0570f97c3..bf42b77b66 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -980,5 +980,9 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu
 		}
 		diffLines = append(diffLines, diffLine)
 	}
+	err = scanner.Err()
+	if err != nil {
+		return nil, fmt.Errorf("scan: %w", err)
+	}
 	return diffLines, nil
 }
diff --git a/services/asymkey/ssh_key_authorized_principals.go b/services/asymkey/ssh_key_authorized_principals.go
index 9154db7dbb..822dd0ffe7 100644
--- a/services/asymkey/ssh_key_authorized_principals.go
+++ b/services/asymkey/ssh_key_authorized_principals.go
@@ -122,6 +122,10 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
 				return err
 			}
 		}
+		err = scanner.Err()
+		if err != nil {
+			return fmt.Errorf("scan: %w", err)
+		}
 		f.Close()
 	}
 	return nil
diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go
index d5a96605b9..bc0266c4bc 100644
--- a/services/doctor/authorizedkeys.go
+++ b/services/doctor/authorizedkeys.go
@@ -51,6 +51,10 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
 		}
 		linesInAuthorizedKeys.Add(line)
 	}
+	err = scanner.Err()
+	if err != nil {
+		return fmt.Errorf("scan: %w", err)
+	}
 	f.Close()
 
 	// now we regenerate and check if there are any lines missing

From 828701ff2de67e179e79c79e6b52e92e770df789 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 19 Mar 2024 12:19:48 +0800
Subject: [PATCH 427/679] Fix template error when comment review doesn't exist
 (#29888)

Fix #29885
---
 models/fixtures/comment.yml                   |   8 +
 models/fixtures/review.yml                    |   9 +
 routers/web/repo/pull_review_test.go          |  17 ++
 templates/repo/diff/comments.tmpl             |   2 +-
 templates/repo/diff/conversation.tmpl         | 130 ++++-----
 .../repo/issue/view_content/comments.tmpl     |  28 +-
 .../repo/issue/view_content/conversation.tmpl | 258 +++++++++---------
 tests/integration/pull_review_test.go         |  14 +-
 8 files changed, 264 insertions(+), 202 deletions(-)

diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml
index 17586caa21..74fc716180 100644
--- a/models/fixtures/comment.yml
+++ b/models/fixtures/comment.yml
@@ -75,3 +75,11 @@
   content: "comment in private pository"
   created_unix: 946684811
   updated_unix: 946684811
+
+-
+  id: 9
+  type: 22 # review
+  poster_id: 2
+  issue_id: 2 # in repo_id 1
+  review_id: 20
+  created_unix: 946684810
diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml
index 7a88080068..ac97e24c2b 100644
--- a/models/fixtures/review.yml
+++ b/models/fixtures/review.yml
@@ -170,3 +170,12 @@
   content: "review request for user15"
   updated_unix: 946684835
   created_unix: 946684835
+
+-
+  id: 20
+  type: 22
+  reviewer_id: 1
+  issue_id: 2
+  content: "Review Comment"
+  updated_unix: 946684810
+  created_unix: 946684810
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
index 5f035f1eb0..8344ff4091 100644
--- a/routers/web/repo/pull_review_test.go
+++ b/routers/web/repo/pull_review_test.go
@@ -4,6 +4,7 @@
 package repo
 
 import (
+	"net/http"
 	"net/http/httptest"
 	"testing"
 
@@ -73,4 +74,20 @@ func TestRenderConversation(t *testing.T) {
 		renderConversation(ctx, preparedComment, "timeline")
 		assert.Contains(t, resp.Body.String(), `<div id="code-comments-`)
 	})
+	run("diff non-existing review", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+		err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{})
+		assert.NoError(t, err)
+		ctx.Data["ShowOutdatedComments"] = true
+		renderConversation(ctx, preparedComment, "diff")
+		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.NotContains(t, resp.Body.String(), `status-page-500`)
+	})
+	run("timeline non-existing review", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+		err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{})
+		assert.NoError(t, err)
+		ctx.Data["ShowOutdatedComments"] = true
+		renderConversation(ctx, preparedComment, "timeline")
+		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.NotContains(t, resp.Body.String(), `status-page-500`)
+	})
 }
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index f5fd7076fa..227dcc49d0 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -37,7 +37,7 @@
 						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
 					</a>
 				{{end}}
-				{{if and .Review}}
+				{{if .Review}}
 					{{if eq .Review.Type 0}}
 						<div class="ui label basic small yellow pending-label" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.pending.tooltip" (ctx.Locale.Tr "repo.diff.review") (ctx.Locale.Tr "repo.diff.review.approve") (ctx.Locale.Tr "repo.diff.review.comment") (ctx.Locale.Tr "repo.diff.review.reject")}}">
 						{{ctx.Locale.Tr "repo.issues.review.pending"}}
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index aaeac3c550..507f17fd94 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -1,66 +1,72 @@
-{{$resolved := (index .comments 0).IsResolved}}
-{{$invalid := (index .comments 0).Invalidated}}
-{{$resolveDoer := (index .comments 0).ResolveDoer}}
-{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
-{{$referenceUrl := printf "%s#%s" $.Issue.Link (index .comments 0).HashTag}}
-<div class="conversation-holder" data-path="{{(index .comments 0).TreePath}}" data-side="{{if lt (index .comments 0).Line 0}}left{{else}}right{{end}}" data-idx="{{(index .comments 0).UnsignedLine}}">
-	{{if $resolved}}
-		<div class="ui attached header resolved-placeholder gt-df gt-ac gt-sb">
-			<div class="ui grey text gt-df gt-ac gt-fw gt-gap-2">
-				{{svg "octicon-check" 16 "icon gt-mr-2"}}
-				<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
-				{{if $invalid}}
-					<!--
-					We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above).
-					The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl
-					-->
-					<a href="{{$referenceUrl}}" class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
-						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
-					</a>
+{{if len .comments}}
+	{{$comment := index .comments 0}}
+	{{$resolved := $comment.IsResolved}}
+	{{$invalid := $comment.Invalidated}}
+	{{$resolveDoer := $comment.ResolveDoer}}
+	{{$hasReview := and $comment.Review}}
+	{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
+	{{$referenceUrl := printf "%s#%s" $.Issue.Link $comment.HashTag}}
+	<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
+		{{if $resolved}}
+			<div class="ui attached header resolved-placeholder gt-df gt-ac gt-sb">
+				<div class="ui grey text gt-df gt-ac gt-fw gt-gap-2">
+					{{svg "octicon-check" 16 "icon gt-mr-2"}}
+					<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
+					{{if $invalid}}
+						<!--
+						We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above).
+						The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl
+						-->
+						<a href="{{$referenceUrl}}" class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+							{{ctx.Locale.Tr "repo.issues.review.outdated"}}
+						</a>
+					{{end}}
+				</div>
+				<div class="gt-df gt-ac gt-gap-3">
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated gt-df gt-ac">
+						{{svg "octicon-unfold" 16 "gt-mr-3"}}
+						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
+					</button>
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated gt-df gt-ac gt-hidden">
+						{{svg "octicon-fold" 16 "gt-mr-3"}}
+						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
+					</button>
+				</div>
+			</div>
+		{{end}}
+		<div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}gt-hidden{{end}}">
+			<div class="comment-list">
+				<ui class="ui comments">
+					{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
+				</ui>
+			</div>
+			<div class="gt-df gt-je gt-ac gt-fw gt-mt-3">
+				<div class="ui buttons gt-mr-2">
+					<button class="ui icon tiny basic button previous-conversation">
+						{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
+					</button>
+					<button class="ui icon tiny basic button next-conversation">
+						{{svg "octicon-arrow-down" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.next"}}
+					</button>
+				</div>
+				{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
+					<button class="ui icon tiny basic button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
+						{{if $resolved}}
+							{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
+						{{else}}
+							{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
+						{{end}}
+					</button>
+				{{end}}
+				{{if and $.SignedUserID (not $.Repository.IsArchived)}}
+					<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
+						{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+					</button>
 				{{end}}
 			</div>
-			<div class="gt-df gt-ac gt-gap-3">
-				<button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="ui tiny labeled button show-outdated gt-df gt-ac">
-					{{svg "octicon-unfold" 16 "gt-mr-3"}}
-					{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
-				</button>
-				<button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="ui tiny labeled button hide-outdated gt-df gt-ac gt-hidden">
-					{{svg "octicon-fold" 16 "gt-mr-3"}}
-					{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
-				</button>
-			</div>
+			{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $comment.ReviewID "root" $ "comment" $comment}}
 		</div>
-	{{end}}
-	<div id="code-comments-{{(index  .comments 0).ID}}" class="field comment-code-cloud {{if $resolved}}gt-hidden{{end}}">
-		<div class="comment-list">
-			<ui class="ui comments">
-				{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
-			</ui>
-		</div>
-		<div class="gt-df gt-je gt-ac gt-fw gt-mt-3">
-			<div class="ui buttons gt-mr-2">
-				<button class="ui icon tiny basic button previous-conversation">
-					{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
-				</button>
-				<button class="ui icon tiny basic button next-conversation">
-					{{svg "octicon-arrow-down" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.next"}}
-				</button>
-			</div>
-			{{if and $.CanMarkConversation $isNotPending}}
-				<button class="ui icon tiny basic button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
-					{{if $resolved}}
-						{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
-					{{else}}
-						{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
-					{{end}}
-				</button>
-			{{end}}
-			{{if and $.SignedUserID (not $.Repository.IsArchived)}}
-				<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
-					{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
-				</button>
-			{{end}}
-		</div>
-		{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}}
 	</div>
-</div>
+{{else}}
+	{{template "repo/diff/conversation_outdated"}}
+{{end}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index a9c6bbe318..c9170d9746 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -371,27 +371,31 @@
 		{{else if eq .Type 22}}
 			<div class="timeline-item-group" id="{{.HashTag}}">
 				<div class="timeline-item event">
+					{{$reviewType := -1}}
+					{{if .Review}}{{$reviewType = .Review.Type}}{{end}}
 					{{if not .OriginalAuthor}}
 					{{/* Some timeline avatars need a offset to correctly align with their speech
 							bubble. The condition depends on review type and for positive reviews whether
 							there is a comment element or not */}}
-					<a class="timeline-avatar{{if or (and (eq .Review.Type 1) (or .Content .Attachments)) (and (eq .Review.Type 2) (or .Content .Attachments)) (eq .Review.Type 3)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
+					<a class="timeline-avatar{{if or (and (eq $reviewType 1) (or .Content .Attachments)) (and (eq $reviewType 2) (or .Content .Attachments)) (eq $reviewType 3)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
 						{{ctx.AvatarUtils.Avatar .Poster 40}}
 					</a>
 					{{end}}
-					<span class="badge{{if eq .Review.Type 1}} tw-bg-green tw-text-white{{else if eq .Review.Type 3}} tw-bg-red tw-text-white{{end}}">{{svg (printf "octicon-%s" .Review.Type.Icon)}}</span>
+					<span class="badge{{if eq $reviewType 1}} tw-bg-green tw-text-white{{else if eq $reviewType 3}} tw-bg-red tw-text-white{{end}}">
+						{{if .Review}}{{svg (printf "octicon-%s" .Review.Type.Icon)}}{{end}}
+					</span>
 					<span class="text grey muted-links">
 						{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
-						{{if eq .Review.Type 1}}
+						{{if eq $reviewType 1}}
 							{{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}}
-						{{else if eq .Review.Type 2}}
+						{{else if eq $reviewType 2}}
 							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}}
-						{{else if eq .Review.Type 3}}
+						{{else if eq $reviewType 3}}
 							{{ctx.Locale.Tr "repo.issues.review.reject" $createdStr}}
 						{{else}}
 							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}}
 						{{end}}
-						{{if .Review.Dismissed}}
+						{{if and .Review .Review.Dismissed}}
 							<div class="ui small label">{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}</div>
 						{{end}}
 					</span>
@@ -451,7 +455,7 @@
 				</div>
 				{{end}}
 
-				{{if .Review.CodeComments}}
+				{{if and .Review .Review.CodeComments}}
 				<div class="timeline-item event">
 					{{range $filename, $lines := .Review.CodeComments}}
 						{{range $line, $comms := $lines}}
@@ -607,10 +611,12 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
 						{{$reviewerName := ""}}
-						{{if eq .Review.OriginalAuthor ""}}
-							{{$reviewerName = .Review.Reviewer.Name}}
-						{{else}}
-							{{$reviewerName = .Review.OriginalAuthor}}
+						{{if .Review}}
+							{{if eq .Review.OriginalAuthor ""}}
+								{{$reviewerName = .Review.Reviewer.Name}}
+							{{else}}
+								{{$reviewerName = .Review.OriginalAuthor}}
+							{{end}}
 						{{end}}
 						{{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr}}
 					</span>
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index b6e075d0ce..d93589539c 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -1,137 +1,143 @@
-{{$invalid := (index .comments 0).Invalidated}}
-{{$resolved := (index .comments 0).IsResolved}}
-{{$resolveDoer := (index .comments 0).ResolveDoer}}
-{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
-<div class="ui segments conversation-holder">
-	<div class="ui segment collapsible-comment-box gt-py-3 gt-df gt-ac gt-sb">
-		<div class="gt-df gt-ac">
-			<a href="{{(index .comments 0).CodeCommentLink ctx}}" class="file-comment gt-ml-3 gt-word-break">{{(index .comments 0).TreePath}}</a>
-			{{if $invalid}}
-				<span class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
-					{{ctx.Locale.Tr "repo.issues.review.outdated"}}
-				</span>
-			{{end}}
-		</div>
-		<div>
-			{{if or $invalid $resolved}}
-				<button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated gt-df gt-ac">
-					{{svg "octicon-unfold" 16 "gt-mr-3"}}
-					{{if $resolved}}
-						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
-					{{else}}
-						{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
-					{{end}}
-				</button>
-				<button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated gt-df gt-ac">
-					{{svg "octicon-fold" 16 "gt-mr-3"}}
-					{{if $resolved}}
-						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
-					{{else}}
-						{{ctx.Locale.Tr "repo.issues.review.hide_outdated"}}
-					{{end}}
-				</button>
-			{{end}}
-		</div>
-	</div>
-	{{$diff := (CommentMustAsDiff ctx (index .comments 0))}}
-	{{if $diff}}
-		{{$file := (index $diff.Files 0)}}
-		<div id="code-preview-{{(index .comments 0).ID}}" class="ui table segment{{if $resolved}} gt-hidden{{end}}">
-			<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
-				<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
-					<table>
-						<tbody>
-							{{template "repo/diff/section_unified" dict "file" $file "root" $}}
-						</tbody>
-					</table>
-				</div>
-			</div>
-		</div>
-	{{end}}
-	<div id="code-comments-{{(index .comments 0).ID}}" class="comment-code-cloud ui segment{{if $resolved}} gt-hidden{{end}}">
-		<div class="ui comments gt-mb-0">
-			{{range .comments}}
-				{{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}}
-				<div class="comment code-comment gt-pb-4" id="{{.HashTag}}">
-					<div class="content">
-						<div class="header comment-header">
-							<div class="comment-header-left gt-df gt-ac">
-								{{if not .OriginalAuthor}}
-									<a class="avatar">
-										{{ctx.AvatarUtils.Avatar .Poster 20}}
-									</a>
-								{{end}}
-								<span class="text grey muted-links">
-									{{if .OriginalAuthor}}
-										<span class="text black">
-											{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
-											{{.OriginalAuthor}}
-										</span>
-										{{if $.Repository.OriginalURL}}
-										<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
-										{{end}}
-									{{else}}
-										{{template "shared/user/authorlink" .Poster}}
-									{{end}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdSubStr}}
-								</span>
-							</div>
-							<div class="comment-header-right actions gt-df gt-ac">
-								{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
-								{{if not $.Repository.IsArchived}}
-									{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
-									{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
-								{{end}}
-							</div>
-						</div>
-						<div class="text comment-content">
-							<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
-							{{if .RenderedContent}}
-								{{.RenderedContent}}
-							{{else}}
-								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
-							{{end}}
-							</div>
-							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
-							{{if .Attachments}}
-								{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
-							{{end}}
-						</div>
-						{{$reactions := .Reactions.GroupByType}}
-						{{if $reactions}}
-							{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
-						{{end}}
-					</div>
-				</div>
-			{{end}}
-		</div>
-		<div class="code-comment-buttons gt-df gt-ac gt-fw gt-mt-3 gt-mb-2 gt-mx-3">
-			<div class="gt-f1">
-				{{if $resolved}}
-					<div class="ui grey text">
-						{{svg "octicon-check" 16 "gt-mr-2"}}
-						<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
-					</div>
+{{if len .comments}}
+	{{$comment := index .comments 0}}
+	{{$invalid := $comment.Invalidated}}
+	{{$resolved := $comment.IsResolved}}
+	{{$resolveDoer := $comment.ResolveDoer}}
+	{{$hasReview := and $comment.Review}}
+	{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
+	<div class="ui segments conversation-holder">
+		<div class="ui segment collapsible-comment-box gt-py-3 gt-df gt-ac gt-sb">
+			<div class="gt-df gt-ac">
+				<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment gt-ml-3 gt-word-break">{{$comment.TreePath}}</a>
+				{{if $invalid}}
+					<span class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
+					</span>
 				{{end}}
 			</div>
-			<div class="code-comment-buttons-buttons">
-				{{if and $.CanMarkConversation $isNotPending}}
-					<button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
+			<div>
+				{{if or $invalid $resolved}}
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated gt-df gt-ac">
+						{{svg "octicon-unfold" 16 "gt-mr-3"}}
 						{{if $resolved}}
-							{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
+							{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
+							{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
 						{{end}}
 					</button>
-				{{end}}
-				{{if and $.SignedUserID (not $.Repository.IsArchived)}}
-					<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
-						{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated gt-df gt-ac">
+						{{svg "octicon-fold" 16 "gt-mr-3"}}
+						{{if $resolved}}
+							{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
+						{{else}}
+							{{ctx.Locale.Tr "repo.issues.review.hide_outdated"}}
+						{{end}}
 					</button>
 				{{end}}
 			</div>
 		</div>
-		{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}}
+		{{$diff := (CommentMustAsDiff ctx $comment)}}
+		{{if $diff}}
+			{{$file := (index $diff.Files 0)}}
+			<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} gt-hidden{{end}}">
+				<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
+					<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
+						<table>
+							<tbody>
+								{{template "repo/diff/section_unified" dict "file" $file "root" $}}
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		{{end}}
+		<div id="code-comments-{{$comment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} gt-hidden{{end}}">
+			<div class="ui comments gt-mb-0">
+				{{range .comments}}
+					{{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}}
+					<div class="comment code-comment gt-pb-4" id="{{.HashTag}}">
+						<div class="content">
+							<div class="header comment-header">
+								<div class="comment-header-left gt-df gt-ac">
+									{{if not .OriginalAuthor}}
+										<a class="avatar">
+											{{ctx.AvatarUtils.Avatar .Poster 20}}
+										</a>
+									{{end}}
+									<span class="text grey muted-links">
+										{{if .OriginalAuthor}}
+											<span class="text black">
+												{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
+												{{.OriginalAuthor}}
+											</span>
+											{{if $.Repository.OriginalURL}}
+											<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
+											{{end}}
+										{{else}}
+											{{template "shared/user/authorlink" .Poster}}
+										{{end}}
+										{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdSubStr}}
+									</span>
+								</div>
+								<div class="comment-header-right actions gt-df gt-ac">
+									{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
+									{{if not $.Repository.IsArchived}}
+										{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
+										{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
+									{{end}}
+								</div>
+							</div>
+							<div class="text comment-content">
+								<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
+								{{if .RenderedContent}}
+									{{.RenderedContent}}
+								{{else}}
+									<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
+								{{end}}
+								</div>
+								<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
+								<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+								{{if .Attachments}}
+									{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
+								{{end}}
+							</div>
+							{{$reactions := .Reactions.GroupByType}}
+							{{if $reactions}}
+								{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
+							{{end}}
+						</div>
+					</div>
+				{{end}}
+			</div>
+			<div class="code-comment-buttons gt-df gt-ac gt-fw gt-mt-3 gt-mb-2 gt-mx-3">
+				<div class="gt-f1">
+					{{if $resolved}}
+						<div class="ui grey text">
+							{{svg "octicon-check" 16 "gt-mr-2"}}
+							<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
+						</div>
+					{{end}}
+				</div>
+				<div class="code-comment-buttons-buttons">
+					{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
+						<button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
+							{{if $resolved}}
+								{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
+							{{else}}
+								{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
+							{{end}}
+						</button>
+					{{end}}
+					{{if and $.SignedUserID (not $.Repository.IsArchived)}}
+						<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
+							{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+						</button>
+					{{end}}
+				</div>
+			</div>
+			{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $comment.ReviewID "root" $ "comment" $comment}}
+		</div>
 	</div>
-</div>
+{{else}}
+	{{template "repo/diff/conversation_outdated"}}
+{{end}}
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 459aa5a58b..bdfecb3280 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/test"
 	repo_service "code.gitea.io/gitea/services/repository"
 	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
@@ -26,10 +27,19 @@ func TestPullView_ReviewerMissed(t *testing.T) {
 	session := loginUser(t, "user1")
 
 	req := NewRequest(t, "GET", "/pulls")
-	session.MakeRequest(t, req, http.StatusOK)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
 
 	req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
-	session.MakeRequest(t, req, http.StatusOK)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
+
+	// if some reviews are missing, the page shouldn't fail
+	err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{})
+	assert.NoError(t, err)
+	req = NewRequest(t, "GET", "/user2/repo1/pulls/2")
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
 }
 
 func TestPullView_CodeOwner(t *testing.T) {

From 656d8e2267dbdbb595704d507a780533038bb7ed Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Tue, 19 Mar 2024 12:46:40 +0800
Subject: [PATCH 428/679] Fix milestoneID filter bug in issue list (#29897)

Fix #29717
---
 routers/web/repo/issue.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index a0a500f0b2..fb03ed9d9c 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -446,13 +446,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
 	linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
 	ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
 		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
-		mentionedID, projectID, assigneeID, posterID, archived)
+		milestoneID, projectID, assigneeID, posterID, archived)
 	ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
 		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
-		mentionedID, projectID, assigneeID, posterID, archived)
+		milestoneID, projectID, assigneeID, posterID, archived)
 	ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link,
 		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels),
-		mentionedID, projectID, assigneeID, posterID, archived)
+		milestoneID, projectID, assigneeID, posterID, archived)
 	ctx.Data["SelLabelIDs"] = labelIDs
 	ctx.Data["SelectLabels"] = selectLabels
 	ctx.Data["ViewType"] = viewType

From 17d7ab5ad4ce3d0fbc1251572c22687c237a30b1 Mon Sep 17 00:00:00 2001
From: Jimmy Praet <jimmy.praet@telenet.be>
Date: Tue, 19 Mar 2024 06:28:43 +0100
Subject: [PATCH 429/679] Notify reviewers added via CODEOWNERS (#29842)

---
 services/issue/assignee.go | 23 +++++++++++++++---
 services/issue/issue.go    | 24 +++++++++++++------
 services/issue/pull.go     | 49 +++++++++++++++++++++++++++-----------
 services/pull/pull.go      |  7 ++++--
 4 files changed, 77 insertions(+), 26 deletions(-)

diff --git a/services/issue/assignee.go b/services/issue/assignee.go
index b5f472ba53..8740a6664a 100644
--- a/services/issue/assignee.go
+++ b/services/issue/assignee.go
@@ -226,16 +226,33 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
 		return nil, nil
 	}
 
+	return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment)
+}
+
+func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifers []*ReviewRequestNotifier) {
+	for _, reviewNotifer := range reviewNotifers {
+		if reviewNotifer.Reviwer != nil {
+			notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifer.Reviwer, reviewNotifer.IsAdd, reviewNotifer.Comment)
+		} else if reviewNotifer.ReviewTeam != nil {
+			if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifer.ReviewTeam, reviewNotifer.IsAdd, reviewNotifer.Comment); err != nil {
+				log.Error("teamReviewRequestNotify: %v", err)
+			}
+		}
+	}
+}
+
+// teamReviewRequestNotify notify all user in this team
+func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error {
 	// notify all user in this team
 	if err := comment.LoadIssue(ctx); err != nil {
-		return nil, err
+		return err
 	}
 
 	members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
 		TeamID: reviewer.ID,
 	})
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	for _, member := range members {
@@ -246,7 +263,7 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
 		notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment)
 	}
 
-	return comment, err
+	return err
 }
 
 // CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 7662c9d9e8..94b0ee6f69 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -85,17 +85,27 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 		}
 	}
 
-	if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
+	var reviewNotifers []*ReviewRequestNotifier
+
+	if err := db.WithTx(ctx, func(ctx context.Context) error {
+		if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
+			return err
+		}
+
+		if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
+			var err error
+			reviewNotifers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	}); err != nil {
 		return err
 	}
 
-	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
-		if err := PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest); err != nil {
-			return err
-		}
-	}
-
 	notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle)
+	ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers)
 
 	return nil
 }
diff --git a/services/issue/pull.go b/services/issue/pull.go
index 698e2622f5..8e85c11e9b 100644
--- a/services/issue/pull.go
+++ b/services/issue/pull.go
@@ -33,34 +33,41 @@ func getMergeBase(repo *git.Repository, pr *issues_model.PullRequest, baseBranch
 	return mergeBase, err
 }
 
-func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue, pr *issues_model.PullRequest) error {
+type ReviewRequestNotifier struct {
+	Comment    *issues_model.Comment
+	IsAdd      bool
+	Reviwer    *user_model.User
+	ReviewTeam *org_model.Team
+}
+
+func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
 	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
 
 	if pr.IsWorkInProgress(ctx) {
-		return nil
+		return nil, nil
 	}
 
 	if err := pr.LoadHeadRepo(ctx); err != nil {
-		return err
+		return nil, err
 	}
 
 	if pr.HeadRepo.IsFork {
-		return nil
+		return nil, nil
 	}
 
 	if err := pr.LoadBaseRepo(ctx); err != nil {
-		return err
+		return nil, err
 	}
 
 	repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	defer repo.Close()
 
 	commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	var data string
@@ -78,14 +85,14 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue,
 	// get the mergebase
 	mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
 	// between the merge base and the head commit but not the base branch and the head commit
 	changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.HeadCommitID)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	uniqUsers := make(map[int64]*user_model.User)
@@ -103,20 +110,34 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue,
 		}
 	}
 
+	notifiers := make([]*ReviewRequestNotifier, 0, len(uniqUsers)+len(uniqTeams))
+
 	for _, u := range uniqUsers {
 		if u.ID != pull.Poster.ID {
-			if _, err := issues_model.AddReviewRequest(ctx, pull, u, pull.Poster); err != nil {
+			comment, err := issues_model.AddReviewRequest(ctx, pull, u, pull.Poster)
+			if err != nil {
 				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
-				return err
+				return nil, err
 			}
+			notifiers = append(notifiers, &ReviewRequestNotifier{
+				Comment: comment,
+				IsAdd:   true,
+				Reviwer: pull.Poster,
+			})
 		}
 	}
 	for _, t := range uniqTeams {
-		if _, err := issues_model.AddTeamReviewRequest(ctx, pull, t, pull.Poster); err != nil {
+		comment, err := issues_model.AddTeamReviewRequest(ctx, pull, t, pull.Poster)
+		if err != nil {
 			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
-			return err
+			return nil, err
 		}
+		notifiers = append(notifiers, &ReviewRequestNotifier{
+			Comment:    comment,
+			IsAdd:      true,
+			ReviewTeam: t,
+		})
 	}
 
-	return nil
+	return notifiers, nil
 }
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 57873b63ba..8a9c6db917 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -77,6 +77,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
 	}
 	defer baseGitRepo.Close()
 
+	var reviewNotifers []*issue_service.ReviewRequestNotifier
 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 		if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
 			return err
@@ -136,7 +137,8 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
 		}
 
 		if !pr.IsWorkInProgress(ctx) {
-			if err := issue_service.PullRequestCodeOwnersReview(ctx, issue, pr); err != nil {
+			reviewNotifers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
+			if err != nil {
 				return err
 			}
 		}
@@ -150,11 +152,12 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
 	}
 	baseGitRepo.Close() // close immediately to avoid notifications will open the repository again
 
+	issue_service.ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers)
+
 	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
 	if err != nil {
 		return err
 	}
-
 	notify_service.NewPullRequest(ctx, pr, mentions)
 	if len(issue.Labels) > 0 {
 		notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)

From 98217b034076157547cf688cc10f47cd3275c872 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 19 Mar 2024 16:23:40 +0900
Subject: [PATCH 430/679] Fix invalid link of the commit status when ref is tag
 (#29752)

Fix #29731
Caused by #24634
Also remove fixme.

ps: we can not fix the existed runs, as wrong refs are all recorded in
DB, and we can not know whether they are branch or tag:

![image](https://github.com/go-gitea/gitea/assets/18380374/cb7cf266-f73f-419a-be1a-4689fdd1952a)
---
 services/actions/notifier.go              |  16 ++-
 tests/integration/actions_trigger_test.go | 119 ++++++++++++++++++++++
 2 files changed, 132 insertions(+), 3 deletions(-)

diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index aa88d4e0d8..eec5f814da 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -515,6 +515,12 @@ func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.U
 }
 
 func (n *actionsNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
+	commitID, _ := git.NewIDFromString(opts.NewCommitID)
+	if commitID.IsZero() {
+		log.Trace("new commitID is empty")
+		return
+	}
+
 	ctx = withMethod(ctx, "PushCommits")
 
 	apiPusher := convert.ToUser(ctx, pusher, nil)
@@ -547,9 +553,9 @@ func (n *actionsNotifier) CreateRef(ctx context.Context, pusher *user_model.User
 	apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone})
 
 	newNotifyInput(repo, pusher, webhook_module.HookEventCreate).
-		WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name
+		WithRef(refFullName.String()).
 		WithPayload(&api.CreatePayload{
-			Ref:     refFullName.ShortName(),
+			Ref:     refFullName.String(),
 			Sha:     refID,
 			RefType: refFullName.RefType(),
 			Repo:    apiRepo,
@@ -566,7 +572,7 @@ func (n *actionsNotifier) DeleteRef(ctx context.Context, pusher *user_model.User
 
 	newNotifyInput(repo, pusher, webhook_module.HookEventDelete).
 		WithPayload(&api.DeletePayload{
-			Ref:        refFullName.ShortName(),
+			Ref:        refFullName.String(),
 			RefType:    refFullName.RefType(),
 			PusherType: api.PusherTypeUser,
 			Repo:       apiRepo,
@@ -623,6 +629,10 @@ func (n *actionsNotifier) UpdateRelease(ctx context.Context, doer *user_model.Us
 }
 
 func (n *actionsNotifier) DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
+	if rel.IsTag {
+		// has sent same action in `PushCommits`, so skip it.
+		return
+	}
 	ctx = withMethod(ctx, "DeleteRelease")
 	notifyRelease(ctx, doer, rel, api.HookReleaseDeleted)
 }
diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go
index 9a8bfc5db6..2a2fdceb61 100644
--- a/tests/integration/actions_trigger_test.go
+++ b/tests/integration/actions_trigger_test.go
@@ -12,6 +12,7 @@ import (
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
@@ -19,9 +20,11 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	actions_module "code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/test"
 	pull_service "code.gitea.io/gitea/services/pull"
+	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
 	files_service "code.gitea.io/gitea/services/repository/files"
 
@@ -324,3 +327,119 @@ func TestSkipCI(t *testing.T) {
 		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
 	})
 }
+
+func TestCreateDeleteRefEvent(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+		// create the repo
+		repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+			Name:          "create-delete-ref-event",
+			Description:   "test create delete ref ci event",
+			AutoInit:      true,
+			Gitignores:    "Go",
+			License:       "MIT",
+			Readme:        "Default",
+			DefaultBranch: "main",
+			IsPrivate:     false,
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, repo)
+
+		// enable actions
+		err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{
+			RepoID: repo.ID,
+			Type:   unit_model.TypeActions,
+		}}, nil)
+		assert.NoError(t, err)
+
+		// add workflow file to the repo
+		addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      ".gitea/workflows/createdelete.yml",
+					ContentReader: strings.NewReader("name: test\non:\n  [create,delete]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo helloworld\n"),
+				},
+			},
+			Message:   "add workflow",
+			OldBranch: "main",
+			NewBranch: "main",
+			Author: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Committer: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Dates: &files_service.CommitDateOptions{
+				Author:    time.Now(),
+				Committer: time.Now(),
+			},
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, addWorkflowToBaseResp)
+
+		// Get the commit ID of the default branch
+		gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+		assert.NoError(t, err)
+		defer gitRepo.Close()
+		branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
+		assert.NoError(t, err)
+
+		// create a branch
+		err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-create-branch")
+		assert.NoError(t, err)
+		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "create",
+			Ref:        "refs/heads/test-create-branch",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
+
+		// create a tag
+		err = release_service.CreateNewTag(db.DefaultContext, user2, repo, branch.CommitID, "test-create-tag", "test create tag event")
+		assert.NoError(t, err)
+		run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "create",
+			Ref:        "refs/tags/test-create-tag",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
+
+		// delete the branch
+		err = repo_service.DeleteBranch(db.DefaultContext, user2, repo, gitRepo, "test-create-branch")
+		assert.NoError(t, err)
+		run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "delete",
+			Ref:        "main",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
+
+		// delete the tag
+		tag, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "test-create-tag")
+		assert.NoError(t, err)
+		err = release_service.DeleteReleaseByID(db.DefaultContext, repo, tag, user2, true)
+		assert.NoError(t, err)
+		run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "delete",
+			Ref:        "main",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
+	})
+}

From 5a8559ec47a271e45bf5836eaf5e9040a0f1d6bf Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 19 Mar 2024 11:36:54 +0100
Subject: [PATCH 431/679] Fix border on focus in dashboard repo search (#29893)

Before:

<img width="449" alt="Screenshot 2024-03-18 at 22 35 10"
src="https://github.com/go-gitea/gitea/assets/115237/f2893870-e7a3-4e34-b0cf-4610735c9b36">

After:

<img width="453" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/36a9f800-28a4-40fc-b6d2-a2e717ddba01">
---
 web_src/css/base.css | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index ea5ba9ce87..b47de5ad50 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -283,7 +283,9 @@ ol.ui.list li,
 .ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
 .ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
 .ui.action.input:not([class*="left action"]) > input:focus + .button,
-.ui.action.input:not([class*="left action"]) > input:focus + .button:hover {
+.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
   border-left-color: var(--color-primary);
 }
 .ui.action.input:not([class*="left action"]) > input:focus {
@@ -1956,6 +1958,10 @@ table th[data-sortt-desc] .svg {
   justify-content: center;
 }
 
+.ui.icon.input > i.icon {
+  transition: none;
+}
+
 .flex-items-block > .item,
 .flex-text-block {
   display: flex;

From fa100618c4b644346bf5666f92d33dce0747d0a2 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 19 Mar 2024 17:28:46 +0100
Subject: [PATCH 432/679] Forbid jQuery `.css` and refactor all usage (#29852)

Tested all functionality. There is a [pre-existing
bug](https://github.com/go-gitea/gitea/issues/29853) when moving a
project panels which is not caused by this refactoring.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 .eslintrc.yaml                          |   4 +-
 web_src/js/features/colorpicker.js      |   8 +-
 web_src/js/features/comp/ColorPicker.js |  16 ++--
 web_src/js/features/comp/LabelEdit.js   |   6 +-
 web_src/js/features/imagediff.js        | 108 ++++++++++++------------
 web_src/js/features/repo-legacy.js      |   2 +-
 web_src/js/features/repo-projects.js    |   7 +-
 7 files changed, 78 insertions(+), 73 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index b65fe56cf2..72039a6013 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -286,7 +286,7 @@ rules:
   jquery/no-class: [0]
   jquery/no-clone: [2]
   jquery/no-closest: [0]
-  jquery/no-css: [0]
+  jquery/no-css: [2]
   jquery/no-data: [0]
   jquery/no-deferred: [2]
   jquery/no-delegate: [2]
@@ -409,7 +409,7 @@ rules:
   no-jquery/no-constructor-attributes: [2]
   no-jquery/no-contains: [2]
   no-jquery/no-context-prop: [2]
-  no-jquery/no-css: [0]
+  no-jquery/no-css: [2]
   no-jquery/no-data: [0]
   no-jquery/no-deferred: [2]
   no-jquery/no-delegate: [2]
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
index a5fdb3f5a6..df0353376d 100644
--- a/web_src/js/features/colorpicker.js
+++ b/web_src/js/features/colorpicker.js
@@ -1,10 +1,12 @@
-export async function createColorPicker($els) {
-  if (!$els || !$els.length) return;
+import $ from 'jquery';
+
+export async function createColorPicker(els) {
+  if (!els.length) return;
 
   await Promise.all([
     import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors'),
     import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors/jquery.minicolors.css'),
   ]);
 
-  $els.minicolors();
+  return $(els).minicolors();
 }
diff --git a/web_src/js/features/comp/ColorPicker.js b/web_src/js/features/comp/ColorPicker.js
index 5665b7a24a..d7e7038803 100644
--- a/web_src/js/features/comp/ColorPicker.js
+++ b/web_src/js/features/comp/ColorPicker.js
@@ -2,11 +2,15 @@ import $ from 'jquery';
 import {createColorPicker} from '../colorpicker.js';
 
 export function initCompColorPicker() {
-  createColorPicker($('.color-picker'));
+  (async () => {
+    await createColorPicker(document.querySelectorAll('.color-picker'));
 
-  $('.precolors .color').on('click', function () {
-    const color_hex = $(this).data('color-hex');
-    $('.color-picker').val(color_hex);
-    $('.minicolors-swatch-color').css('background-color', color_hex);
-  });
+    for (const el of document.querySelectorAll('.precolors .color')) {
+      el.addEventListener('click', (e) => {
+        const color = e.target.getAttribute('data-color-hex');
+        const parent = e.target.closest('.color.picker');
+        $(parent.querySelector('.color-picker')).minicolors('value', color);
+      });
+    }
+  })();
 }
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index 26800ae05c..44fc9d9b6b 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -43,7 +43,6 @@ export function initCompLabelEdit(selector) {
 
   // Edit label
   $('.edit-label-button').on('click', function () {
-    $('.edit-label .color-picker').minicolors('value', $(this).data('color'));
     $('#label-modal-id').val($(this).data('id'));
 
     const $nameInput = $('.edit-label .label-name-input');
@@ -60,9 +59,8 @@ export function initCompLabelEdit(selector) {
       (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val())));
     updateExclusiveLabelEdit('.edit-label');
 
-    $('.edit-label .label-desc-input').val($(this).data('description'));
-    $('.edit-label .color-picker').val($(this).data('color'));
-    $('.edit-label .minicolors-swatch-color').css('background-color', $(this).data('color'));
+    $('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
+    $('.edit-label .color-picker').minicolors('value', this.getAttribute('data-color'));
 
     $('.edit-label.modal').modal({
       onApprove() {
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index 80b7e83385..293e1f809a 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -133,24 +133,25 @@ export function initImageDiff() {
         $container.find('.bounds-info-before .bounds-info-height').text(`${sizes.image2[0].naturalHeight}px`).addClass(heightChanged ? 'red' : '');
       }
 
-      sizes.image1.css({
-        width: sizes.size1.width * factor,
-        height: sizes.size1.height * factor
-      });
-      sizes.image1.parent().css({
-        margin: `10px auto`,
-        width: sizes.size1.width * factor + 2,
-        height: sizes.size1.height * factor + 2
-      });
-      sizes.image2.css({
-        width: sizes.size2.width * factor,
-        height: sizes.size2.height * factor
-      });
-      sizes.image2.parent().css({
-        margin: `10px auto`,
-        width: sizes.size2.width * factor + 2,
-        height: sizes.size2.height * factor + 2
-      });
+      const image1 = sizes.image1[0];
+      if (image1) {
+        const container = image1.parentNode;
+        image1.style.width = `${sizes.size1.width * factor}px`;
+        image1.style.height = `${sizes.size1.height * factor}px`;
+        container.style.margin = '10px auto';
+        container.style.width = `${sizes.size1.width * factor + 2}px`;
+        container.style.height = `${sizes.size1.height * factor + 2}px`;
+      }
+
+      const image2 = sizes.image2[0];
+      if (image2) {
+        const container = image2.parentNode;
+        image2.style.width = `${sizes.size2.width * factor}px`;
+        image2.style.height = `${sizes.size2.height * factor}px`;
+        container.style.margin = '10px auto';
+        container.style.width = `${sizes.size2.width * factor + 2}px`;
+        container.style.height = `${sizes.size2.height * factor + 2}px`;
+      }
     }
 
     function initSwipe(sizes) {
@@ -159,36 +160,39 @@ export function initImageDiff() {
         factor = (diffContainerWidth - 12) / sizes.max.width;
       }
 
-      sizes.image1.css({
-        width: sizes.size1.width * factor,
-        height: sizes.size1.height * factor
-      });
-      sizes.image1.parent().css({
-        margin: `0px ${sizes.ratio[0] * factor}px`,
-        width: sizes.size1.width * factor + 2,
-        height: sizes.size1.height * factor + 2
-      });
-      sizes.image1.parent().parent().css({
-        padding: `${sizes.ratio[1] * factor}px 0 0 0`,
-        width: sizes.max.width * factor + 2
-      });
-      sizes.image2.css({
-        width: sizes.size2.width * factor,
-        height: sizes.size2.height * factor
-      });
-      sizes.image2.parent().css({
-        margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
-        width: sizes.size2.width * factor + 2,
-        height: sizes.size2.height * factor + 2
-      });
-      sizes.image2.parent().parent().css({
-        width: sizes.max.width * factor + 2,
-        height: sizes.max.height * factor + 2
-      });
-      $container.find('.diff-swipe').css({
-        width: sizes.max.width * factor + 2,
-        height: sizes.max.height * factor + 30 /* extra height for inner "position: absolute" elements */,
-      });
+      const image1 = sizes.image1[0];
+      if (image1) {
+        const container = image1.parentNode;
+        const swipeFrame = container.parentNode;
+        image1.style.width = `${sizes.size1.width * factor}px`;
+        image1.style.height = `${sizes.size1.height * factor}px`;
+        container.style.margin = `0px ${sizes.ratio[0] * factor}px`;
+        container.style.width = `${sizes.size1.width * factor + 2}px`;
+        container.style.height = `${sizes.size1.height * factor + 2}px`;
+        swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`;
+        swipeFrame.style.width = `${sizes.max.width * factor + 2}px`;
+      }
+
+      const image2 = sizes.image2[0];
+      if (image2) {
+        const container = image2.parentNode;
+        const swipeFrame = container.parentNode;
+        image2.style.width = `${sizes.size2.width * factor}px`;
+        image2.style.height = `${sizes.size2.height * factor}px`;
+        container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
+        container.style.width = `${sizes.size2.width * factor + 2}px`;
+        container.style.height = `${sizes.size2.height * factor + 2}px`;
+        swipeFrame.style.width = `${sizes.max.width * factor + 2}px`;
+        swipeFrame.style.height = `${sizes.max.height * factor + 2}px`;
+      }
+
+      // extra height for inner "position: absolute" elements
+      const swipe = $container.find('.diff-swipe')[0];
+      if (swipe) {
+        swipe.style.width = `${sizes.max.width * factor + 2}px`;
+        swipe.style.height = `${sizes.max.height * factor + 30}px`;
+      }
+
       $container.find('.swipe-bar').on('mousedown', function(e) {
         e.preventDefault();
 
@@ -200,13 +204,9 @@ export function initImageDiff() {
           e2.preventDefault();
 
           const value = Math.max(0, Math.min(e2.clientX - $swipeFrame.offset().left, width));
+          $swipeBar[0].style.left = `${value}px`;
+          $container.find('.swipe-container')[0].style.width = `${$swipeFrame.width() - value}px`;
 
-          $swipeBar.css({
-            left: value
-          });
-          $container.find('.swipe-container').css({
-            width: $swipeFrame.width() - value
-          });
           $(document).on('mouseup.diff-swipe', () => {
             $(document).off('.diff-swipe');
           });
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 96cfa78d0b..5afb407223 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -389,7 +389,7 @@ async function onEditContent(event) {
               dz.emit('complete', attachment);
               dz.files.push(attachment);
               fileUuidDict[attachment.uuid] = {submitted: true};
-              $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
+              $dropzone.find(`img[src='${imgSrc}']`)[0].style.maxWidth = '100%';
               const $input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
               $dropzone.find('.files').append($input);
             }
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index 1f86711ab1..e95d875ec4 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -72,7 +72,7 @@ async function initRepoProjectSortable() {
             await PUT($(column).data('url'), {
               data: {
                 sorting: i,
-                color: rgbToHex($(column).css('backgroundColor')),
+                color: rgbToHex(window.getComputedStyle($(column)[0]).backgroundColor),
               },
             });
           } catch (error) {
@@ -111,8 +111,9 @@ export function initRepoProject() {
     const $projectColorInput = $(this).find('#new_project_column_color');
     const $boardColumn = $(this).closest('.project-column');
 
-    if ($boardColumn.css('backgroundColor')) {
-      setLabelColor($projectHeader, rgbToHex($boardColumn.css('backgroundColor')));
+    const bgColor = $boardColumn[0].style.backgroundColor;
+    if (bgColor) {
+      setLabelColor($projectHeader, rgbToHex(bgColor));
     }
 
     $(this).find('.edit-project-column-button').on('click', async function (e) {

From 8bf4173e31a4018fb277c871df7d8d31c98dba0b Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 20 Mar 2024 01:08:42 +0200
Subject: [PATCH 433/679] Switch to the maintained vitest extension (#29914)

https://marketplace.visualstudio.com/items?itemName=zixuanchen.vitest-explorer
was moved to
https://marketplace.visualstudio.com/items?itemName=vitest.explorer

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 .devcontainer/devcontainer.json | 4 ++--
 .gitpod.yml                     | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index a4a6ce8fcf..d391cf78cf 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -4,7 +4,7 @@
   "features": {
     // installs nodejs into container
     "ghcr.io/devcontainers/features/node:1": {
-      "version":"20"
+      "version": "20"
     },
     "ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
     "ghcr.io/devcontainers-contrib/features/poetry:2": {},
@@ -24,7 +24,7 @@
         "DavidAnson.vscode-markdownlint",
         "Vue.volar",
         "ms-azuretools.vscode-docker",
-        "zixuanchen.vitest-explorer",
+        "vitest.explorer",
         "qwtel.sqlite-viewer",
         "GitHub.vscode-pull-request-github"
       ]
diff --git a/.gitpod.yml b/.gitpod.yml
index ed2f57f4bf..f573d55a76 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -42,7 +42,7 @@ vscode:
     - DavidAnson.vscode-markdownlint
     - Vue.volar
     - ms-azuretools.vscode-docker
-    - zixuanchen.vitest-explorer
+    - vitest.explorer
     - qwtel.sqlite-viewer
     - GitHub.vscode-pull-request-github
 

From 55a8f4510af5ef67e484d45dd3789c29f120d58e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 20 Mar 2024 01:39:36 +0200
Subject: [PATCH 434/679] Remove jQuery `.attr` from the issue author dropdown
 (#29915)

- Switched from jQuery `.attr` to plain javascript `.getAttribute`
- Tested the issue author dropdown functionality and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-issue-list.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 880ecf9489..48658fd723 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -93,9 +93,9 @@ function initRepoIssueListAuthorDropdown() {
   const $searchDropdown = $('.user-remote-search');
   if (!$searchDropdown.length) return;
 
-  let searchUrl = $searchDropdown.attr('data-search-url');
-  const actionJumpUrl = $searchDropdown.attr('data-action-jump-url');
-  const selectedUserId = $searchDropdown.attr('data-selected-user-id');
+  let searchUrl = $searchDropdown[0].getAttribute('data-search-url');
+  const actionJumpUrl = $searchDropdown[0].getAttribute('data-action-jump-url');
+  const selectedUserId = $searchDropdown[0].getAttribute('data-selected-user-id');
   if (!searchUrl.includes('?')) searchUrl += '?';
 
   $searchDropdown.dropdown('setting', {

From 4cfda0241993eabb50ed937dab2ffa7deff3d36e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 20 Mar 2024 01:44:21 +0200
Subject: [PATCH 435/679] Remove jQuery `.attr` from the quick pull request
 button text (#29916)

- Switched from jQuery `.attr` to plain javascript `.getAttribute`
- Tested the quick pull request button text change functionality and it
works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-editor.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index ba00573c07..1ab0a57865 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -72,7 +72,7 @@ export function initRepoEditor() {
       hideElem($('.quick-pull-branch-name'));
       document.querySelector('.quick-pull-branch-name input').required = false;
     }
-    $('#commit-button').text($(this).attr('button_text'));
+    $('#commit-button').text(this.getAttribute('button_text'));
   });
 
   const joinTreePath = ($fileNameEl) => {

From cb98e27992cae7d62e527e922cae3c0f738a7cab Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 20 Mar 2024 01:49:15 +0200
Subject: [PATCH 436/679] Remove jQuery `.attr` from the image diff (#29917)

- Switched from jQuery `.attr` to plain javascript `.setAttribute`
- Tested the image diff functionality and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/imagediff.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index 293e1f809a..2bac13b0bf 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -70,7 +70,7 @@ export function initImageDiff() {
 
   $('.image-diff:not([data-image-diff-loaded])').each(async function() {
     const $container = $(this);
-    $container.attr('data-image-diff-loaded', 'true');
+    this.setAttribute('data-image-diff-loaded', 'true');
 
     // the container may be hidden by "viewed" checkbox, so use the parent's width for reference
     const diffContainerWidth = Math.max($container.closest('.diff-file-box').width() - 300, 100);

From dd043854ee8963057daa7b835fc700731ae5fb9e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 20 Mar 2024 02:04:24 +0200
Subject: [PATCH 437/679] Remove jQuery `.attr` from the archive download and
 compare page branch selector (#29918)

- Switched from jQuery `.attr` to plain javascript `.getAttribute`
- Tested the archive download and compare page branch selector
functionality and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-common.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js
index a8221bbea8..9e11ffe197 100644
--- a/web_src/js/features/repo-common.js
+++ b/web_src/js/features/repo-common.js
@@ -34,7 +34,7 @@ async function getArchive($target, url, first) {
 export function initRepoArchiveLinks() {
   $('.archive-link').on('click', function (event) {
     event.preventDefault();
-    const url = $(this).attr('href');
+    const url = this.getAttribute('href');
     if (!url) return;
     getArchive($(event.target), url, true);
   });
@@ -80,10 +80,10 @@ export function initRepoCommonFilterSearchDropdown(selector) {
     fullTextSearch: 'exact',
     selectOnKeydown: false,
     onChange(_text, _value, $choice) {
-      if ($choice.attr('data-url')) {
-        window.location.href = $choice.attr('data-url');
+      if ($choice[0].getAttribute('data-url')) {
+        window.location.href = $choice[0].getAttribute('data-url');
       }
     },
-    message: {noResults: $dropdown.attr('data-no-results')},
+    message: {noResults: $dropdown[0].getAttribute('data-no-results')},
   });
 }

From adc61c5d71651acc316bbc3a7fee6f2c0c60b06e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 20 Mar 2024 02:09:52 +0200
Subject: [PATCH 438/679] Remove jQuery `.attr` from the user search box
 (#29919)

- Switched from jQuery `.attr` to plain javascript `.getAttribute`
- Tested the user search box and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/comp/SearchUserBox.js | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js
index 992d4ef020..541052c174 100644
--- a/web_src/js/features/comp/SearchUserBox.js
+++ b/web_src/js/features/comp/SearchUserBox.js
@@ -5,9 +5,12 @@ const {appSubUrl} = window.config;
 const looksLikeEmailAddressCheck = /^\S+@\S+$/;
 
 export function initCompSearchUserBox() {
-  const $searchUserBox = $('#search-user-box');
-  const allowEmailInput = $searchUserBox.attr('data-allow-email') === 'true';
-  const allowEmailDescription = $searchUserBox.attr('data-allow-email-description');
+  const searchUserBox = document.getElementById('search-user-box');
+  if (!searchUserBox) return;
+
+  const $searchUserBox = $(searchUserBox);
+  const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true';
+  const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined;
   $searchUserBox.search({
     minCharacters: 2,
     apiSettings: {

From 6ed2c29b1459daa6e7b0cf63b9c08fd9c001ac09 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 20 Mar 2024 02:38:16 +0200
Subject: [PATCH 439/679] Don't lock using GitHub actions (#29913)

We have our bot for this. See:
- https://github.com/GiteaBot/gitea-backporter?tab=readme-ov-file#locks
- https://github.com/GiteaBot/gitea-backporter/blob/main/src/lock.ts

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 .github/workflows/cron-lock.yml | 23 -----------------------
 1 file changed, 23 deletions(-)
 delete mode 100644 .github/workflows/cron-lock.yml

diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml
deleted file mode 100644
index 665313135b..0000000000
--- a/.github/workflows/cron-lock.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: cron-lock
-
-on:
-  schedule:
-    - cron: "0 0 * * *" # every day at 00:00 UTC
-  workflow_dispatch:
-
-permissions:
-  issues: write
-  pull-requests: write
-
-concurrency:
-  group: lock
-
-jobs:
-  action:
-    runs-on: ubuntu-latest
-    if: github.repository == 'go-gitea/gitea'
-    steps:
-      - uses: dessant/lock-threads@v5
-        with:
-          issue-inactive-days: 10
-          pr-inactive-days: 7

From f371f84fa3456c2a71470632b6458d81e4892a54 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Wed, 20 Mar 2024 09:45:27 +0800
Subject: [PATCH 440/679] Restore deleted branches when syncing (#29898)

Regression of #29493. If a branch has been deleted, repushing it won't
restore it.

Lunny may have noticed that, but I didn't delve into the comment then
overlooked it:
https://github.com/go-gitea/gitea/pull/29493#discussion_r1509046867

The additional comments added are to explain the issue I found during
testing, which are unrelated to the fixes.
---
 routers/private/hook_post_receive.go |  8 +++++---
 services/repository/branch.go        |  4 ++--
 tests/integration/git_push_test.go   | 17 +++++++++++++++++
 3 files changed, 24 insertions(+), 5 deletions(-)

diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 3dad39f7b1..a09956f738 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -75,6 +75,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 			updates = append(updates, option)
 			if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
 				// put the master/main branch first
+				// FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
+				//        If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
+				//        See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
+				//        If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
 				copy(updates[1:], updates)
 				updates[0] = option
 			}
@@ -129,9 +133,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 				commitIDs = append(commitIDs, update.NewCommitID)
 			}
 
-			if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, func(commitID string) (*git.Commit, error) {
-				return gitRepo.GetCommit(commitID)
-			}); err != nil {
+			if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
 				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
 					Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
 				})
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 8d8cfa2d19..db7acdb505 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -318,11 +318,11 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
 		for i, branchName := range branchNames {
 			commitID := commitIDs[i]
 			branch, exist := branchMap[branchName]
-			if exist && branch.CommitID == commitID {
+			if exist && branch.CommitID == commitID && !branch.IsDeleted {
 				continue
 			}
 
-			commit, err := getCommit(branchName)
+			commit, err := getCommit(commitID)
 			if err != nil {
 				return fmt.Errorf("get commit of %s failed: %v", branchName, err)
 			}
diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go
index cb2910b175..0a35724807 100644
--- a/tests/integration/git_push_test.go
+++ b/tests/integration/git_push_test.go
@@ -69,6 +69,23 @@ func testGitPush(t *testing.T, u *url.URL) {
 			return pushed, deleted
 		})
 	})
+
+	t.Run("Push to deleted branch", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete
+			pushed = append(pushed, "master")
+
+			doGitCreateBranch(gitPath, "branch-1")(t)
+			doGitPushTestRepository(gitPath, "origin", "branch-1")(t)
+			pushed = append(pushed, "branch-1")
+
+			// delete and restore
+			doGitPushTestRepository(gitPath, "origin", "--delete", "branch-1")(t)
+			doGitPushTestRepository(gitPath, "origin", "branch-1")(t)
+
+			return pushed, deleted
+		})
+	})
 }
 
 func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gitPath string) (pushed, deleted []string)) {

From 02bbdd478706032f3d4948333aa4ea38e33f8e32 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 20 Mar 2024 12:26:19 +0800
Subject: [PATCH 441/679] Fix the wrong default value of ENABLE_OPENID_SIGNIN
 on docs (#29925)

Fix #29923
---
 docs/content/administration/config-cheat-sheet.en-us.md | 2 +-
 docs/content/administration/config-cheat-sheet.zh-cn.md | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 04923acdcb..2309021f94 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -590,7 +590,7 @@ And the following unique queues:
 
 ## OpenID (`openid`)
 
-- `ENABLE_OPENID_SIGNIN`: **false**: Allow authentication in via OpenID.
+- `ENABLE_OPENID_SIGNIN`: **true**: Allow authentication in via OpenID.
 - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**: Allow registering via OpenID.
 - `WHITELISTED_URIS`: **_empty_**: If non-empty, list of POSIX regex patterns matching
    OpenID URI's to permit.
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 1f98db78aa..3115e4cc06 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -562,7 +562,7 @@ Gitea 创建以下非唯一队列:
 
 ## OpenID (`openid`)
 
-- `ENABLE_OPENID_SIGNIN`: **false**:允许通过OpenID进行身份验证。
+- `ENABLE_OPENID_SIGNIN`: **true**:允许通过OpenID进行身份验证。
 - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**:允许通过OpenID进行注册。
 - `WHITELISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于允许访问。
 - `BLACKLISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于阻止访问。

From 35cfd98e121005f90f6d0d55d9a69e37d7990010 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Wed, 20 Mar 2024 12:59:01 +0800
Subject: [PATCH 442/679] Show Actions post step when it's running (#29926)

The post step was always waiting, even if all steps were done. Then,
once the task was done, the post step became success immediately.

Before:

<img width="915" alt="xnip_240320_120228"
src="https://github.com/go-gitea/gitea/assets/9418365/00347430-f998-4c43-917a-bf6dd6d0e333">

After:

<img width="905" alt="xnip_240320_120443"
src="https://github.com/go-gitea/gitea/assets/9418365/a419b111-17c2-4029-a022-c761cc419091">
---
 modules/actions/task_state.go      | 14 ++++++++++--
 modules/actions/task_state_test.go | 34 ++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go
index fe925bbb5d..31a74be3fd 100644
--- a/modules/actions/task_state.go
+++ b/modules/actions/task_state.go
@@ -41,6 +41,12 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
 	}
 	logIndex += preStep.LogLength
 
+	// lastHasRunStep is the last step that has run.
+	// For example,
+	// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
+	// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
+	// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
+	// So its Stopped is the Started of postStep when there are no more steps to run.
 	var lastHasRunStep *actions_model.ActionTaskStep
 	for _, step := range task.Steps {
 		if step.Status.HasRun() {
@@ -56,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
 		Name:   postStepName,
 		Status: actions_model.StatusWaiting,
 	}
-	if task.Status.IsDone() {
+	// If the lastHasRunStep is the last step, or it has failed, postStep has started.
+	if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] {
 		postStep.LogIndex = logIndex
 		postStep.LogLength = task.LogLength - postStep.LogIndex
-		postStep.Status = task.Status
 		postStep.Started = lastHasRunStep.Stopped
+		postStep.Status = actions_model.StatusRunning
+	}
+	if task.Status.IsDone() {
+		postStep.Status = task.Status
 		postStep.Stopped = task.Stopped
 	}
 	ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2)
diff --git a/modules/actions/task_state_test.go b/modules/actions/task_state_test.go
index 3a599fbcbd..28213d781b 100644
--- a/modules/actions/task_state_test.go
+++ b/modules/actions/task_state_test.go
@@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) {
 				{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100},
 			},
 		},
+		{
+			name: "all steps finished but task is running",
+			task: &actions_model.ActionTask{
+				Steps: []*actions_model.ActionTaskStep{
+					{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
+				},
+				Status:    actions_model.StatusRunning,
+				Started:   10000,
+				Stopped:   0,
+				LogLength: 100,
+			},
+			want: []*actions_model.ActionTaskStep{
+				{Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010},
+				{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
+				{Name: postStepName, Status: actions_model.StatusRunning, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 0},
+			},
+		},
+		{
+			name: "skipped task",
+			task: &actions_model.ActionTask{
+				Steps: []*actions_model.ActionTaskStep{
+					{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+				},
+				Status:    actions_model.StatusSkipped,
+				Started:   0,
+				Stopped:   0,
+				LogLength: 0,
+			},
+			want: []*actions_model.ActionTaskStep{
+				{Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+				{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+				{Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {

From 4c476fa41dc29dc24afda0925023ae3d0b9707cd Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 20 Mar 2024 13:56:42 +0800
Subject: [PATCH 443/679] Remove unnecessary ".Link" usages (#29909)

In HTML, `?key=val` already means "use the current link with new query parameters"
---
 templates/admin/emails/list.tmpl              |  8 +--
 templates/admin/org/list.tmpl                 | 12 ++--
 templates/explore/repo_list.tmpl              |  2 +-
 templates/explore/search.tmpl                 |  8 +--
 templates/projects/list.tmpl                  | 10 ++--
 templates/repo/actions/list.tmpl              | 12 ++--
 templates/repo/diff/box.tmpl                  |  6 +-
 templates/repo/issue/filter_list.tmpl         | 58 +++++++++----------
 templates/repo/issue/labels/label_list.tmpl   |  8 +--
 .../repo/issue/milestone/filter_list.tmpl     | 12 ++--
 templates/repo/view_file.tmpl                 |  2 +-
 templates/shared/issuelist.tmpl               |  2 +-
 templates/shared/search/code/results.tmpl     |  2 +-
 templates/user/dashboard/issues.tmpl          | 16 ++---
 templates/user/dashboard/milestones.tmpl      | 14 ++---
 .../notification_subscriptions.tmpl           | 28 ++++-----
 templates/user/settings/keys_gpg.tmpl         |  2 +-
 templates/user/settings/keys_ssh.tmpl         |  2 +-
 tests/integration/explore_user_test.go        | 12 ++--
 19 files changed, 108 insertions(+), 108 deletions(-)

diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 1e552fba88..660df55999 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -15,10 +15,10 @@
 					</span>
 					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 					<div class="menu">
-						<a class="{{if or (eq .SortType "email") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=email&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email"}}</a>
-						<a class="{{if eq .SortType "reverseemail"}}active {{end}}item" href="{{$.Link}}?sort=reverseemail&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email_reverse"}}</a>
-						<a class="{{if eq .SortType "username"}}active {{end}}item" href="{{$.Link}}?sort=username&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name"}}</a>
-						<a class="{{if eq .SortType "reverseusername"}}active {{end}}item" href="{{$.Link}}?sort=reverseusername&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name_reverse"}}</a>
+						<a class="{{if or (eq .SortType "email") (not .SortType)}}active {{end}}item" href="?sort=email&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email"}}</a>
+						<a class="{{if eq .SortType "reverseemail"}}active {{end}}item" href="?sort=reverseemail&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email_reverse"}}</a>
+						<a class="{{if eq .SortType "username"}}active {{end}}item" href="?sort=username&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name"}}</a>
+						<a class="{{if eq .SortType "reverseusername"}}active {{end}}item" href="?sort=reverseusername&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name_reverse"}}</a>
 					</div>
 				</div>
 			</div>
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index efb0a8847e..4609d1b8b4 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -18,12 +18,12 @@
 					</span>
 					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 					<div class="menu">
-						<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-						<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-						<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-						<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-						<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-						<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+						<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+						<a class="{{if eq .SortType "newest"}}active {{end}}item" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+						<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+						<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
+						<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+						<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
 					</div>
 				</div>
 			</div>
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index 17c8bcb6a3..7de3df5bee 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -32,7 +32,7 @@
 					</div>
 					<div class="flex-item-trailing">
 						{{if .PrimaryLanguage}}
-							<a class="muted" href="{{$.Link}}?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
+							<a class="muted" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
 								<span class="flex-text-inline"><i class="color-icon gt-mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span>
 							</a>
 						{{end}}
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index 54d995989a..c1d114125e 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -13,10 +13,10 @@
 		</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 		<div class="menu">
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
+			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
 		</div>
 	</div>
 </div>
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index 414c9dca2e..d87e7e0663 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -1,11 +1,11 @@
 {{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
 	<div class="gt-df gt-sb gt-mb-4">
 		<div class="small-menu-items ui compact tiny menu list-header-toggle">
-			<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=open&q={{$.Keyword}}">
+			<a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}">
 				{{svg "octicon-project-symlink" 16 "gt-mr-3"}}
 				{{ctx.Locale.PrettyNumber .OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 			</a>
-			<a class="item{{if .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=closed&q={{$.Keyword}}">
+			<a class="item{{if .IsShowClosed}} active{{end}}" href="?state=closed&q={{$.Keyword}}">
 				{{svg "octicon-check" 16 "gt-mr-3"}}
 				{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 			</a>
@@ -31,9 +31,9 @@
 		</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 		<div class="menu">
-			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index 62d30305b3..55c0494566 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -8,9 +8,9 @@
 		<div class="ui stackable grid">
 			<div class="four wide column">
 				<div class="ui fluid vertical menu">
-					<a class="item{{if not $.CurWorkflow}} active{{end}}" href="{{$.Link}}?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
+					<a class="item{{if not $.CurWorkflow}} active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
 					{{range .workflows}}
-						<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="{{$.Link}}?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
+						<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
 							{{if .ErrMsg}}
 								<span data-tooltip-content="{{.ErrMsg}}">
 									{{svg "octicon-alert" 16 "text red"}}
@@ -35,11 +35,11 @@
 								<i class="icon">{{svg "octicon-search"}}</i>
 								<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.actor"}}">
 							</div>
-							<a class="item{{if not $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
+							<a class="item{{if not $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
 								{{ctx.Locale.Tr "actions.runs.actors_no_select"}}
 							</a>
 							{{range .Actors}}
-								<a class="item{{if eq .ID $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}">
+								<a class="item{{if eq .ID $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}">
 									{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
 								</a>
 							{{end}}
@@ -54,11 +54,11 @@
 								<i class="icon">{{svg "octicon-search"}}</i>
 								<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.status"}}">
 							</div>
-							<a class="item{{if not $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0">
+							<a class="item{{if not $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0">
 								{{ctx.Locale.Tr "actions.runs.status_no_select"}}
 							</a>
 							{{range .StatusInfoList}}
-								<a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}">
+								<a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}">
 									{{.DisplayedStatus}}
 								</a>
 							{{end}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 39df6faea5..d2ee5db1b8 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -68,7 +68,7 @@
 				binaryFileMessage: "{{ctx.Locale.Tr "repo.diff.bin"}}",
 				showMoreMessage: "{{ctx.Locale.Tr "repo.diff.show_more"}}",
 				statisticsMessage: "{{ctx.Locale.Tr "repo.diff.stats_desc_file"}}",
-				linkLoadMore: "{{$.Link}}?skip-to={{.Diff.End}}&file-only=true",
+				linkLoadMore: "?skip-to={{.Diff.End}}&file-only=true",
 			};
 
 			// for first time loading, the diffFileInfo is a plain object
@@ -184,7 +184,7 @@
 												{{ctx.Locale.Tr "repo.diff.file_suppressed_line_too_long"}}
 											{{else}}
 												{{ctx.Locale.Tr "repo.diff.file_suppressed"}}
-												<a class="ui basic tiny button diff-load-button" data-href="{{$.Link}}?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{ctx.Locale.Tr "repo.diff.load"}}</a>
+												<a class="ui basic tiny button diff-load-button" data-href="?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{ctx.Locale.Tr "repo.diff.load"}}</a>
 											{{end}}
 										{{else}}
 											{{ctx.Locale.Tr "repo.diff.bin_not_shown"}}
@@ -220,7 +220,7 @@
 					<div class="diff-file-box diff-box file-content gt-mt-3" id="diff-incomplete">
 						<h4 class="ui top attached normal header gt-df gt-ac gt-sb">
 							{{ctx.Locale.Tr "repo.diff.too_many_files"}}
-							<a class="ui basic tiny button" id="diff-show-more-files" data-href="{{$.Link}}?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
+							<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
 						</h4>
 					</div>
 				{{end}}
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index bb13f63b98..696b7db46b 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -23,8 +23,8 @@
 		</div>
 		<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
 		<div class="divider"></div>
-		<a class="{{if .AllLabels}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
-		<a class="{{if .NoLabel}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
+		<a class="{{if .AllLabels}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
+		<a class="{{if .NoLabel}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
 		{{$previousExclusiveScope := "_no_scope"}}
 		{{range .Labels}}
 			{{$exclusiveScope := .ExclusiveScope}}
@@ -32,7 +32,7 @@
 				<div class="divider"></div>
 			{{end}}
 			{{$previousExclusiveScope = $exclusiveScope}}
-			<a class="item label-filter-item gt-df gt-ac" {{if .IsArchived}}data-is-archived{{end}} href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
+			<a class="item label-filter-item gt-df gt-ac" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
 				{{if .IsExcluded}}
 					{{svg "octicon-circle-slash"}}
 				{{else if .IsSelected}}
@@ -62,13 +62,13 @@
 			<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
 		</div>
 		<div class="divider"></div>
-		<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
-		<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
+		<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
+		<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
 		{{if .OpenMilestones}}
 			<div class="divider"></div>
 			<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
 			{{range .OpenMilestones}}
-			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 				{{svg "octicon-milestone" 16 "mr-2"}}
 				{{.Name}}
 			</a>
@@ -78,7 +78,7 @@
 			<div class="divider"></div>
 			<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
 			{{range .ClosedMilestones}}
-			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 				{{svg "octicon-milestone" 16 "mr-2"}}
 				{{.Name}}
 			</a>
@@ -99,15 +99,15 @@
 			<i class="icon">{{svg "octicon-search" 16}}</i>
 			<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_project"}}">
 		</div>
-		<a class="{{if not .ProjectID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a>
-		<a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&project=-1&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a>
+		<a class="{{if not .ProjectID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a>
+		<a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&project=-1&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a>
 		{{if .OpenProjects}}
 			<div class="divider"></div>
 			<div class="header">
 				{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
 			</div>
 			{{range .OpenProjects}}
-				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item gt-df" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item gt-df" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 					{{svg .IconName 18 "gt-mr-3 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
 				</a>
 			{{end}}
@@ -118,7 +118,7 @@
 				{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
 			</div>
 			{{range .ClosedProjects}}
-				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 					{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
 				</a>
 			{{end}}
@@ -130,7 +130,7 @@
 <div class="ui dropdown jump item user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.author_search_tooltip"}}"
 	data-search-url="{{if .Milestone}}{{$.RepoLink}}/issues/posters{{else}}{{$.Link}}/posters{{end}}"
 	data-selected-user-id="{{$.PosterID}}"
-	data-action-jump-url="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={user_id}{{if $.ShowArchivedLabels}}&archived=true{{end}}"
+	data-action-jump-url="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={user_id}{{if $.ShowArchivedLabels}}&archived=true{{end}}"
 >
 	<span class="text">
 		{{ctx.Locale.Tr "repo.issues.filter_poster"}}
@@ -156,11 +156,11 @@
 			<i class="icon">{{svg "octicon-search" 16}}</i>
 			<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignee"}}">
 		</div>
-		<a class="{{if not .AssigneeID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a>
-		<a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a>
+		<a class="{{if not .AssigneeID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a>
+		<a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a>
 		<div class="divider"></div>
 		{{range .Assignees}}
-			<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item gt-df" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+			<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item gt-df" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 				{{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}}
 			</a>
 		{{end}}
@@ -175,14 +175,14 @@
 		</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 		<div class="menu">
-			<a class="{{if eq .ViewType "all"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a>
-			<a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
-			<a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a>
+			<a class="{{if eq .ViewType "all"}}active {{end}}item" href="?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a>
+			<a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
+			<a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a>
 			{{if .PageIsPullList}}
-				<a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a>
-				<a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=reviewed_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a>
+				<a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a>
+				<a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=reviewed_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a>
 			{{end}}
-			<a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a>
+			<a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a>
 		</div>
 	</div>
 {{end}}
@@ -194,13 +194,13 @@
 	</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 	<div class="menu">
-		<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-		<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-		<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-		<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-		<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
-		<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
-		<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
-		<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
+		<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+		<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+		<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+		<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+		<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+		<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+		<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+		<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
 	</div>
 </div>
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index ca8d40f1e0..ca28e3af2d 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -9,10 +9,10 @@
 				</span>
 				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 				<div class="left menu">
-					<a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-					<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-					<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
-					<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?sort=mostissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+					<a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+					<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
+					<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+					<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
 				</div>
 			</div>
 		</div>
diff --git a/templates/repo/issue/milestone/filter_list.tmpl b/templates/repo/issue/milestone/filter_list.tmpl
index 0eea42d6ee..45f9866a16 100644
--- a/templates/repo/issue/milestone/filter_list.tmpl
+++ b/templates/repo/issue/milestone/filter_list.tmpl
@@ -5,11 +5,11 @@
 	</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 	<div class="menu">
-		<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
-		<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
-		<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="{{$.Link}}?sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
-		<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="{{$.Link}}?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
-		<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
-		<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+		<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
+		<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
+		<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
+		<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
+		<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+		<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
 	</div>
 </div>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index cdd415a47a..8c1e7982eb 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -37,7 +37,7 @@
 		<div class="file-header-right file-actions gt-df gt-ac gt-fw">
 			{{if .HasSourceRenderedToggle}}
 				<div class="ui compact icon buttons">
-					<a href="{{$.Link}}?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
+					<a href="?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
 					<a href="{{$.Link}}" class="ui mini basic button {{if .IsDisplayingRendered}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_rendered"}}">{{svg "octicon-file" 15}}</a>
 				</div>
 			{{end}}
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index fb502413fa..adb2f61c54 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -21,7 +21,7 @@
 						{{end}}
 						<span class="labels-list gt-ml-2">
 							{{range .Labels}}
-								<a href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context ctx.Locale .}}</a>
+								<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context ctx.Locale .}}</a>
 							{{end}}
 						</span>
 					</div>
diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl
index de5ee0c311..68b183e9bf 100644
--- a/templates/shared/search/code/results.tmpl
+++ b/templates/shared/search/code/results.tmpl
@@ -1,7 +1,7 @@
 <div class="flex-text-block gt-fw">
 	{{range $term := .SearchResultLanguages}}
 	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0"
-		href="{{$.Link}}?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&fuzzy={{$.IsFuzzy}}">
+		href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&fuzzy={{$.IsFuzzy}}">
 		<i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i>
 		{{$term.Language}}
 		<div class="detail">{{$term.Count}}</div>
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 0fbf9a7361..c9972f9426 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -62,14 +62,14 @@
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						</span>
 						<div class="menu">
-							<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-							<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-							<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-							<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-							<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
-							<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
-							<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
-							<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
+							<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+							<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+							<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?type={{$.ViewType}}&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+							<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+							<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+							<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+							<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+							<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
 						</div>
 					</div>
 				</div>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 7b62c9fc27..944043e806 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -12,7 +12,7 @@
 					<div class="divider"></div>
 					{{range .Repos}}
 						{{with $Repo := .}}
-							<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="{{$.Link}}?repos=[
+							<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="?repos=[
 								{{- with $include := true -}}
 										{{- range $.RepoIDs -}}
 											{{- if eq . $Repo.ID -}}
@@ -59,12 +59,12 @@
 						</span>
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						<div class="menu">
-							<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
-							<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
-							<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
-							<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
-							<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
-							<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+							<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
+							<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
+							<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
+							<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
+							<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+							<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
 						</div>
 					</div>
 				</div>
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
index a37f0c352e..d39e628263 100644
--- a/templates/user/notification/notification_subscriptions.tmpl
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -14,14 +14,14 @@
 				<div class="gt-df gt-sb">
 					<div class="gt-df">
 						<div class="small-menu-items ui compact tiny menu">
-							<a class="{{if eq .State "all"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}">
+							<a class="{{if eq .State "all"}}active {{end}}item" href="?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}">
 								{{ctx.Locale.Tr "all"}}
 							</a>
-							<a class="{{if eq .State "open"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}">
+							<a class="{{if eq .State "open"}}active {{end}}item" href="?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}">
 								{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
 								{{ctx.Locale.Tr "repo.issues.open_title"}}
 							</a>
-							<a class="{{if eq .State "closed"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}">
+							<a class="{{if eq .State "closed"}}active {{end}}item" href="?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}">
 								{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
 								{{ctx.Locale.Tr "repo.issues.closed_title"}}
 							</a>
@@ -36,9 +36,9 @@
 									</span>
 									{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 									<div class="menu">
-										<a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{ctx.Locale.Tr "all"}}</a>
-										<a class="{{if eq .IssueType "issues"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{ctx.Locale.Tr "issues"}}</a>
-										<a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{ctx.Locale.Tr "pull_requests"}}</a>
+										<a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{ctx.Locale.Tr "all"}}</a>
+										<a class="{{if eq .IssueType "issues"}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{ctx.Locale.Tr "issues"}}</a>
+										<a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{ctx.Locale.Tr "pull_requests"}}</a>
 									</div>
 								</div>
 
@@ -49,14 +49,14 @@
 								</span>
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 								<div class="menu">
-									<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-									<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-									<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-									<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-									<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
-									<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
-									<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
-									<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
+									<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+									<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+									<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+									<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+									<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+									<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+									<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+									<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
 								</div>
 							</div>
 						</div>
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index e57658b197..2f90d0bdad 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -73,7 +73,7 @@
 						{{ctx.Locale.Tr "settings.delete_key"}}
 					</button>
 					{{if and (not .Verified) (ne $.VerifyingID .KeyID)}}
-						<a class="ui primary tiny button" href="{{$.Link}}?verify_gpg={{.KeyID}}">{{ctx.Locale.Tr "settings.gpg_key_verify"}}</a>
+						<a class="ui primary tiny button" href="?verify_gpg={{.KeyID}}">{{ctx.Locale.Tr "settings.gpg_key_verify"}}</a>
 					{{end}}
 				</div>
 			</div>
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index 94ee2a1a55..5577cd0ffd 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -61,7 +61,7 @@
 						{{ctx.Locale.Tr "settings.delete_key"}}
 					</button>
 					{{if and (not .Verified) (ne $.VerifyingFingerprint .Fingerprint)}}
-						<a class="ui primary tiny button" href="{{$.Link}}?verify_ssh={{.Fingerprint}}">{{ctx.Locale.Tr "settings.ssh_key_verify"}}</a>
+						<a class="ui primary tiny button" href="?verify_ssh={{.Fingerprint}}">{{ctx.Locale.Tr "settings.ssh_key_verify"}}</a>
 					{{end}}
 				</div>
 			</div>
diff --git a/tests/integration/explore_user_test.go b/tests/integration/explore_user_test.go
index 046caf378e..441d89cea5 100644
--- a/tests/integration/explore_user_test.go
+++ b/tests/integration/explore_user_test.go
@@ -16,17 +16,17 @@ func TestExploreUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	cases := []struct{ sortOrder, expected string }{
-		{"", "/explore/users?sort=newest&q="},
-		{"newest", "/explore/users?sort=newest&q="},
-		{"oldest", "/explore/users?sort=oldest&q="},
-		{"alphabetically", "/explore/users?sort=alphabetically&q="},
-		{"reversealphabetically", "/explore/users?sort=reversealphabetically&q="},
+		{"", "?sort=newest&q="},
+		{"newest", "?sort=newest&q="},
+		{"oldest", "?sort=oldest&q="},
+		{"alphabetically", "?sort=alphabetically&q="},
+		{"reversealphabetically", "?sort=reversealphabetically&q="},
 	}
 	for _, c := range cases {
 		req := NewRequest(t, "GET", "/explore/users?sort="+c.sortOrder)
 		resp := MakeRequest(t, req, http.StatusOK)
 		h := NewHTMLParser(t, resp.Body)
-		href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="/explore/users"]`).Attr("href")
+		href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="?sort="]`).Attr("href")
 		assert.Equal(t, c.expected, href)
 	}
 

From 91699a9bb1fc59029a2605912f1e380eff7297fa Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 20 Mar 2024 14:58:10 +0800
Subject: [PATCH 444/679] Remove unnecessary ".Link" usages (#29929)

Follow #29909
---
 templates/admin/notice.tmpl              |  2 +-
 templates/admin/stacktrace.tmpl          |  4 ++--
 templates/repo/wiki/new.tmpl             |  2 +-
 templates/user/auth/reset_passwd.tmpl    |  2 +-
 templates/user/dashboard/issues.tmpl     | 16 ++++++++--------
 templates/user/dashboard/milestones.tmpl |  4 ++--
 6 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index e0abe4f8c0..26462596bc 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -49,7 +49,7 @@
 										</div>
 									</div>
 								</div>
-								<button class="ui small teal button" id="delete-selection" data-link="{{.Link}}/delete" data-redirect="{{.Link}}?page={{.Page.Paginater.Current}}">
+								<button class="ui small teal button" id="delete-selection" data-link="{{.Link}}/delete" data-redirect="?page={{.Page.Paginater.Current}}">
 									{{ctx.Locale.Tr "admin.notices.delete_selected"}}
 								</button>
 							</th>
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index 42944615c3..950aa0ea86 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -4,8 +4,8 @@
 	<div class="gt-df gt-ac">
 		<div class="gt-f1">
 			<div class="ui compact small menu">
-				<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="{{.Link}}?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
-				<a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="{{.Link}}?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
+				<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
+				<a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
 			</div>
 		</div>
 		<form target="_blank" action="{{AppSubUrl}}/admin/monitor/diagnosis" class="ui form">
diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl
index 640f8ca9cd..411c7fc869 100644
--- a/templates/repo/wiki/new.tmpl
+++ b/templates/repo/wiki/new.tmpl
@@ -9,7 +9,7 @@
 				<a class="ui tiny primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
 			{{end}}
 		</div>
-		<form class="ui form" action="{{.Link}}?action={{if .PageIsWikiEdit}}_edit{{else}}_new{{end}}" method="post">
+		<form class="ui form" action="?action={{if .PageIsWikiEdit}}_edit{{else}}_new{{end}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="field {{if .Err_Title}}error{{end}}">
 				<input name="title" value="{{.title}}" aria-label="{{ctx.Locale.Tr "repo.wiki.page_title"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.page_title"}}" autofocus required>
diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl
index 4d569e206c..f8303feef3 100644
--- a/templates/user/auth/reset_passwd.tmpl
+++ b/templates/user/auth/reset_passwd.tmpl
@@ -51,7 +51,7 @@
 						<div class="inline field">
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.reset_password_helper"}}</button>
 							{{if and .has_two_factor (not .scratch_code)}}
-								<a href="{{.Link}}?code={{.Code}}&amp;scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
+								<a href="?code={{.Code}}&scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
 							{{end}}
 						</div>
 					{{else}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index c9972f9426..88afcf58ec 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -5,29 +5,29 @@
 		<div class="flex-container">
 			<div class="flex-container-nav">
 				<div class="ui secondary vertical filter menu tw-bg-transparent">
-					<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+					<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "home.issues.in_your_repos"}}
 						<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
 					</a>
-					<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+					<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
 						<strong>{{CountFmt .IssueStats.AssignCount}}</strong>
 					</a>
-					<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+					<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
 						<strong>{{CountFmt .IssueStats.CreateCount}}</strong>
 					</a>
 					{{if .PageIsPulls}}
-						<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+						<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 							{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
 							<strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
 						</a>
-						<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+						<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 							{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
 							<strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
 						</a>
 					{{end}}
-					<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+					<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
 						<strong>{{CountFmt .IssueStats.MentionCount}}</strong>
 					</a>
@@ -36,11 +36,11 @@
 			<div class="flex-container-main content">
 				<div class="list-header">
 					<div class="small-menu-items ui compact tiny menu list-header-toggle">
-						<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
+						<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
 							{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
 							{{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 						</a>
-						<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
+						<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
 							{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
 							{{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 						</a>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 944043e806..214081d423 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -36,11 +36,11 @@
 			<div class="flex-container-main content">
 				<div class="list-header">
 					<div class="small-menu-items ui compact tiny menu list-header-toggle">
-						<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
+						<a class="item{{if not .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
 							{{svg "octicon-milestone" 16 "gt-mr-3"}}
 							{{ctx.Locale.PrettyNumber .MilestoneStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 						</a>
-						<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
+						<a class="item{{if .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
 							{{svg "octicon-check" 16 "gt-mr-3"}}
 							{{ctx.Locale.PrettyNumber .MilestoneStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 						</a>

From b25eec41eb4d7058be808daefd6fd47eed61c7d3 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 20 Mar 2024 18:28:35 +0800
Subject: [PATCH 445/679] Move notifications to a standalone file (#29930)

There is no code change. Just move notification list related
structs/functions from one file to another.
---
 models/activities/notification.go      | 457 ------------------------
 models/activities/notification_list.go | 472 +++++++++++++++++++++++++
 2 files changed, 472 insertions(+), 457 deletions(-)
 create mode 100644 models/activities/notification_list.go

diff --git a/models/activities/notification.go b/models/activities/notification.go
index 230bcdd6e8..dc1b8c6fae 100644
--- a/models/activities/notification.go
+++ b/models/activities/notification.go
@@ -12,12 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
-	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 
@@ -79,53 +75,6 @@ func init() {
 	db.RegisterModel(new(Notification))
 }
 
-// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
-type FindNotificationOptions struct {
-	db.ListOptions
-	UserID            int64
-	RepoID            int64
-	IssueID           int64
-	Status            []NotificationStatus
-	Source            []NotificationSource
-	UpdatedAfterUnix  int64
-	UpdatedBeforeUnix int64
-}
-
-// ToCond will convert each condition into a xorm-Cond
-func (opts FindNotificationOptions) ToConds() builder.Cond {
-	cond := builder.NewCond()
-	if opts.UserID != 0 {
-		cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
-	}
-	if opts.RepoID != 0 {
-		cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
-	}
-	if opts.IssueID != 0 {
-		cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
-	}
-	if len(opts.Status) > 0 {
-		if len(opts.Status) == 1 {
-			cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
-		} else {
-			cond = cond.And(builder.In("notification.status", opts.Status))
-		}
-	}
-	if len(opts.Source) > 0 {
-		cond = cond.And(builder.In("notification.source", opts.Source))
-	}
-	if opts.UpdatedAfterUnix != 0 {
-		cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
-	}
-	if opts.UpdatedBeforeUnix != 0 {
-		cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
-	}
-	return cond
-}
-
-func (opts FindNotificationOptions) ToOrders() string {
-	return "notification.updated_unix DESC"
-}
-
 // CreateRepoTransferNotification creates  notification for the user a repository was transferred to
 func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
 	return db.WithTx(ctx, func(ctx context.Context) error {
@@ -159,109 +108,6 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo
 	})
 }
 
-// CreateOrUpdateIssueNotifications creates an issue notification
-// for each watcher, or updates it if already exists
-// receiverID > 0 just send to receiver, else send to all watcher
-func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
-	ctx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
-		return err
-	}
-
-	return committer.Commit()
-}
-
-func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
-	// init
-	var toNotify container.Set[int64]
-	notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
-		IssueID: issueID,
-	})
-	if err != nil {
-		return err
-	}
-
-	issue, err := issues_model.GetIssueByID(ctx, issueID)
-	if err != nil {
-		return err
-	}
-
-	if receiverID > 0 {
-		toNotify = make(container.Set[int64], 1)
-		toNotify.Add(receiverID)
-	} else {
-		toNotify = make(container.Set[int64], 32)
-		issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
-		if err != nil {
-			return err
-		}
-		toNotify.AddMultiple(issueWatches...)
-		if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
-			repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
-			if err != nil {
-				return err
-			}
-			toNotify.AddMultiple(repoWatches...)
-		}
-		issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
-		if err != nil {
-			return err
-		}
-		toNotify.AddMultiple(issueParticipants...)
-
-		// dont notify user who cause notification
-		delete(toNotify, notificationAuthorID)
-		// explicit unwatch on issue
-		issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
-		if err != nil {
-			return err
-		}
-		for _, id := range issueUnWatches {
-			toNotify.Remove(id)
-		}
-	}
-
-	err = issue.LoadRepo(ctx)
-	if err != nil {
-		return err
-	}
-
-	// notify
-	for userID := range toNotify {
-		issue.Repo.Units = nil
-		user, err := user_model.GetUserByID(ctx, userID)
-		if err != nil {
-			if user_model.IsErrUserNotExist(err) {
-				continue
-			}
-
-			return err
-		}
-		if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
-			continue
-		}
-		if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
-			continue
-		}
-
-		if notificationExists(notifications, issue.ID, userID) {
-			if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
-				return err
-			}
-			continue
-		}
-		if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
 func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
 	notification := &Notification{
 		UserID:    userID,
@@ -449,309 +295,6 @@ func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.Tim
 	return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res)
 }
 
-// NotificationList contains a list of notifications
-type NotificationList []*Notification
-
-// LoadAttributes load Repo Issue User and Comment if not loaded
-func (nl NotificationList) LoadAttributes(ctx context.Context) error {
-	if _, _, err := nl.LoadRepos(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadIssues(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadUsers(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadComments(ctx); err != nil {
-		return err
-	}
-	return nil
-}
-
-func (nl NotificationList) getPendingRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.Repository != nil {
-			continue
-		}
-		ids.Add(notification.RepoID)
-	}
-	return ids.Values()
-}
-
-// LoadRepos loads repositories from database
-func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
-	if len(nl) == 0 {
-		return repo_model.RepositoryList{}, []int{}, nil
-	}
-
-	repoIDs := nl.getPendingRepoIDs()
-	repos := make(map[int64]*repo_model.Repository, len(repoIDs))
-	left := len(repoIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", repoIDs[:limit]).
-			Rows(new(repo_model.Repository))
-		if err != nil {
-			return nil, nil, err
-		}
-
-		for rows.Next() {
-			var repo repo_model.Repository
-			err = rows.Scan(&repo)
-			if err != nil {
-				rows.Close()
-				return nil, nil, err
-			}
-
-			repos[repo.ID] = &repo
-		}
-		_ = rows.Close()
-
-		left -= limit
-		repoIDs = repoIDs[limit:]
-	}
-
-	failed := []int{}
-
-	reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
-	for i, notification := range nl {
-		if notification.Repository == nil {
-			notification.Repository = repos[notification.RepoID]
-		}
-		if notification.Repository == nil {
-			log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
-			failed = append(failed, i)
-			continue
-		}
-		var found bool
-		for _, r := range reposList {
-			if r.ID == notification.RepoID {
-				found = true
-				break
-			}
-		}
-		if !found {
-			reposList = append(reposList, notification.Repository)
-		}
-	}
-	return reposList, failed, nil
-}
-
-func (nl NotificationList) getPendingIssueIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.Issue != nil {
-			continue
-		}
-		ids.Add(notification.IssueID)
-	}
-	return ids.Values()
-}
-
-// LoadIssues loads issues from database
-func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	issueIDs := nl.getPendingIssueIDs()
-	issues := make(map[int64]*issues_model.Issue, len(issueIDs))
-	left := len(issueIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", issueIDs[:limit]).
-			Rows(new(issues_model.Issue))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var issue issues_model.Issue
-			err = rows.Scan(&issue)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			issues[issue.ID] = &issue
-		}
-		_ = rows.Close()
-
-		left -= limit
-		issueIDs = issueIDs[limit:]
-	}
-
-	failures := []int{}
-
-	for i, notification := range nl {
-		if notification.Issue == nil {
-			notification.Issue = issues[notification.IssueID]
-			if notification.Issue == nil {
-				if notification.IssueID != 0 {
-					log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
-					failures = append(failures, i)
-				}
-				continue
-			}
-			notification.Issue.Repo = notification.Repository
-		}
-	}
-	return failures, nil
-}
-
-// Without returns the notification list without the failures
-func (nl NotificationList) Without(failures []int) NotificationList {
-	if len(failures) == 0 {
-		return nl
-	}
-	remaining := make([]*Notification, 0, len(nl))
-	last := -1
-	var i int
-	for _, i = range failures {
-		remaining = append(remaining, nl[last+1:i]...)
-		last = i
-	}
-	if len(nl) > i {
-		remaining = append(remaining, nl[i+1:]...)
-	}
-	return remaining
-}
-
-func (nl NotificationList) getPendingCommentIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.CommentID == 0 || notification.Comment != nil {
-			continue
-		}
-		ids.Add(notification.CommentID)
-	}
-	return ids.Values()
-}
-
-func (nl NotificationList) getUserIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.UserID == 0 || notification.User != nil {
-			continue
-		}
-		ids.Add(notification.UserID)
-	}
-	return ids.Values()
-}
-
-// LoadUsers loads users from database
-func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	userIDs := nl.getUserIDs()
-	users := make(map[int64]*user_model.User, len(userIDs))
-	left := len(userIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", userIDs[:limit]).
-			Rows(new(user_model.User))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var user user_model.User
-			err = rows.Scan(&user)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			users[user.ID] = &user
-		}
-		_ = rows.Close()
-
-		left -= limit
-		userIDs = userIDs[limit:]
-	}
-
-	failures := []int{}
-	for i, notification := range nl {
-		if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
-			notification.User = users[notification.UserID]
-			if notification.User == nil {
-				log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
-				failures = append(failures, i)
-				continue
-			}
-		}
-	}
-	return failures, nil
-}
-
-// LoadComments loads comments from database
-func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	commentIDs := nl.getPendingCommentIDs()
-	comments := make(map[int64]*issues_model.Comment, len(commentIDs))
-	left := len(commentIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", commentIDs[:limit]).
-			Rows(new(issues_model.Comment))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var comment issues_model.Comment
-			err = rows.Scan(&comment)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			comments[comment.ID] = &comment
-		}
-		_ = rows.Close()
-
-		left -= limit
-		commentIDs = commentIDs[limit:]
-	}
-
-	failures := []int{}
-	for i, notification := range nl {
-		if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
-			notification.Comment = comments[notification.CommentID]
-			if notification.Comment == nil {
-				log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
-				failures = append(failures, i)
-				continue
-			}
-			notification.Comment.Issue = notification.Issue
-		}
-	}
-	return failures, nil
-}
-
 // SetIssueReadBy sets issue to be read by given user.
 func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
 	if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil {
diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go
new file mode 100644
index 0000000000..957f9456e7
--- /dev/null
+++ b/models/activities/notification_list.go
@@ -0,0 +1,472 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package activities
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/log"
+
+	"xorm.io/builder"
+)
+
+// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
+type FindNotificationOptions struct {
+	db.ListOptions
+	UserID            int64
+	RepoID            int64
+	IssueID           int64
+	Status            []NotificationStatus
+	Source            []NotificationSource
+	UpdatedAfterUnix  int64
+	UpdatedBeforeUnix int64
+}
+
+// ToCond will convert each condition into a xorm-Cond
+func (opts FindNotificationOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.UserID != 0 {
+		cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
+	}
+	if opts.RepoID != 0 {
+		cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
+	}
+	if opts.IssueID != 0 {
+		cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
+	}
+	if len(opts.Status) > 0 {
+		if len(opts.Status) == 1 {
+			cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
+		} else {
+			cond = cond.And(builder.In("notification.status", opts.Status))
+		}
+	}
+	if len(opts.Source) > 0 {
+		cond = cond.And(builder.In("notification.source", opts.Source))
+	}
+	if opts.UpdatedAfterUnix != 0 {
+		cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
+	}
+	if opts.UpdatedBeforeUnix != 0 {
+		cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
+	}
+	return cond
+}
+
+func (opts FindNotificationOptions) ToOrders() string {
+	return "notification.updated_unix DESC"
+}
+
+// CreateOrUpdateIssueNotifications creates an issue notification
+// for each watcher, or updates it if already exists
+// receiverID > 0 just send to receiver, else send to all watcher
+func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
+	ctx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
+		return err
+	}
+
+	return committer.Commit()
+}
+
+func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
+	// init
+	var toNotify container.Set[int64]
+	notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
+		IssueID: issueID,
+	})
+	if err != nil {
+		return err
+	}
+
+	issue, err := issues_model.GetIssueByID(ctx, issueID)
+	if err != nil {
+		return err
+	}
+
+	if receiverID > 0 {
+		toNotify = make(container.Set[int64], 1)
+		toNotify.Add(receiverID)
+	} else {
+		toNotify = make(container.Set[int64], 32)
+		issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
+		if err != nil {
+			return err
+		}
+		toNotify.AddMultiple(issueWatches...)
+		if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
+			repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
+			if err != nil {
+				return err
+			}
+			toNotify.AddMultiple(repoWatches...)
+		}
+		issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
+		if err != nil {
+			return err
+		}
+		toNotify.AddMultiple(issueParticipants...)
+
+		// dont notify user who cause notification
+		delete(toNotify, notificationAuthorID)
+		// explicit unwatch on issue
+		issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
+		if err != nil {
+			return err
+		}
+		for _, id := range issueUnWatches {
+			toNotify.Remove(id)
+		}
+	}
+
+	err = issue.LoadRepo(ctx)
+	if err != nil {
+		return err
+	}
+
+	// notify
+	for userID := range toNotify {
+		issue.Repo.Units = nil
+		user, err := user_model.GetUserByID(ctx, userID)
+		if err != nil {
+			if user_model.IsErrUserNotExist(err) {
+				continue
+			}
+
+			return err
+		}
+		if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
+			continue
+		}
+		if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
+			continue
+		}
+
+		if notificationExists(notifications, issue.ID, userID) {
+			if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
+				return err
+			}
+			continue
+		}
+		if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// NotificationList contains a list of notifications
+type NotificationList []*Notification
+
+// LoadAttributes load Repo Issue User and Comment if not loaded
+func (nl NotificationList) LoadAttributes(ctx context.Context) error {
+	if _, _, err := nl.LoadRepos(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadIssues(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadUsers(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadComments(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (nl NotificationList) getPendingRepoIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.Repository != nil {
+			continue
+		}
+		ids.Add(notification.RepoID)
+	}
+	return ids.Values()
+}
+
+// LoadRepos loads repositories from database
+func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
+	if len(nl) == 0 {
+		return repo_model.RepositoryList{}, []int{}, nil
+	}
+
+	repoIDs := nl.getPendingRepoIDs()
+	repos := make(map[int64]*repo_model.Repository, len(repoIDs))
+	left := len(repoIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", repoIDs[:limit]).
+			Rows(new(repo_model.Repository))
+		if err != nil {
+			return nil, nil, err
+		}
+
+		for rows.Next() {
+			var repo repo_model.Repository
+			err = rows.Scan(&repo)
+			if err != nil {
+				rows.Close()
+				return nil, nil, err
+			}
+
+			repos[repo.ID] = &repo
+		}
+		_ = rows.Close()
+
+		left -= limit
+		repoIDs = repoIDs[limit:]
+	}
+
+	failed := []int{}
+
+	reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
+	for i, notification := range nl {
+		if notification.Repository == nil {
+			notification.Repository = repos[notification.RepoID]
+		}
+		if notification.Repository == nil {
+			log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
+			failed = append(failed, i)
+			continue
+		}
+		var found bool
+		for _, r := range reposList {
+			if r.ID == notification.RepoID {
+				found = true
+				break
+			}
+		}
+		if !found {
+			reposList = append(reposList, notification.Repository)
+		}
+	}
+	return reposList, failed, nil
+}
+
+func (nl NotificationList) getPendingIssueIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.Issue != nil {
+			continue
+		}
+		ids.Add(notification.IssueID)
+	}
+	return ids.Values()
+}
+
+// LoadIssues loads issues from database
+func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	issueIDs := nl.getPendingIssueIDs()
+	issues := make(map[int64]*issues_model.Issue, len(issueIDs))
+	left := len(issueIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", issueIDs[:limit]).
+			Rows(new(issues_model.Issue))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var issue issues_model.Issue
+			err = rows.Scan(&issue)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			issues[issue.ID] = &issue
+		}
+		_ = rows.Close()
+
+		left -= limit
+		issueIDs = issueIDs[limit:]
+	}
+
+	failures := []int{}
+
+	for i, notification := range nl {
+		if notification.Issue == nil {
+			notification.Issue = issues[notification.IssueID]
+			if notification.Issue == nil {
+				if notification.IssueID != 0 {
+					log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
+					failures = append(failures, i)
+				}
+				continue
+			}
+			notification.Issue.Repo = notification.Repository
+		}
+	}
+	return failures, nil
+}
+
+// Without returns the notification list without the failures
+func (nl NotificationList) Without(failures []int) NotificationList {
+	if len(failures) == 0 {
+		return nl
+	}
+	remaining := make([]*Notification, 0, len(nl))
+	last := -1
+	var i int
+	for _, i = range failures {
+		remaining = append(remaining, nl[last+1:i]...)
+		last = i
+	}
+	if len(nl) > i {
+		remaining = append(remaining, nl[i+1:]...)
+	}
+	return remaining
+}
+
+func (nl NotificationList) getPendingCommentIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.CommentID == 0 || notification.Comment != nil {
+			continue
+		}
+		ids.Add(notification.CommentID)
+	}
+	return ids.Values()
+}
+
+func (nl NotificationList) getUserIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.UserID == 0 || notification.User != nil {
+			continue
+		}
+		ids.Add(notification.UserID)
+	}
+	return ids.Values()
+}
+
+// LoadUsers loads users from database
+func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	userIDs := nl.getUserIDs()
+	users := make(map[int64]*user_model.User, len(userIDs))
+	left := len(userIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", userIDs[:limit]).
+			Rows(new(user_model.User))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var user user_model.User
+			err = rows.Scan(&user)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			users[user.ID] = &user
+		}
+		_ = rows.Close()
+
+		left -= limit
+		userIDs = userIDs[limit:]
+	}
+
+	failures := []int{}
+	for i, notification := range nl {
+		if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
+			notification.User = users[notification.UserID]
+			if notification.User == nil {
+				log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
+				failures = append(failures, i)
+				continue
+			}
+		}
+	}
+	return failures, nil
+}
+
+// LoadComments loads comments from database
+func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	commentIDs := nl.getPendingCommentIDs()
+	comments := make(map[int64]*issues_model.Comment, len(commentIDs))
+	left := len(commentIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", commentIDs[:limit]).
+			Rows(new(issues_model.Comment))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var comment issues_model.Comment
+			err = rows.Scan(&comment)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			comments[comment.ID] = &comment
+		}
+		_ = rows.Close()
+
+		left -= limit
+		commentIDs = commentIDs[limit:]
+	}
+
+	failures := []int{}
+	for i, notification := range nl {
+		if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
+			notification.Comment = comments[notification.CommentID]
+			if notification.Comment == nil {
+				log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
+				failures = append(failures, i)
+				continue
+			}
+			notification.Comment.Issue = notification.Issue
+		}
+	}
+	return failures, nil
+}

From 8cad44f4109b6f87e565d43e137e99ab23b54349 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 20 Mar 2024 12:21:18 +0100
Subject: [PATCH 446/679] Remove the negative margin from `.page-content`
 (#29922)

The negative margin was suboptimal and presents a few unnecessary
challenges while styling the page. Remove it and add custom margin
values, which slightly changes the height a few things near the top of
the page as well:

15px less height of explore and login navbar:

<img width="899" alt="Screenshot 2024-03-20 at 00 52 34"
src="https://github.com/go-gitea/gitea/assets/115237/72a01ca4-5d17-4a0f-b915-61f95054fcb1">

15px reduced padding-top height of "user bar" and equal 4px padding
added:

<img width="484" alt="Screenshot 2024-03-20 at 00 52 50"
src="https://github.com/go-gitea/gitea/assets/115237/a8507e6d-372d-4a8b-9048-66fcf8a5facd">

3px less padding on top of repo:

<img width="552" alt="Screenshot 2024-03-20 at 00 53 49"
src="https://github.com/go-gitea/gitea/assets/115237/dede6e44-7688-440f-a1b6-13532638ae03">
---
 templates/user/dashboard/navbar.tmpl |  2 +-
 web_src/css/base.css                 | 15 +++++----------
 web_src/css/dashboard.css            |  3 +--
 web_src/css/repo/header.css          |  1 +
 web_src/css/user.css                 |  4 ++++
 5 files changed, 12 insertions(+), 13 deletions(-)

diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl
index 3e9442d6fc..480c39e8bf 100644
--- a/templates/user/dashboard/navbar.tmpl
+++ b/templates/user/dashboard/navbar.tmpl
@@ -105,4 +105,4 @@
 	{{end}}
 	</div>
 </div>
-<div class="divider"></div>
+<div class="divider tw-mt-0"></div>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index b47de5ad50..018c7d7bcd 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -227,16 +227,6 @@ a.label,
   border-bottom-color: var(--color-secondary);
 }
 
-.page-content {
-  margin-top: 15px;
-}
-
-.page-content .header-wrapper,
-.page-content overflow-menu {
-  margin-top: -15px !important;
-  padding-top: 15px !important;
-}
-
 /* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */
 .ui.input > input {
   line-height: var(--line-height-default);
@@ -678,6 +668,11 @@ img.ui.avatar,
   padding-bottom: 80px;
 }
 
+.page-content.new:is(.repo,.migrate,.org),
+.page-content.profile:is(.user,.organization) {
+  padding-top: 15px;
+}
+
 /* overwrite semantic width of containers inside the main page content div (div with class "page-content") */
 .page-content .ui.ui.ui.container:not(.fluid) {
   width: 1280px;
diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css
index 51ddd45e31..140cd2e4bf 100644
--- a/web_src/css/dashboard.css
+++ b/web_src/css/dashboard.css
@@ -78,8 +78,7 @@
 }
 
 .dashboard .dashboard-navbar {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
+  padding: 4px 12px;
 }
 
 .dashboard .dashboard-navbar .org-visibility .label {
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index 4461e3338e..f66df4cd8b 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -71,6 +71,7 @@
 }
 
 .repository .header-wrapper {
+  padding-top: 12px;
   background-color: var(--color-header-wrapper);
 }
 
diff --git a/web_src/css/user.css b/web_src/css/user.css
index af8a2f5adc..4267ca0b7d 100644
--- a/web_src/css/user.css
+++ b/web_src/css/user.css
@@ -112,6 +112,10 @@
   border: 1px solid var(--color-secondary);
 }
 
+#notification_div {
+  padding-top: 15px;
+}
+
 #notification_table {
   background: var(--color-box-body);
   border: 1px solid var(--color-secondary);

From bc55a80693aded26efd856812097536e2402d491 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 20 Mar 2024 22:05:34 +0800
Subject: [PATCH 447/679] Fix comment review avatar alignment (#29935)

Fix #29934
---
 templates/repo/issue/view_content/comments.tmpl | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index c9170d9746..6654224320 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -374,10 +374,9 @@
 					{{$reviewType := -1}}
 					{{if .Review}}{{$reviewType = .Review.Type}}{{end}}
 					{{if not .OriginalAuthor}}
-					{{/* Some timeline avatars need a offset to correctly align with their speech
-							bubble. The condition depends on review type and for positive reviews whether
-							there is a comment element or not */}}
-					<a class="timeline-avatar{{if or (and (eq $reviewType 1) (or .Content .Attachments)) (and (eq $reviewType 2) (or .Content .Attachments)) (eq $reviewType 3)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
+					{{/* Some timeline avatars need a offset to correctly align with their speech bubble.
+						The condition depends on whether the comment has contents/attachments or reviews */}}
+					<a class="timeline-avatar{{if or .Content .Attachments (and .Review .Review.CodeComments)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
 						{{ctx.AvatarUtils.Avatar .Poster 40}}
 					</a>
 					{{end}}

From 21151474e36eecc5b808963b924cd27ec34e0608 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 20 Mar 2024 23:38:22 +0800
Subject: [PATCH 448/679] Fix loadOneBranch panic (#29938)

Try to fix #29936

Far from ideal, but still better than panic.
---
 modules/git/repo.go           |  2 +-
 services/repository/branch.go | 10 ++++++----
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/modules/git/repo.go b/modules/git/repo.go
index cef45c6af0..4511e900e0 100644
--- a/modules/git/repo.go
+++ b/modules/git/repo.go
@@ -283,7 +283,7 @@ type DivergeObject struct {
 // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
 func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {
 	cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
-		AddDynamicArguments(baseBranch + "..." + targetBranch)
+		AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
 	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
 	if err != nil {
 		return do, err
diff --git a/services/repository/branch.go b/services/repository/branch.go
index db7acdb505..0353c75fe9 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -158,10 +158,7 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
 	p := protectedBranches.GetFirstMatched(branchName)
 	isProtected := p != nil
 
-	divergence := &git.DivergeObject{
-		Ahead:  -1,
-		Behind: -1,
-	}
+	var divergence *git.DivergeObject
 
 	// it's not default branch
 	if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted {
@@ -180,6 +177,11 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
 		}
 	}
 
+	if divergence == nil {
+		// tolerate the error that we cannot get divergence
+		divergence = &git.DivergeObject{Ahead: -1, Behind: -1}
+	}
+
 	pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx, repo.ID, branchName)
 	if err != nil {
 		return nil, fmt.Errorf("GetLatestPullRequestByHeadInfo: %v", err)

From 99d7ef50917e8d61798715e1b0b3dc1a99709f27 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 20 Mar 2024 18:00:35 +0100
Subject: [PATCH 449/679] Prevent layout shift in `<overflow-menu>` items
 (#29831)

There is a small layout shift in when active tab changes. Notice how the
actions SVG is unstable:


![](https://github.com/go-gitea/gitea/assets/115237/a6928e89-5d47-4a91-8f36-1fa22fddbce7)

This is because the active item with bold text is wider then the
inactive one. I have applied [this
trick](https://stackoverflow.com/a/32570813/808699) to prevent this
layout shift. It's only active inside `<overflow-menu>` because I wanted
to avoid changing HTML and doing it in regular JS would cause a flicker.
I don't expect us to introduce other similar menus without
`<overflow-menu>`, so that place is likely fine.


![after](https://github.com/go-gitea/gitea/assets/115237/d6089924-8de6-4ee0-8db4-15f16069a131)

I also changed the weight from 500 to 600, slightly reduced horizontal
padding, merged some tab-bar related CSS rules and a added a small
margin below repo-header so it does not look so crammed against the
buttons on top.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/css/base.css                      | 32 +++++++++++++++--------
 web_src/css/repo/header.css               |  1 +
 web_src/js/webcomponents/overflow-menu.js | 19 ++++++++++++++
 3 files changed, 41 insertions(+), 11 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 018c7d7bcd..fdfbea610b 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1778,15 +1778,6 @@ table th[data-sortt-desc] .svg {
   border-color: var(--color-secondary);
 }
 
-.ui.tabular.menu .item {
-  padding: 11px 12px;
-  color: var(--color-text-light-2);
-}
-
-.ui.tabular.menu .item:hover {
-  color: var(--color-text);
-}
-
 .ui.tabular.menu .active.item,
 .ui.tabular.menu .active.item:hover {
   background: var(--color-body);
@@ -1803,17 +1794,36 @@ table th[data-sortt-desc] .svg {
   border-color: var(--color-secondary);
 }
 
+.ui.tabular.menu .item,
 .ui.secondary.pointing.menu .item {
+  padding: 11px 12px !important;
   color: var(--color-text-light-2);
 }
 
+.ui.tabular.menu .item:hover,
+.ui.secondary.pointing.menu a.item:hover {
+  color: var(--color-text);
+}
+
 .ui.secondary.pointing.menu .active.item,
 .ui.secondary.pointing.menu .active.item:hover,
-.ui.secondary.pointing.menu .dropdown.item:hover,
-.ui.secondary.pointing.menu a.item:hover {
+.ui.secondary.pointing.menu .dropdown.item:hover {
   color: var(--color-text-dark);
 }
 
+.ui.tabular.menu .active.item,
+.ui.secondary.pointing.menu .active.item,
+.resize-for-semibold::before {
+  font-weight: var(--font-weight-semibold);
+}
+
+.resize-for-semibold::before {
+  content: attr(data-text);
+  visibility: hidden;
+  display: block;
+  height: 0;
+}
+
 .ui.header {
   color: var(--color-text);
 }
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index f66df4cd8b..e998bb9a73 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -8,6 +8,7 @@
   flex-flow: row wrap;
   justify-content: space-between;
   gap: 0.5rem;
+  margin-bottom: 4px;
 }
 
 .repo-header .flex-item {
diff --git a/web_src/js/webcomponents/overflow-menu.js b/web_src/js/webcomponents/overflow-menu.js
index 9fa4585567..604fce7d4b 100644
--- a/web_src/js/webcomponents/overflow-menu.js
+++ b/web_src/js/webcomponents/overflow-menu.js
@@ -127,6 +127,25 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
   });
 
   init() {
+    // for horizontal menus where fomantic boldens active items, prevent this bold text from
+    // enlarging the menu's active item replacing the text node with a div that renders a
+    // invisible pseudo-element that enlarges the box.
+    if (this.matches('.ui.secondary.pointing.menu, .ui.tabular.menu')) {
+      for (const item of this.querySelectorAll('.item')) {
+        for (const child of item.childNodes) {
+          if (child.nodeType === Node.TEXT_NODE) {
+            const text = child.textContent.trim(); // whitespace is insignificant inside flexbox
+            if (!text) continue;
+            const span = document.createElement('span');
+            span.classList.add('resize-for-semibold');
+            span.setAttribute('data-text', text);
+            span.textContent = text;
+            child.replaceWith(span);
+          }
+        }
+      }
+    }
+
     // ResizeObserver triggers on initial render, so we don't manually call `updateItems` here which
     // also avoids a full-page FOUC in Firefox that happens when `updateItems` is called too soon.
     this.resizeObserver = new ResizeObserver((entries) => {

From 97b078d2260c0e9aee53c5b9e73c70e29f736363 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 20 Mar 2024 19:33:00 +0100
Subject: [PATCH 450/679] Add background to dashboard navbar, fix missing
 padding (#29940)

Two small CSS fixes:

1. Add background and reduced padding/avatar size to dashboard navbar.
We use that background already in a number of "secondary navbars", so it
fits.

<img width="1344" alt="Screenshot 2024-03-20 at 18 18 21"
src="https://github.com/go-gitea/gitea/assets/115237/ce5ebedc-e607-42c7-b7b4-b7a4c0ee68f2">

2. Fix padding on top of user settings and subscriptions, regressed by
https://github.com/go-gitea/gitea/pull/29922.
---
 templates/user/dashboard/navbar.tmpl | 4 ++--
 web_src/css/base.css                 | 5 +++--
 web_src/css/dashboard.css            | 3 ++-
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl
index 480c39e8bf..917e024a6f 100644
--- a/templates/user/dashboard/navbar.tmpl
+++ b/templates/user/dashboard/navbar.tmpl
@@ -3,13 +3,13 @@
 		<div class="item">
 			<div class="ui floating dropdown jump">
 				<span class="text truncated-item-container">
-					{{ctx.AvatarUtils.Avatar .ContextUser}}
+					{{ctx.AvatarUtils.Avatar .ContextUser 24 "tw-mr-1"}}
 					<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
 					<span class="org-visibility">
 						{{if .ContextUser.Visibility.IsLimited}}<div class="ui basic tiny horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</div>{{end}}
 						{{if .ContextUser.Visibility.IsPrivate}}<div class="ui basic tiny horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</div>{{end}}
 					</span>
-					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+					{{svg "octicon-triangle-down" 14 "dropdown icon tw-ml-1"}}
 				</span>
 				<div class="context user overflow menu">
 					<div class="ui header">
diff --git a/web_src/css/base.css b/web_src/css/base.css
index fdfbea610b..58332fb21c 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -669,7 +669,8 @@ img.ui.avatar,
 }
 
 .page-content.new:is(.repo,.migrate,.org),
-.page-content.profile:is(.user,.organization) {
+.page-content.profile:is(.user,.organization),
+.page-content.user:is(.settings,.notification) {
   padding-top: 15px;
 }
 
@@ -1796,7 +1797,7 @@ table th[data-sortt-desc] .svg {
 
 .ui.tabular.menu .item,
 .ui.secondary.pointing.menu .item {
-  padding: 11px 12px !important;
+  padding: 11.55px 12px !important; /* match .dashboard-navbar in height */
   color: var(--color-text-light-2);
 }
 
diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css
index 140cd2e4bf..0fa81b1c2a 100644
--- a/web_src/css/dashboard.css
+++ b/web_src/css/dashboard.css
@@ -78,7 +78,8 @@
 }
 
 .dashboard .dashboard-navbar {
-  padding: 4px 12px;
+  padding: 1px 12px; /* match .overflow-menu-items in height */
+  background: var(--color-header-wrapper);
 }
 
 .dashboard .dashboard-navbar .org-visibility .label {

From 286268c9155c9e0b3a3aa0a18675111e5b744a5b Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 20 Mar 2024 23:05:24 +0100
Subject: [PATCH 451/679] Remove fomantic grid module (#29894)

Removed the grid module and moved the used parts it into our own CSS,
eliminating around 75% unused CSS in turn.
---
 web_src/css/index.css               |    1 +
 web_src/css/modules/grid.css        |  498 +++++++
 web_src/css/modules/message.css     |    3 +
 web_src/fomantic/build/semantic.css | 2003 ---------------------------
 web_src/fomantic/semantic.json      |    1 -
 5 files changed, 502 insertions(+), 2004 deletions(-)
 create mode 100644 web_src/css/modules/grid.css

diff --git a/web_src/css/index.css b/web_src/css/index.css
index f6e4c196e6..bf568bff4d 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -1,5 +1,6 @@
 @import "./modules/normalize.css";
 @import "./modules/animations.css";
+@import "./modules/grid.css";
 @import "./modules/button.css";
 @import "./modules/select.css";
 @import "./modules/tippy.css";
diff --git a/web_src/css/modules/grid.css b/web_src/css/modules/grid.css
new file mode 100644
index 0000000000..5a80576c8a
--- /dev/null
+++ b/web_src/css/modules/grid.css
@@ -0,0 +1,498 @@
+/* based on Fomantic UI grid module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-items: stretch;
+  padding: 0;
+  margin-top: -1rem;
+  margin-bottom: -1rem;
+  margin-left: -1rem;
+  margin-right: -1rem;
+}
+
+.ui.relaxed.grid {
+  margin-left: -1.5rem;
+  margin-right: -1.5rem;
+}
+.ui[class*="very relaxed"].grid {
+  margin-left: -2.5rem;
+  margin-right: -2.5rem;
+}
+
+.ui.grid + .grid {
+  margin-top: 1rem;
+}
+
+.ui.grid > .column:not(.row),
+.ui.grid > .row > .column {
+  position: relative;
+  display: inline-block;
+  width: 6.25%;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  vertical-align: top;
+}
+.ui.grid > * {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+
+.ui.grid > .row {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: inherit;
+  align-items: stretch;
+  width: 100% !important;
+  padding: 0;
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+.ui.grid > .column:not(.row) {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+.ui.grid > .row > .column {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.ui.grid > .row > img,
+.ui.grid > .row > .column > img {
+  max-width: 100%;
+}
+
+.ui.grid > .ui.grid:first-child {
+  margin-top: 0;
+}
+.ui.grid > .ui.grid:last-child {
+  margin-bottom: 0;
+}
+
+.ui.grid .aligned.row > .column > .segment:not(.compact):not(.attached),
+.ui.aligned.grid .column > .segment:not(.compact):not(.attached) {
+  width: 100%;
+}
+
+.ui.grid .row + .ui.divider {
+  flex-grow: 1;
+  margin: 1rem;
+}
+.ui.grid .column + .ui.vertical.divider {
+  height: calc(50% - 1rem);
+}
+
+.ui.grid > .row > .column:last-child > .horizontal.segment,
+.ui.grid > .column:last-child > .horizontal.segment {
+  box-shadow: none;
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui.page.grid {
+    width: auto;
+    padding-left: 0;
+    padding-right: 0;
+    margin-left: 0;
+    margin-right: 0;
+  }
+}
+@media only screen and (min-width: 768px) and (max-width: 991.98px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 2em;
+    padding-right: 2em;
+  }
+}
+@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 3%;
+    padding-right: 3%;
+  }
+}
+@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 15%;
+    padding-right: 15%;
+  }
+}
+@media only screen and (min-width: 1920px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 23%;
+    padding-right: 23%;
+  }
+}
+
+.ui.grid > .column:only-child,
+.ui.grid > .row > .column:only-child {
+  width: 100%;
+}
+
+.ui[class*="one column"].grid > .row > .column,
+.ui[class*="one column"].grid > .column:not(.row) {
+  width: 100%;
+}
+.ui[class*="two column"].grid > .row > .column,
+.ui[class*="two column"].grid > .column:not(.row) {
+  width: 50%;
+}
+.ui[class*="three column"].grid > .row > .column,
+.ui[class*="three column"].grid > .column:not(.row) {
+  width: 33.33333333%;
+}
+.ui[class*="four column"].grid > .row > .column,
+.ui[class*="four column"].grid > .column:not(.row) {
+  width: 25%;
+}
+.ui[class*="five column"].grid > .row > .column,
+.ui[class*="five column"].grid > .column:not(.row) {
+  width: 20%;
+}
+.ui[class*="six column"].grid > .row > .column,
+.ui[class*="six column"].grid > .column:not(.row) {
+  width: 16.66666667%;
+}
+.ui[class*="seven column"].grid > .row > .column,
+.ui[class*="seven column"].grid > .column:not(.row) {
+  width: 14.28571429%;
+}
+.ui[class*="eight column"].grid > .row > .column,
+.ui[class*="eight column"].grid > .column:not(.row) {
+  width: 12.5%;
+}
+.ui[class*="nine column"].grid > .row > .column,
+.ui[class*="nine column"].grid > .column:not(.row) {
+  width: 11.11111111%;
+}
+.ui[class*="ten column"].grid > .row > .column,
+.ui[class*="ten column"].grid > .column:not(.row) {
+  width: 10%;
+}
+.ui[class*="eleven column"].grid > .row > .column,
+.ui[class*="eleven column"].grid > .column:not(.row) {
+  width: 9.09090909%;
+}
+.ui[class*="twelve column"].grid > .row > .column,
+.ui[class*="twelve column"].grid > .column:not(.row) {
+  width: 8.33333333%;
+}
+.ui[class*="thirteen column"].grid > .row > .column,
+.ui[class*="thirteen column"].grid > .column:not(.row) {
+  width: 7.69230769%;
+}
+.ui[class*="fourteen column"].grid > .row > .column,
+.ui[class*="fourteen column"].grid > .column:not(.row) {
+  width: 7.14285714%;
+}
+.ui[class*="fifteen column"].grid > .row > .column,
+.ui[class*="fifteen column"].grid > .column:not(.row) {
+  width: 6.66666667%;
+}
+.ui[class*="sixteen column"].grid > .row > .column,
+.ui[class*="sixteen column"].grid > .column:not(.row) {
+  width: 6.25%;
+}
+
+.ui.grid > [class*="one column"].row > .column {
+  width: 100% !important;
+}
+.ui.grid > [class*="two column"].row > .column {
+  width: 50% !important;
+}
+.ui.grid > [class*="three column"].row > .column {
+  width: 33.33333333% !important;
+}
+.ui.grid > [class*="four column"].row > .column {
+  width: 25% !important;
+}
+.ui.grid > [class*="five column"].row > .column {
+  width: 20% !important;
+}
+.ui.grid > [class*="six column"].row > .column {
+  width: 16.66666667% !important;
+}
+.ui.grid > [class*="seven column"].row > .column {
+  width: 14.28571429% !important;
+}
+.ui.grid > [class*="eight column"].row > .column {
+  width: 12.5% !important;
+}
+.ui.grid > [class*="nine column"].row > .column {
+  width: 11.11111111% !important;
+}
+.ui.grid > [class*="ten column"].row > .column {
+  width: 10% !important;
+}
+.ui.grid > [class*="eleven column"].row > .column {
+  width: 9.09090909% !important;
+}
+.ui.grid > [class*="twelve column"].row > .column {
+  width: 8.33333333% !important;
+}
+.ui.grid > [class*="thirteen column"].row > .column {
+  width: 7.69230769% !important;
+}
+.ui.grid > [class*="fourteen column"].row > .column {
+  width: 7.14285714% !important;
+}
+.ui.grid > [class*="fifteen column"].row > .column {
+  width: 6.66666667% !important;
+}
+.ui.grid > [class*="sixteen column"].row > .column {
+  width: 6.25% !important;
+}
+
+.ui.grid > .row > [class*="one wide"].column,
+.ui.grid > .column.row > [class*="one wide"].column,
+.ui.grid > [class*="one wide"].column,
+.ui.column.grid > [class*="one wide"].column {
+  width: 6.25% !important;
+}
+.ui.grid > .row > [class*="two wide"].column,
+.ui.grid > .column.row > [class*="two wide"].column,
+.ui.grid > [class*="two wide"].column,
+.ui.column.grid > [class*="two wide"].column {
+  width: 12.5% !important;
+}
+.ui.grid > .row > [class*="three wide"].column,
+.ui.grid > .column.row > [class*="three wide"].column,
+.ui.grid > [class*="three wide"].column,
+.ui.column.grid > [class*="three wide"].column {
+  width: 18.75% !important;
+}
+.ui.grid > .row > [class*="four wide"].column,
+.ui.grid > .column.row > [class*="four wide"].column,
+.ui.grid > [class*="four wide"].column,
+.ui.column.grid > [class*="four wide"].column {
+  width: 25% !important;
+}
+.ui.grid > .row > [class*="five wide"].column,
+.ui.grid > .column.row > [class*="five wide"].column,
+.ui.grid > [class*="five wide"].column,
+.ui.column.grid > [class*="five wide"].column {
+  width: 31.25% !important;
+}
+.ui.grid > .row > [class*="six wide"].column,
+.ui.grid > .column.row > [class*="six wide"].column,
+.ui.grid > [class*="six wide"].column,
+.ui.column.grid > [class*="six wide"].column {
+  width: 37.5% !important;
+}
+.ui.grid > .row > [class*="seven wide"].column,
+.ui.grid > .column.row > [class*="seven wide"].column,
+.ui.grid > [class*="seven wide"].column,
+.ui.column.grid > [class*="seven wide"].column {
+  width: 43.75% !important;
+}
+.ui.grid > .row > [class*="eight wide"].column,
+.ui.grid > .column.row > [class*="eight wide"].column,
+.ui.grid > [class*="eight wide"].column,
+.ui.column.grid > [class*="eight wide"].column {
+  width: 50% !important;
+}
+.ui.grid > .row > [class*="nine wide"].column,
+.ui.grid > .column.row > [class*="nine wide"].column,
+.ui.grid > [class*="nine wide"].column,
+.ui.column.grid > [class*="nine wide"].column {
+  width: 56.25% !important;
+}
+.ui.grid > .row > [class*="ten wide"].column,
+.ui.grid > .column.row > [class*="ten wide"].column,
+.ui.grid > [class*="ten wide"].column,
+.ui.column.grid > [class*="ten wide"].column {
+  width: 62.5% !important;
+}
+.ui.grid > .row > [class*="eleven wide"].column,
+.ui.grid > .column.row > [class*="eleven wide"].column,
+.ui.grid > [class*="eleven wide"].column,
+.ui.column.grid > [class*="eleven wide"].column {
+  width: 68.75% !important;
+}
+.ui.grid > .row > [class*="twelve wide"].column,
+.ui.grid > .column.row > [class*="twelve wide"].column,
+.ui.grid > [class*="twelve wide"].column,
+.ui.column.grid > [class*="twelve wide"].column {
+  width: 75% !important;
+}
+.ui.grid > .row > [class*="thirteen wide"].column,
+.ui.grid > .column.row > [class*="thirteen wide"].column,
+.ui.grid > [class*="thirteen wide"].column,
+.ui.column.grid > [class*="thirteen wide"].column {
+  width: 81.25% !important;
+}
+.ui.grid > .row > [class*="fourteen wide"].column,
+.ui.grid > .column.row > [class*="fourteen wide"].column,
+.ui.grid > [class*="fourteen wide"].column,
+.ui.column.grid > [class*="fourteen wide"].column {
+  width: 87.5% !important;
+}
+.ui.grid > .row > [class*="fifteen wide"].column,
+.ui.grid > .column.row > [class*="fifteen wide"].column,
+.ui.grid > [class*="fifteen wide"].column,
+.ui.column.grid > [class*="fifteen wide"].column {
+  width: 93.75% !important;
+}
+.ui.grid > .row > [class*="sixteen wide"].column,
+.ui.grid > .column.row > [class*="sixteen wide"].column,
+.ui.grid > [class*="sixteen wide"].column,
+.ui.column.grid > [class*="sixteen wide"].column {
+  width: 100% !important;
+}
+
+.ui.centered.grid,
+.ui.centered.grid > .row,
+.ui.grid > .centered.row {
+  text-align: center;
+  justify-content: center;
+}
+.ui.centered.grid > .column:not(.aligned):not(.justified):not(.row),
+.ui.centered.grid > .row > .column:not(.aligned):not(.justified),
+.ui.grid .centered.row > .column:not(.aligned):not(.justified) {
+  text-align: left;
+}
+.ui.grid > .centered.column,
+.ui.grid > .row > .centered.column {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.ui.relaxed.grid > .column:not(.row),
+.ui.relaxed.grid > .row > .column,
+.ui.grid > .relaxed.row > .column {
+  padding-left: 1.5rem;
+  padding-right: 1.5rem;
+}
+.ui[class*="very relaxed"].grid > .column:not(.row),
+.ui[class*="very relaxed"].grid > .row > .column,
+.ui.grid > [class*="very relaxed"].row > .column {
+  padding-left: 2.5rem;
+  padding-right: 2.5rem;
+}
+
+.ui.relaxed.grid .row + .ui.divider,
+.ui.grid .relaxed.row + .ui.divider {
+  margin-left: 1.5rem;
+  margin-right: 1.5rem;
+}
+.ui[class*="very relaxed"].grid .row + .ui.divider,
+.ui.grid [class*="very relaxed"].row + .ui.divider {
+  margin-left: 2.5rem;
+  margin-right: 2.5rem;
+}
+
+.ui[class*="middle aligned"].grid > .column:not(.row),
+.ui[class*="middle aligned"].grid > .row > .column,
+.ui.grid > [class*="middle aligned"].row > .column,
+.ui.grid > [class*="middle aligned"].column:not(.row),
+.ui.grid > .row > [class*="middle aligned"].column {
+  flex-direction: column;
+  vertical-align: middle;
+  align-self: center !important;
+}
+
+.ui[class*="center aligned"].grid > .column,
+.ui[class*="center aligned"].grid > .row > .column,
+.ui.grid > [class*="center aligned"].row > .column,
+.ui.grid > [class*="center aligned"].column.column,
+.ui.grid > .row > [class*="center aligned"].column.column {
+  text-align: center;
+  align-self: inherit;
+}
+.ui[class*="center aligned"].grid {
+  justify-content: center;
+}
+
+.ui[class*="equal width"].grid > .column:not(.row),
+.ui[class*="equal width"].grid > .row > .column,
+.ui.grid > [class*="equal width"].row > .column {
+  display: inline-block;
+  flex-grow: 1;
+}
+.ui[class*="equal width"].grid > .wide.column,
+.ui[class*="equal width"].grid > .row > .wide.column,
+.ui.grid > [class*="equal width"].row > .wide.column {
+  flex-grow: 0;
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui[class*="mobile reversed"].grid,
+  .ui[class*="mobile reversed"].grid > .row,
+  .ui.grid > [class*="mobile reversed"].row {
+    flex-direction: row-reverse;
+  }
+  .ui.stackable[class*="mobile reversed"] {
+    flex-direction: column-reverse;
+  }
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui.stackable.grid {
+    width: auto;
+    margin-left: 0 !important;
+    margin-right: 0 !important;
+  }
+  .ui.stackable.grid > .row > .wide.column,
+  .ui.stackable.grid > .wide.column,
+  .ui.stackable.grid > .column.grid > .column,
+  .ui.stackable.grid > .column.row > .column,
+  .ui.stackable.grid > .row > .column,
+  .ui.stackable.grid > .column:not(.row),
+  .ui.grid > .stackable.stackable.stackable.row > .column {
+    width: 100% !important;
+    margin: 0 !important;
+    box-shadow: none !important;
+    padding: 1rem;
+  }
+  .ui.stackable.grid:not(.vertically) > .row {
+    margin: 0;
+    padding: 0;
+  }
+
+  .ui.container > .ui.stackable.grid > .column,
+  .ui.container > .ui.stackable.grid > .row > .column {
+    padding-left: 0 !important;
+    padding-right: 0 !important;
+  }
+
+  .ui.grid .ui.stackable.grid,
+  .ui.segment:not(.vertical) .ui.stackable.page.grid {
+    margin-left: -1rem !important;
+    margin-right: -1rem !important;
+  }
+}
+
+.ui.ui.ui.compact.grid > .column:not(.row),
+.ui.ui.ui.compact.grid > .row > .column {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+}
+.ui.ui.ui.compact.grid > * {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+}
+
+.ui.ui.ui.compact.grid > .row {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.ui.ui.ui.compact.grid > .column:not(.row) {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css
index 22dd03232b..a29603cd91 100644
--- a/web_src/css/modules/message.css
+++ b/web_src/css/modules/message.css
@@ -1,3 +1,6 @@
+/* based on Fomantic UI message module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
 .ui.message {
   background: var(--color-box-body);
   color: var(--color-text);
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 6f45c1944c..538dfefdc1 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -7340,2009 +7340,6 @@ select.ui.dropdown {
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Grid
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Standard
-*******************************/
-
-.ui.grid {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  align-items: stretch;
-  padding: 0;
-}
-
-/*----------------------
-      Remove Gutters
------------------------*/
-
-.ui.grid {
-  margin-top: -1rem;
-  margin-bottom: -1rem;
-  margin-left: -1rem;
-  margin-right: -1rem;
-}
-
-.ui.relaxed.grid {
-  margin-left: -1.5rem;
-  margin-right: -1.5rem;
-}
-
-.ui[class*="very relaxed"].grid {
-  margin-left: -2.5rem;
-  margin-right: -2.5rem;
-}
-
-/* Preserve Rows Spacing on Consecutive Grids */
-
-.ui.grid + .grid {
-  margin-top: 1rem;
-}
-
-/*-------------------
-       Columns
---------------------*/
-
-/* Standard 16 column */
-
-.ui.grid > .column:not(.row),
-.ui.grid > .row > .column {
-  position: relative;
-  display: inline-block;
-  width: 6.25%;
-  padding-left: 1rem;
-  padding-right: 1rem;
-  vertical-align: top;
-}
-
-.ui.grid > * {
-  padding-left: 1rem;
-  padding-right: 1rem;
-}
-
-/*-------------------
-        Rows
---------------------*/
-
-.ui.grid > .row {
-  position: relative;
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  justify-content: inherit;
-  align-items: stretch;
-  width: 100% !important;
-  padding: 0;
-  padding-top: 1rem;
-  padding-bottom: 1rem;
-}
-
-/*-------------------
-       Columns
---------------------*/
-
-/* Vertical padding when no rows */
-
-.ui.grid > .column:not(.row) {
-  padding-top: 1rem;
-  padding-bottom: 1rem;
-}
-
-.ui.grid > .row > .column {
-  margin-top: 0;
-  margin-bottom: 0;
-}
-
-/*-------------------
-      Content
---------------------*/
-
-.ui.grid > .row > img,
-.ui.grid > .row > .column > img {
-  max-width: 100%;
-}
-
-/*-------------------
-    Loose Coupling
---------------------*/
-
-/* Collapse Margin on Consecutive Grid */
-
-.ui.grid > .ui.grid:first-child {
-  margin-top: 0;
-}
-
-.ui.grid > .ui.grid:last-child {
-  margin-bottom: 0;
-}
-
-/* Segment inside Aligned Grid */
-
-.ui.grid .aligned.row > .column > .segment:not(.compact):not(.attached),
-.ui.aligned.grid .column > .segment:not(.compact):not(.attached) {
-  width: 100%;
-}
-
-/* Align Dividers with Gutter */
-
-.ui.grid .row + .ui.divider {
-  flex-grow: 1;
-  margin: 1rem 1rem;
-}
-
-.ui.grid .column + .ui.vertical.divider {
-  height: calc(50% - 1rem);
-}
-
-/* Remove Border on Last Horizontal Segment */
-
-.ui.grid > .row > .column:last-child > .horizontal.segment,
-.ui.grid > .column:last-child > .horizontal.segment {
-  box-shadow: none;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-----------------------
-         Page Grid
-  -------------------------*/
-
-@media only screen and (max-width: 767.98px) {
-  .ui.page.grid {
-    width: auto;
-    padding-left: 0;
-    padding-right: 0;
-    margin-left: 0;
-    margin-right: 0;
-  }
-}
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 2em;
-    padding-right: 2em;
-  }
-}
-
-@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 3%;
-    padding-right: 3%;
-  }
-}
-
-@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 15%;
-    padding-right: 15%;
-  }
-}
-
-@media only screen and (min-width: 1920px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 23%;
-    padding-right: 23%;
-  }
-}
-
-/*-------------------
-     Column Count
---------------------*/
-
-/* Assume full width with one column */
-
-.ui.grid > .column:only-child,
-.ui.grid > .row > .column:only-child {
-  width: 100%;
-}
-
-/* Grid Based */
-
-.ui[class*="one column"].grid > .row > .column,
-.ui[class*="one column"].grid > .column:not(.row) {
-  width: 100%;
-}
-
-.ui[class*="two column"].grid > .row > .column,
-.ui[class*="two column"].grid > .column:not(.row) {
-  width: 50%;
-}
-
-.ui[class*="three column"].grid > .row > .column,
-.ui[class*="three column"].grid > .column:not(.row) {
-  width: 33.33333333%;
-}
-
-.ui[class*="four column"].grid > .row > .column,
-.ui[class*="four column"].grid > .column:not(.row) {
-  width: 25%;
-}
-
-.ui[class*="five column"].grid > .row > .column,
-.ui[class*="five column"].grid > .column:not(.row) {
-  width: 20%;
-}
-
-.ui[class*="six column"].grid > .row > .column,
-.ui[class*="six column"].grid > .column:not(.row) {
-  width: 16.66666667%;
-}
-
-.ui[class*="seven column"].grid > .row > .column,
-.ui[class*="seven column"].grid > .column:not(.row) {
-  width: 14.28571429%;
-}
-
-.ui[class*="eight column"].grid > .row > .column,
-.ui[class*="eight column"].grid > .column:not(.row) {
-  width: 12.5%;
-}
-
-.ui[class*="nine column"].grid > .row > .column,
-.ui[class*="nine column"].grid > .column:not(.row) {
-  width: 11.11111111%;
-}
-
-.ui[class*="ten column"].grid > .row > .column,
-.ui[class*="ten column"].grid > .column:not(.row) {
-  width: 10%;
-}
-
-.ui[class*="eleven column"].grid > .row > .column,
-.ui[class*="eleven column"].grid > .column:not(.row) {
-  width: 9.09090909%;
-}
-
-.ui[class*="twelve column"].grid > .row > .column,
-.ui[class*="twelve column"].grid > .column:not(.row) {
-  width: 8.33333333%;
-}
-
-.ui[class*="thirteen column"].grid > .row > .column,
-.ui[class*="thirteen column"].grid > .column:not(.row) {
-  width: 7.69230769%;
-}
-
-.ui[class*="fourteen column"].grid > .row > .column,
-.ui[class*="fourteen column"].grid > .column:not(.row) {
-  width: 7.14285714%;
-}
-
-.ui[class*="fifteen column"].grid > .row > .column,
-.ui[class*="fifteen column"].grid > .column:not(.row) {
-  width: 6.66666667%;
-}
-
-.ui[class*="sixteen column"].grid > .row > .column,
-.ui[class*="sixteen column"].grid > .column:not(.row) {
-  width: 6.25%;
-}
-
-/* Row Based Overrides */
-
-.ui.grid > [class*="one column"].row > .column {
-  width: 100% !important;
-}
-
-.ui.grid > [class*="two column"].row > .column {
-  width: 50% !important;
-}
-
-.ui.grid > [class*="three column"].row > .column {
-  width: 33.33333333% !important;
-}
-
-.ui.grid > [class*="four column"].row > .column {
-  width: 25% !important;
-}
-
-.ui.grid > [class*="five column"].row > .column {
-  width: 20% !important;
-}
-
-.ui.grid > [class*="six column"].row > .column {
-  width: 16.66666667% !important;
-}
-
-.ui.grid > [class*="seven column"].row > .column {
-  width: 14.28571429% !important;
-}
-
-.ui.grid > [class*="eight column"].row > .column {
-  width: 12.5% !important;
-}
-
-.ui.grid > [class*="nine column"].row > .column {
-  width: 11.11111111% !important;
-}
-
-.ui.grid > [class*="ten column"].row > .column {
-  width: 10% !important;
-}
-
-.ui.grid > [class*="eleven column"].row > .column {
-  width: 9.09090909% !important;
-}
-
-.ui.grid > [class*="twelve column"].row > .column {
-  width: 8.33333333% !important;
-}
-
-.ui.grid > [class*="thirteen column"].row > .column {
-  width: 7.69230769% !important;
-}
-
-.ui.grid > [class*="fourteen column"].row > .column {
-  width: 7.14285714% !important;
-}
-
-.ui.grid > [class*="fifteen column"].row > .column {
-  width: 6.66666667% !important;
-}
-
-.ui.grid > [class*="sixteen column"].row > .column {
-  width: 6.25% !important;
-}
-
-/* Celled Page */
-
-.ui.celled.page.grid {
-  box-shadow: none;
-}
-
-/*-------------------
-    Column Width
---------------------*/
-
-/* Sizing Combinations */
-
-.ui.grid > .row > [class*="one wide"].column,
-.ui.grid > .column.row > [class*="one wide"].column,
-.ui.grid > [class*="one wide"].column,
-.ui.column.grid > [class*="one wide"].column {
-  width: 6.25% !important;
-}
-
-.ui.grid > .row > [class*="two wide"].column,
-.ui.grid > .column.row > [class*="two wide"].column,
-.ui.grid > [class*="two wide"].column,
-.ui.column.grid > [class*="two wide"].column {
-  width: 12.5% !important;
-}
-
-.ui.grid > .row > [class*="three wide"].column,
-.ui.grid > .column.row > [class*="three wide"].column,
-.ui.grid > [class*="three wide"].column,
-.ui.column.grid > [class*="three wide"].column {
-  width: 18.75% !important;
-}
-
-.ui.grid > .row > [class*="four wide"].column,
-.ui.grid > .column.row > [class*="four wide"].column,
-.ui.grid > [class*="four wide"].column,
-.ui.column.grid > [class*="four wide"].column {
-  width: 25% !important;
-}
-
-.ui.grid > .row > [class*="five wide"].column,
-.ui.grid > .column.row > [class*="five wide"].column,
-.ui.grid > [class*="five wide"].column,
-.ui.column.grid > [class*="five wide"].column {
-  width: 31.25% !important;
-}
-
-.ui.grid > .row > [class*="six wide"].column,
-.ui.grid > .column.row > [class*="six wide"].column,
-.ui.grid > [class*="six wide"].column,
-.ui.column.grid > [class*="six wide"].column {
-  width: 37.5% !important;
-}
-
-.ui.grid > .row > [class*="seven wide"].column,
-.ui.grid > .column.row > [class*="seven wide"].column,
-.ui.grid > [class*="seven wide"].column,
-.ui.column.grid > [class*="seven wide"].column {
-  width: 43.75% !important;
-}
-
-.ui.grid > .row > [class*="eight wide"].column,
-.ui.grid > .column.row > [class*="eight wide"].column,
-.ui.grid > [class*="eight wide"].column,
-.ui.column.grid > [class*="eight wide"].column {
-  width: 50% !important;
-}
-
-.ui.grid > .row > [class*="nine wide"].column,
-.ui.grid > .column.row > [class*="nine wide"].column,
-.ui.grid > [class*="nine wide"].column,
-.ui.column.grid > [class*="nine wide"].column {
-  width: 56.25% !important;
-}
-
-.ui.grid > .row > [class*="ten wide"].column,
-.ui.grid > .column.row > [class*="ten wide"].column,
-.ui.grid > [class*="ten wide"].column,
-.ui.column.grid > [class*="ten wide"].column {
-  width: 62.5% !important;
-}
-
-.ui.grid > .row > [class*="eleven wide"].column,
-.ui.grid > .column.row > [class*="eleven wide"].column,
-.ui.grid > [class*="eleven wide"].column,
-.ui.column.grid > [class*="eleven wide"].column {
-  width: 68.75% !important;
-}
-
-.ui.grid > .row > [class*="twelve wide"].column,
-.ui.grid > .column.row > [class*="twelve wide"].column,
-.ui.grid > [class*="twelve wide"].column,
-.ui.column.grid > [class*="twelve wide"].column {
-  width: 75% !important;
-}
-
-.ui.grid > .row > [class*="thirteen wide"].column,
-.ui.grid > .column.row > [class*="thirteen wide"].column,
-.ui.grid > [class*="thirteen wide"].column,
-.ui.column.grid > [class*="thirteen wide"].column {
-  width: 81.25% !important;
-}
-
-.ui.grid > .row > [class*="fourteen wide"].column,
-.ui.grid > .column.row > [class*="fourteen wide"].column,
-.ui.grid > [class*="fourteen wide"].column,
-.ui.column.grid > [class*="fourteen wide"].column {
-  width: 87.5% !important;
-}
-
-.ui.grid > .row > [class*="fifteen wide"].column,
-.ui.grid > .column.row > [class*="fifteen wide"].column,
-.ui.grid > [class*="fifteen wide"].column,
-.ui.column.grid > [class*="fifteen wide"].column {
-  width: 93.75% !important;
-}
-
-.ui.grid > .row > [class*="sixteen wide"].column,
-.ui.grid > .column.row > [class*="sixteen wide"].column,
-.ui.grid > [class*="sixteen wide"].column,
-.ui.column.grid > [class*="sixteen wide"].column {
-  width: 100% !important;
-}
-
-/*----------------------
-    Width per Device
------------------------*/
-
-/* Mobile Sizing Combinations */
-
-@media only screen and (min-width: 320px) and (max-width: 767.98px) {
-  .ui.grid > .row > [class*="one wide mobile"].column,
-  .ui.grid > .column.row > [class*="one wide mobile"].column,
-  .ui.grid > [class*="one wide mobile"].column,
-  .ui.column.grid > [class*="one wide mobile"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide mobile"].column,
-  .ui.grid > .column.row > [class*="two wide mobile"].column,
-  .ui.grid > [class*="two wide mobile"].column,
-  .ui.column.grid > [class*="two wide mobile"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide mobile"].column,
-  .ui.grid > .column.row > [class*="three wide mobile"].column,
-  .ui.grid > [class*="three wide mobile"].column,
-  .ui.column.grid > [class*="three wide mobile"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide mobile"].column,
-  .ui.grid > .column.row > [class*="four wide mobile"].column,
-  .ui.grid > [class*="four wide mobile"].column,
-  .ui.column.grid > [class*="four wide mobile"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide mobile"].column,
-  .ui.grid > .column.row > [class*="five wide mobile"].column,
-  .ui.grid > [class*="five wide mobile"].column,
-  .ui.column.grid > [class*="five wide mobile"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide mobile"].column,
-  .ui.grid > .column.row > [class*="six wide mobile"].column,
-  .ui.grid > [class*="six wide mobile"].column,
-  .ui.column.grid > [class*="six wide mobile"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide mobile"].column,
-  .ui.grid > .column.row > [class*="seven wide mobile"].column,
-  .ui.grid > [class*="seven wide mobile"].column,
-  .ui.column.grid > [class*="seven wide mobile"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide mobile"].column,
-  .ui.grid > .column.row > [class*="eight wide mobile"].column,
-  .ui.grid > [class*="eight wide mobile"].column,
-  .ui.column.grid > [class*="eight wide mobile"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide mobile"].column,
-  .ui.grid > .column.row > [class*="nine wide mobile"].column,
-  .ui.grid > [class*="nine wide mobile"].column,
-  .ui.column.grid > [class*="nine wide mobile"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide mobile"].column,
-  .ui.grid > .column.row > [class*="ten wide mobile"].column,
-  .ui.grid > [class*="ten wide mobile"].column,
-  .ui.column.grid > [class*="ten wide mobile"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide mobile"].column,
-  .ui.grid > .column.row > [class*="eleven wide mobile"].column,
-  .ui.grid > [class*="eleven wide mobile"].column,
-  .ui.column.grid > [class*="eleven wide mobile"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide mobile"].column,
-  .ui.grid > .column.row > [class*="twelve wide mobile"].column,
-  .ui.grid > [class*="twelve wide mobile"].column,
-  .ui.column.grid > [class*="twelve wide mobile"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="thirteen wide mobile"].column,
-  .ui.grid > [class*="thirteen wide mobile"].column,
-  .ui.column.grid > [class*="thirteen wide mobile"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="fourteen wide mobile"].column,
-  .ui.grid > [class*="fourteen wide mobile"].column,
-  .ui.column.grid > [class*="fourteen wide mobile"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="fifteen wide mobile"].column,
-  .ui.grid > [class*="fifteen wide mobile"].column,
-  .ui.column.grid > [class*="fifteen wide mobile"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="sixteen wide mobile"].column,
-  .ui.grid > [class*="sixteen wide mobile"].column,
-  .ui.column.grid > [class*="sixteen wide mobile"].column {
-    width: 100% !important;
-  }
-}
-
-/* Tablet Sizing Combinations */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui.grid > .row > [class*="one wide tablet"].column,
-  .ui.grid > .column.row > [class*="one wide tablet"].column,
-  .ui.grid > [class*="one wide tablet"].column,
-  .ui.column.grid > [class*="one wide tablet"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide tablet"].column,
-  .ui.grid > .column.row > [class*="two wide tablet"].column,
-  .ui.grid > [class*="two wide tablet"].column,
-  .ui.column.grid > [class*="two wide tablet"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide tablet"].column,
-  .ui.grid > .column.row > [class*="three wide tablet"].column,
-  .ui.grid > [class*="three wide tablet"].column,
-  .ui.column.grid > [class*="three wide tablet"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide tablet"].column,
-  .ui.grid > .column.row > [class*="four wide tablet"].column,
-  .ui.grid > [class*="four wide tablet"].column,
-  .ui.column.grid > [class*="four wide tablet"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide tablet"].column,
-  .ui.grid > .column.row > [class*="five wide tablet"].column,
-  .ui.grid > [class*="five wide tablet"].column,
-  .ui.column.grid > [class*="five wide tablet"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide tablet"].column,
-  .ui.grid > .column.row > [class*="six wide tablet"].column,
-  .ui.grid > [class*="six wide tablet"].column,
-  .ui.column.grid > [class*="six wide tablet"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide tablet"].column,
-  .ui.grid > .column.row > [class*="seven wide tablet"].column,
-  .ui.grid > [class*="seven wide tablet"].column,
-  .ui.column.grid > [class*="seven wide tablet"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide tablet"].column,
-  .ui.grid > .column.row > [class*="eight wide tablet"].column,
-  .ui.grid > [class*="eight wide tablet"].column,
-  .ui.column.grid > [class*="eight wide tablet"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide tablet"].column,
-  .ui.grid > .column.row > [class*="nine wide tablet"].column,
-  .ui.grid > [class*="nine wide tablet"].column,
-  .ui.column.grid > [class*="nine wide tablet"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide tablet"].column,
-  .ui.grid > .column.row > [class*="ten wide tablet"].column,
-  .ui.grid > [class*="ten wide tablet"].column,
-  .ui.column.grid > [class*="ten wide tablet"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide tablet"].column,
-  .ui.grid > .column.row > [class*="eleven wide tablet"].column,
-  .ui.grid > [class*="eleven wide tablet"].column,
-  .ui.column.grid > [class*="eleven wide tablet"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide tablet"].column,
-  .ui.grid > .column.row > [class*="twelve wide tablet"].column,
-  .ui.grid > [class*="twelve wide tablet"].column,
-  .ui.column.grid > [class*="twelve wide tablet"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="thirteen wide tablet"].column,
-  .ui.grid > [class*="thirteen wide tablet"].column,
-  .ui.column.grid > [class*="thirteen wide tablet"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="fourteen wide tablet"].column,
-  .ui.grid > [class*="fourteen wide tablet"].column,
-  .ui.column.grid > [class*="fourteen wide tablet"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="fifteen wide tablet"].column,
-  .ui.grid > [class*="fifteen wide tablet"].column,
-  .ui.column.grid > [class*="fifteen wide tablet"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="sixteen wide tablet"].column,
-  .ui.grid > [class*="sixteen wide tablet"].column,
-  .ui.column.grid > [class*="sixteen wide tablet"].column {
-    width: 100% !important;
-  }
-}
-
-/* Computer/Desktop Sizing Combinations */
-
-@media only screen and (min-width: 992px) {
-  .ui.grid > .row > [class*="one wide computer"].column,
-  .ui.grid > .column.row > [class*="one wide computer"].column,
-  .ui.grid > [class*="one wide computer"].column,
-  .ui.column.grid > [class*="one wide computer"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide computer"].column,
-  .ui.grid > .column.row > [class*="two wide computer"].column,
-  .ui.grid > [class*="two wide computer"].column,
-  .ui.column.grid > [class*="two wide computer"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide computer"].column,
-  .ui.grid > .column.row > [class*="three wide computer"].column,
-  .ui.grid > [class*="three wide computer"].column,
-  .ui.column.grid > [class*="three wide computer"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide computer"].column,
-  .ui.grid > .column.row > [class*="four wide computer"].column,
-  .ui.grid > [class*="four wide computer"].column,
-  .ui.column.grid > [class*="four wide computer"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide computer"].column,
-  .ui.grid > .column.row > [class*="five wide computer"].column,
-  .ui.grid > [class*="five wide computer"].column,
-  .ui.column.grid > [class*="five wide computer"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide computer"].column,
-  .ui.grid > .column.row > [class*="six wide computer"].column,
-  .ui.grid > [class*="six wide computer"].column,
-  .ui.column.grid > [class*="six wide computer"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide computer"].column,
-  .ui.grid > .column.row > [class*="seven wide computer"].column,
-  .ui.grid > [class*="seven wide computer"].column,
-  .ui.column.grid > [class*="seven wide computer"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide computer"].column,
-  .ui.grid > .column.row > [class*="eight wide computer"].column,
-  .ui.grid > [class*="eight wide computer"].column,
-  .ui.column.grid > [class*="eight wide computer"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide computer"].column,
-  .ui.grid > .column.row > [class*="nine wide computer"].column,
-  .ui.grid > [class*="nine wide computer"].column,
-  .ui.column.grid > [class*="nine wide computer"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide computer"].column,
-  .ui.grid > .column.row > [class*="ten wide computer"].column,
-  .ui.grid > [class*="ten wide computer"].column,
-  .ui.column.grid > [class*="ten wide computer"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide computer"].column,
-  .ui.grid > .column.row > [class*="eleven wide computer"].column,
-  .ui.grid > [class*="eleven wide computer"].column,
-  .ui.column.grid > [class*="eleven wide computer"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide computer"].column,
-  .ui.grid > .column.row > [class*="twelve wide computer"].column,
-  .ui.grid > [class*="twelve wide computer"].column,
-  .ui.column.grid > [class*="twelve wide computer"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide computer"].column,
-  .ui.grid > .column.row > [class*="thirteen wide computer"].column,
-  .ui.grid > [class*="thirteen wide computer"].column,
-  .ui.column.grid > [class*="thirteen wide computer"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide computer"].column,
-  .ui.grid > .column.row > [class*="fourteen wide computer"].column,
-  .ui.grid > [class*="fourteen wide computer"].column,
-  .ui.column.grid > [class*="fourteen wide computer"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide computer"].column,
-  .ui.grid > .column.row > [class*="fifteen wide computer"].column,
-  .ui.grid > [class*="fifteen wide computer"].column,
-  .ui.column.grid > [class*="fifteen wide computer"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide computer"].column,
-  .ui.grid > .column.row > [class*="sixteen wide computer"].column,
-  .ui.grid > [class*="sixteen wide computer"].column,
-  .ui.column.grid > [class*="sixteen wide computer"].column {
-    width: 100% !important;
-  }
-}
-
-/* Large Monitor Sizing Combinations */
-
-@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
-  .ui.grid > .row > [class*="one wide large screen"].column,
-  .ui.grid > .column.row > [class*="one wide large screen"].column,
-  .ui.grid > [class*="one wide large screen"].column,
-  .ui.column.grid > [class*="one wide large screen"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide large screen"].column,
-  .ui.grid > .column.row > [class*="two wide large screen"].column,
-  .ui.grid > [class*="two wide large screen"].column,
-  .ui.column.grid > [class*="two wide large screen"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide large screen"].column,
-  .ui.grid > .column.row > [class*="three wide large screen"].column,
-  .ui.grid > [class*="three wide large screen"].column,
-  .ui.column.grid > [class*="three wide large screen"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide large screen"].column,
-  .ui.grid > .column.row > [class*="four wide large screen"].column,
-  .ui.grid > [class*="four wide large screen"].column,
-  .ui.column.grid > [class*="four wide large screen"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide large screen"].column,
-  .ui.grid > .column.row > [class*="five wide large screen"].column,
-  .ui.grid > [class*="five wide large screen"].column,
-  .ui.column.grid > [class*="five wide large screen"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide large screen"].column,
-  .ui.grid > .column.row > [class*="six wide large screen"].column,
-  .ui.grid > [class*="six wide large screen"].column,
-  .ui.column.grid > [class*="six wide large screen"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide large screen"].column,
-  .ui.grid > .column.row > [class*="seven wide large screen"].column,
-  .ui.grid > [class*="seven wide large screen"].column,
-  .ui.column.grid > [class*="seven wide large screen"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide large screen"].column,
-  .ui.grid > .column.row > [class*="eight wide large screen"].column,
-  .ui.grid > [class*="eight wide large screen"].column,
-  .ui.column.grid > [class*="eight wide large screen"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide large screen"].column,
-  .ui.grid > .column.row > [class*="nine wide large screen"].column,
-  .ui.grid > [class*="nine wide large screen"].column,
-  .ui.column.grid > [class*="nine wide large screen"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide large screen"].column,
-  .ui.grid > .column.row > [class*="ten wide large screen"].column,
-  .ui.grid > [class*="ten wide large screen"].column,
-  .ui.column.grid > [class*="ten wide large screen"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide large screen"].column,
-  .ui.grid > .column.row > [class*="eleven wide large screen"].column,
-  .ui.grid > [class*="eleven wide large screen"].column,
-  .ui.column.grid > [class*="eleven wide large screen"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide large screen"].column,
-  .ui.grid > .column.row > [class*="twelve wide large screen"].column,
-  .ui.grid > [class*="twelve wide large screen"].column,
-  .ui.column.grid > [class*="twelve wide large screen"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="thirteen wide large screen"].column,
-  .ui.grid > [class*="thirteen wide large screen"].column,
-  .ui.column.grid > [class*="thirteen wide large screen"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="fourteen wide large screen"].column,
-  .ui.grid > [class*="fourteen wide large screen"].column,
-  .ui.column.grid > [class*="fourteen wide large screen"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="fifteen wide large screen"].column,
-  .ui.grid > [class*="fifteen wide large screen"].column,
-  .ui.column.grid > [class*="fifteen wide large screen"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="sixteen wide large screen"].column,
-  .ui.grid > [class*="sixteen wide large screen"].column,
-  .ui.column.grid > [class*="sixteen wide large screen"].column {
-    width: 100% !important;
-  }
-}
-
-/* Widescreen Sizing Combinations */
-
-@media only screen and (min-width: 1920px) {
-  .ui.grid > .row > [class*="one wide widescreen"].column,
-  .ui.grid > .column.row > [class*="one wide widescreen"].column,
-  .ui.grid > [class*="one wide widescreen"].column,
-  .ui.column.grid > [class*="one wide widescreen"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide widescreen"].column,
-  .ui.grid > .column.row > [class*="two wide widescreen"].column,
-  .ui.grid > [class*="two wide widescreen"].column,
-  .ui.column.grid > [class*="two wide widescreen"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide widescreen"].column,
-  .ui.grid > .column.row > [class*="three wide widescreen"].column,
-  .ui.grid > [class*="three wide widescreen"].column,
-  .ui.column.grid > [class*="three wide widescreen"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide widescreen"].column,
-  .ui.grid > .column.row > [class*="four wide widescreen"].column,
-  .ui.grid > [class*="four wide widescreen"].column,
-  .ui.column.grid > [class*="four wide widescreen"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide widescreen"].column,
-  .ui.grid > .column.row > [class*="five wide widescreen"].column,
-  .ui.grid > [class*="five wide widescreen"].column,
-  .ui.column.grid > [class*="five wide widescreen"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide widescreen"].column,
-  .ui.grid > .column.row > [class*="six wide widescreen"].column,
-  .ui.grid > [class*="six wide widescreen"].column,
-  .ui.column.grid > [class*="six wide widescreen"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide widescreen"].column,
-  .ui.grid > .column.row > [class*="seven wide widescreen"].column,
-  .ui.grid > [class*="seven wide widescreen"].column,
-  .ui.column.grid > [class*="seven wide widescreen"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide widescreen"].column,
-  .ui.grid > .column.row > [class*="eight wide widescreen"].column,
-  .ui.grid > [class*="eight wide widescreen"].column,
-  .ui.column.grid > [class*="eight wide widescreen"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide widescreen"].column,
-  .ui.grid > .column.row > [class*="nine wide widescreen"].column,
-  .ui.grid > [class*="nine wide widescreen"].column,
-  .ui.column.grid > [class*="nine wide widescreen"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide widescreen"].column,
-  .ui.grid > .column.row > [class*="ten wide widescreen"].column,
-  .ui.grid > [class*="ten wide widescreen"].column,
-  .ui.column.grid > [class*="ten wide widescreen"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide widescreen"].column,
-  .ui.grid > .column.row > [class*="eleven wide widescreen"].column,
-  .ui.grid > [class*="eleven wide widescreen"].column,
-  .ui.column.grid > [class*="eleven wide widescreen"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide widescreen"].column,
-  .ui.grid > .column.row > [class*="twelve wide widescreen"].column,
-  .ui.grid > [class*="twelve wide widescreen"].column,
-  .ui.column.grid > [class*="twelve wide widescreen"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="thirteen wide widescreen"].column,
-  .ui.grid > [class*="thirteen wide widescreen"].column,
-  .ui.column.grid > [class*="thirteen wide widescreen"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="fourteen wide widescreen"].column,
-  .ui.grid > [class*="fourteen wide widescreen"].column,
-  .ui.column.grid > [class*="fourteen wide widescreen"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="fifteen wide widescreen"].column,
-  .ui.grid > [class*="fifteen wide widescreen"].column,
-  .ui.column.grid > [class*="fifteen wide widescreen"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="sixteen wide widescreen"].column,
-  .ui.grid > [class*="sixteen wide widescreen"].column,
-  .ui.column.grid > [class*="sixteen wide widescreen"].column {
-    width: 100% !important;
-  }
-}
-
-/*----------------------
-          Centered
-  -----------------------*/
-
-.ui.centered.grid,
-.ui.centered.grid > .row,
-.ui.grid > .centered.row {
-  text-align: center;
-  justify-content: center;
-}
-
-.ui.centered.grid > .column:not(.aligned):not(.justified):not(.row),
-.ui.centered.grid > .row > .column:not(.aligned):not(.justified),
-.ui.grid .centered.row > .column:not(.aligned):not(.justified) {
-  text-align: left;
-}
-
-.ui.grid > .centered.column,
-.ui.grid > .row > .centered.column {
-  display: block;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-/*----------------------
-          Relaxed
-  -----------------------*/
-
-.ui.relaxed.grid > .column:not(.row),
-.ui.relaxed.grid > .row > .column,
-.ui.grid > .relaxed.row > .column {
-  padding-left: 1.5rem;
-  padding-right: 1.5rem;
-}
-
-.ui[class*="very relaxed"].grid > .column:not(.row),
-.ui[class*="very relaxed"].grid > .row > .column,
-.ui.grid > [class*="very relaxed"].row > .column {
-  padding-left: 2.5rem;
-  padding-right: 2.5rem;
-}
-
-/* Coupling with UI Divider */
-
-.ui.relaxed.grid .row + .ui.divider,
-.ui.grid .relaxed.row + .ui.divider {
-  margin-left: 1.5rem;
-  margin-right: 1.5rem;
-}
-
-.ui[class*="very relaxed"].grid .row + .ui.divider,
-.ui.grid [class*="very relaxed"].row + .ui.divider {
-  margin-left: 2.5rem;
-  margin-right: 2.5rem;
-}
-
-/*----------------------
-          Padded
-  -----------------------*/
-
-.ui.padded.grid:not(.vertically):not(.horizontally) {
-  margin: 0 !important;
-}
-
-[class*="horizontally padded"].ui.grid {
-  margin-left: 0 !important;
-  margin-right: 0 !important;
-}
-
-[class*="vertically padded"].ui.grid {
-  margin-top: 0 !important;
-  margin-bottom: 0 !important;
-}
-
-/*----------------------
-         "Floated"
-  -----------------------*/
-
-.ui.grid [class*="left floated"].column {
-  margin-right: auto;
-}
-
-.ui.grid [class*="right floated"].column {
-  margin-left: auto;
-}
-
-/*----------------------
-          Divided
-  -----------------------*/
-
-.ui.divided.grid:not([class*="vertically divided"]) > .column:not(.row),
-.ui.divided.grid:not([class*="vertically divided"]) > .row > .column {
-  box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-}
-
-/* Swap from padding to margin on columns to have dividers align */
-
-.ui[class*="vertically divided"].grid > .column:not(.row),
-.ui[class*="vertically divided"].grid > .row > .column {
-  margin-top: 1rem;
-  margin-bottom: 1rem;
-  padding-top: 0;
-  padding-bottom: 0;
-}
-
-.ui[class*="vertically divided"].grid > .row {
-  margin-top: 0;
-  margin-bottom: 0;
-}
-
-/* No divider on first column on row */
-
-.ui.divided.grid:not([class*="vertically divided"]) > .column:first-child,
-.ui.divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-  box-shadow: none;
-}
-
-/* No space on top of first row */
-
-.ui[class*="vertically divided"].grid > .row:first-child > .column {
-  margin-top: 0;
-}
-
-/* Divided Row */
-
-.ui.grid > .divided.row > .column {
-  box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.grid > .divided.row > .column:first-child {
-  box-shadow: none;
-}
-
-/* Vertically Divided */
-
-.ui[class*="vertically divided"].grid > .row {
-  position: relative;
-}
-
-.ui[class*="vertically divided"].grid > .row:before {
-  position: absolute;
-  content: "";
-  top: 0;
-  left: 0;
-  width: calc(100% - 2rem);
-  height: 1px;
-  margin: 0 1rem;
-  box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-}
-
-/* Padded Horizontally Divided */
-
-[class*="horizontally padded"].ui.divided.grid,
-.ui.padded.divided.grid:not(.vertically):not(.horizontally) {
-  width: 100%;
-}
-
-/* First Row Vertically Divided */
-
-.ui[class*="vertically divided"].grid > .row:first-child:before {
-  box-shadow: none;
-}
-
-/* Relaxed */
-
-.ui.relaxed[class*="vertically divided"].grid > .row:before {
-  margin-left: 1.5rem;
-  margin-right: 1.5rem;
-  width: calc(100% - 3rem);
-}
-
-.ui[class*="very relaxed"][class*="vertically divided"].grid > .row:before {
-  margin-left: 2.5rem;
-  margin-right: 2.5rem;
-  width: calc(100% - 5rem);
-}
-
-/*----------------------
-           Celled
-  -----------------------*/
-
-.ui.celled.grid {
-  width: 100%;
-  margin: 1em 0;
-  box-shadow: 0 0 0 1px #D4D4D5;
-}
-
-.ui.celled.grid > .row {
-  width: 100% !important;
-  margin: 0;
-  padding: 0;
-  box-shadow: 0 -1px 0 0 #D4D4D5;
-}
-
-.ui.celled.grid > .column:not(.row),
-.ui.celled.grid > .row > .column {
-  box-shadow: -1px 0 0 0 #D4D4D5;
-}
-
-.ui.celled.grid > .column:first-child,
-.ui.celled.grid > .row > .column:first-child {
-  box-shadow: none;
-}
-
-.ui.celled.grid > .column:not(.row),
-.ui.celled.grid > .row > .column {
-  padding: 1em;
-}
-
-.ui.relaxed.celled.grid > .column:not(.row),
-.ui.relaxed.celled.grid > .row > .column {
-  padding: 1.5em;
-}
-
-.ui[class*="very relaxed"].celled.grid > .column:not(.row),
-.ui[class*="very relaxed"].celled.grid > .row > .column {
-  padding: 2em;
-}
-
-/* Internally Celled */
-
-.ui[class*="internally celled"].grid {
-  box-shadow: none;
-  margin: 0;
-}
-
-.ui[class*="internally celled"].grid > .row:first-child {
-  box-shadow: none;
-}
-
-.ui[class*="internally celled"].grid > .row > .column:first-child {
-  box-shadow: none;
-}
-
-/*----------------------
-     Vertically Aligned
-  -----------------------*/
-
-/* Top Aligned */
-
-.ui[class*="top aligned"].grid > .column:not(.row),
-.ui[class*="top aligned"].grid > .row > .column,
-.ui.grid > [class*="top aligned"].row > .column,
-.ui.grid > [class*="top aligned"].column:not(.row),
-.ui.grid > .row > [class*="top aligned"].column {
-  flex-direction: column;
-  vertical-align: top;
-  align-self: flex-start !important;
-}
-
-/* Middle Aligned */
-
-.ui[class*="middle aligned"].grid > .column:not(.row),
-.ui[class*="middle aligned"].grid > .row > .column,
-.ui.grid > [class*="middle aligned"].row > .column,
-.ui.grid > [class*="middle aligned"].column:not(.row),
-.ui.grid > .row > [class*="middle aligned"].column {
-  flex-direction: column;
-  vertical-align: middle;
-  align-self: center !important;
-}
-
-/* Bottom Aligned */
-
-.ui[class*="bottom aligned"].grid > .column:not(.row),
-.ui[class*="bottom aligned"].grid > .row > .column,
-.ui.grid > [class*="bottom aligned"].row > .column,
-.ui.grid > [class*="bottom aligned"].column:not(.row),
-.ui.grid > .row > [class*="bottom aligned"].column {
-  flex-direction: column;
-  vertical-align: bottom;
-  align-self: flex-end !important;
-}
-
-/* Stretched */
-
-.ui.stretched.grid > .row > .column,
-.ui.stretched.grid > .column,
-.ui.grid > .stretched.row > .column,
-.ui.grid > .stretched.column:not(.row),
-.ui.grid > .row > .stretched.column {
-  display: inline-flex !important;
-  align-self: stretch;
-  flex-direction: column;
-}
-
-.ui.stretched.grid > .row > .column > *,
-.ui.stretched.grid > .column > *,
-.ui.grid > .stretched.row > .column > *,
-.ui.grid > .stretched.column:not(.row) > *,
-.ui.grid > .row > .stretched.column > * {
-  flex-grow: 1;
-}
-
-/*----------------------
-    Horizontally Centered
-  -----------------------*/
-
-/* Left Aligned */
-
-.ui[class*="left aligned"].grid > .column,
-.ui[class*="left aligned"].grid > .row > .column,
-.ui.grid > [class*="left aligned"].row > .column,
-.ui.grid > [class*="left aligned"].column.column,
-.ui.grid > .row > [class*="left aligned"].column.column {
-  text-align: left;
-  align-self: inherit;
-}
-
-/* Center Aligned */
-
-.ui[class*="center aligned"].grid > .column,
-.ui[class*="center aligned"].grid > .row > .column,
-.ui.grid > [class*="center aligned"].row > .column,
-.ui.grid > [class*="center aligned"].column.column,
-.ui.grid > .row > [class*="center aligned"].column.column {
-  text-align: center;
-  align-self: inherit;
-}
-
-.ui[class*="center aligned"].grid {
-  justify-content: center;
-}
-
-/* Right Aligned */
-
-.ui[class*="right aligned"].grid > .column,
-.ui[class*="right aligned"].grid > .row > .column,
-.ui.grid > [class*="right aligned"].row > .column,
-.ui.grid > [class*="right aligned"].column.column,
-.ui.grid > .row > [class*="right aligned"].column.column {
-  text-align: right;
-  align-self: inherit;
-}
-
-/* Justified */
-
-.ui.justified.grid > .column,
-.ui.justified.grid > .row > .column,
-.ui.grid > .justified.row > .column,
-.ui.grid > .justified.column.column,
-.ui.grid > .row > .justified.column.column {
-  text-align: justify;
-  -webkit-hyphens: auto;
-  hyphens: auto;
-}
-
-/*----------------------
-         Colored
------------------------*/
-
-.ui.grid > .primary.row,
-.ui.grid > .primary.column,
-.ui.grid > .row > .primary.column {
-  background-color: #2185D0;
-  color: #FFFFFF;
-}
-
-.ui.grid > .secondary.row,
-.ui.grid > .secondary.column,
-.ui.grid > .row > .secondary.column {
-  background-color: #1B1C1D;
-  color: #FFFFFF;
-}
-
-.ui.grid > .red.row,
-.ui.grid > .red.column,
-.ui.grid > .row > .red.column {
-  background-color: #DB2828;
-  color: #FFFFFF;
-}
-
-.ui.grid > .orange.row,
-.ui.grid > .orange.column,
-.ui.grid > .row > .orange.column {
-  background-color: #F2711C;
-  color: #FFFFFF;
-}
-
-.ui.grid > .yellow.row,
-.ui.grid > .yellow.column,
-.ui.grid > .row > .yellow.column {
-  background-color: #FBBD08;
-  color: #FFFFFF;
-}
-
-.ui.grid > .olive.row,
-.ui.grid > .olive.column,
-.ui.grid > .row > .olive.column {
-  background-color: #B5CC18;
-  color: #FFFFFF;
-}
-
-.ui.grid > .green.row,
-.ui.grid > .green.column,
-.ui.grid > .row > .green.column {
-  background-color: #21BA45;
-  color: #FFFFFF;
-}
-
-.ui.grid > .teal.row,
-.ui.grid > .teal.column,
-.ui.grid > .row > .teal.column {
-  background-color: #00B5AD;
-  color: #FFFFFF;
-}
-
-.ui.grid > .blue.row,
-.ui.grid > .blue.column,
-.ui.grid > .row > .blue.column {
-  background-color: #2185D0;
-  color: #FFFFFF;
-}
-
-.ui.grid > .violet.row,
-.ui.grid > .violet.column,
-.ui.grid > .row > .violet.column {
-  background-color: #6435C9;
-  color: #FFFFFF;
-}
-
-.ui.grid > .purple.row,
-.ui.grid > .purple.column,
-.ui.grid > .row > .purple.column {
-  background-color: #A333C8;
-  color: #FFFFFF;
-}
-
-.ui.grid > .pink.row,
-.ui.grid > .pink.column,
-.ui.grid > .row > .pink.column {
-  background-color: #E03997;
-  color: #FFFFFF;
-}
-
-.ui.grid > .brown.row,
-.ui.grid > .brown.column,
-.ui.grid > .row > .brown.column {
-  background-color: #A5673F;
-  color: #FFFFFF;
-}
-
-.ui.grid > .grey.row,
-.ui.grid > .grey.column,
-.ui.grid > .row > .grey.column {
-  background-color: #767676;
-  color: #FFFFFF;
-}
-
-.ui.grid > .black.row,
-.ui.grid > .black.column,
-.ui.grid > .row > .black.column {
-  background-color: #1B1C1D;
-  color: #FFFFFF;
-}
-
-/*----------------------
-      Equal Width
------------------------*/
-
-.ui[class*="equal width"].grid > .column:not(.row),
-.ui[class*="equal width"].grid > .row > .column,
-.ui.grid > [class*="equal width"].row > .column {
-  display: inline-block;
-  flex-grow: 1;
-}
-
-.ui[class*="equal width"].grid > .wide.column,
-.ui[class*="equal width"].grid > .row > .wide.column,
-.ui.grid > [class*="equal width"].row > .wide.column {
-  flex-grow: 0;
-}
-
-/*----------------------
-          Reverse
-  -----------------------*/
-
-/* Mobile */
-
-@media only screen and (max-width: 767.98px) {
-  .ui[class*="mobile reversed"].grid,
-  .ui[class*="mobile reversed"].grid > .row,
-  .ui.grid > [class*="mobile reversed"].row {
-    flex-direction: row-reverse;
-  }
-
-  .ui[class*="mobile vertically reversed"].grid,
-  .ui.stackable[class*="mobile reversed"] {
-    flex-direction: column-reverse;
-  }
-
-  /* Divided Reversed */
-
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child,
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child,
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child {
-    box-shadow: none;
-  }
-
-  /* Vertically Divided Reversed */
-
-  .ui.grid[class*="vertically divided"][class*="mobile vertically reversed"] > .row:first-child:before {
-    box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui.grid[class*="vertically divided"][class*="mobile vertically reversed"] > .row:last-child:before {
-    box-shadow: none;
-  }
-
-  /* Celled Reversed */
-
-  .ui[class*="mobile reversed"].celled.grid > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 #D4D4D5;
-  }
-
-  .ui[class*="mobile reversed"].celled.grid > .row > .column:last-child {
-    box-shadow: none;
-  }
-}
-
-/* Tablet */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui[class*="tablet reversed"].grid,
-  .ui[class*="tablet reversed"].grid > .row,
-  .ui.grid > [class*="tablet reversed"].row {
-    flex-direction: row-reverse;
-  }
-
-  .ui[class*="tablet vertically reversed"].grid {
-    flex-direction: column-reverse;
-  }
-
-  /* Divided Reversed */
-
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child,
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child,
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child {
-    box-shadow: none;
-  }
-
-  /* Vertically Divided Reversed */
-
-  .ui.grid[class*="vertically divided"][class*="tablet vertically reversed"] > .row:first-child:before {
-    box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui.grid[class*="vertically divided"][class*="tablet vertically reversed"] > .row:last-child:before {
-    box-shadow: none;
-  }
-
-  /* Celled Reversed */
-
-  .ui[class*="tablet reversed"].celled.grid > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 #D4D4D5;
-  }
-
-  .ui[class*="tablet reversed"].celled.grid > .row > .column:last-child {
-    box-shadow: none;
-  }
-}
-
-/* Computer */
-
-@media only screen and (min-width: 992px) {
-  .ui[class*="computer reversed"].grid,
-  .ui[class*="computer reversed"].grid > .row,
-  .ui.grid > [class*="computer reversed"].row {
-    flex-direction: row-reverse;
-  }
-
-  .ui[class*="computer vertically reversed"].grid {
-    flex-direction: column-reverse;
-  }
-
-  /* Divided Reversed */
-
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child,
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child,
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child {
-    box-shadow: none;
-  }
-
-  /* Vertically Divided Reversed */
-
-  .ui.grid[class*="vertically divided"][class*="computer vertically reversed"] > .row:first-child:before {
-    box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui.grid[class*="vertically divided"][class*="computer vertically reversed"] > .row:last-child:before {
-    box-shadow: none;
-  }
-
-  /* Celled Reversed */
-
-  .ui[class*="computer reversed"].celled.grid > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 #D4D4D5;
-  }
-
-  .ui[class*="computer reversed"].celled.grid > .row > .column:last-child {
-    box-shadow: none;
-  }
-}
-
-/*-------------------
-        Stackable
-  --------------------*/
-
-@media only screen and (max-width: 767.98px) {
-  .ui.stackable.grid {
-    width: auto;
-    margin-left: 0 !important;
-    margin-right: 0 !important;
-  }
-
-  .ui.stackable.grid > .row > .wide.column,
-  .ui.stackable.grid > .wide.column,
-  .ui.stackable.grid > .column.grid > .column,
-  .ui.stackable.grid > .column.row > .column,
-  .ui.stackable.grid > .row > .column,
-  .ui.stackable.grid > .column:not(.row),
-  .ui.grid > .stackable.stackable.stackable.row > .column {
-    width: 100% !important;
-    margin: 0 0 !important;
-    box-shadow: none !important;
-    padding: 1rem 1rem;
-  }
-
-  .ui.stackable.grid:not(.vertically) > .row {
-    margin: 0;
-    padding: 0;
-  }
-
-  /* Coupling */
-
-  .ui.container > .ui.stackable.grid > .column,
-  .ui.container > .ui.stackable.grid > .row > .column {
-    padding-left: 0 !important;
-    padding-right: 0 !important;
-  }
-
-  /* Don't pad inside segment or nested grid */
-
-  .ui.grid .ui.stackable.grid,
-  .ui.segment:not(.vertical) .ui.stackable.page.grid {
-    margin-left: -1rem !important;
-    margin-right: -1rem !important;
-  }
-
-  /* Divided Stackable */
-
-  .ui.stackable.divided.grid > .row:first-child > .column:first-child,
-  .ui.stackable.celled.grid > .row:first-child > .column:first-child,
-  .ui.stackable.divided.grid > .column:not(.row):first-child,
-  .ui.stackable.celled.grid > .column:not(.row):first-child {
-    border-top: none !important;
-  }
-
-  .ui.stackable.celled.grid > .column:not(.row),
-  .ui.stackable.divided:not(.vertically).grid > .column:not(.row),
-  .ui.stackable.celled.grid > .row > .column,
-  .ui.stackable.divided:not(.vertically).grid > .row > .column {
-    border-top: 1px solid rgba(34, 36, 38, 0.15);
-    box-shadow: none !important;
-    padding-top: 2rem !important;
-    padding-bottom: 2rem !important;
-  }
-
-  .ui.stackable.celled.grid > .row {
-    box-shadow: none !important;
-  }
-
-  .ui.stackable.divided:not(.vertically).grid > .column:not(.row),
-  .ui.stackable.divided:not(.vertically).grid > .row > .column {
-    padding-left: 0 !important;
-    padding-right: 0 !important;
-  }
-}
-
-/*----------------------
-     Only (Device)
------------------------*/
-
-/* These include arbitrary class repetitions for forced specificity */
-
-/* Mobile Only Hide */
-
-@media only screen and (max-width: 767.98px) {
-  .ui[class*="tablet only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="computer only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="computer only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="computer only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="computer only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="large screen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Tablet Only Hide */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.tablet),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.tablet),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.tablet),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.tablet) {
-    display: none !important;
-  }
-
-  .ui[class*="computer only"].grid.grid.grid:not(.tablet),
-  .ui.grid.grid.grid > [class*="computer only"].row:not(.tablet),
-  .ui.grid.grid.grid > [class*="computer only"].column:not(.tablet),
-  .ui.grid.grid.grid > .row > [class*="computer only"].column:not(.tablet) {
-    display: none !important;
-  }
-
-  .ui[class*="large screen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Computer Only Hide */
-
-@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="tablet only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="large screen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Large Screen Only Hide */
-
-@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="tablet only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Widescreen Only Hide */
-
-@media only screen and (min-width: 1920px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="tablet only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) {
-    display: none !important;
-  }
-}
-
-/*-----------------
-        Compact
-  -----------------*/
-
-.ui.ui.ui.compact.grid > .column:not(.row),
-.ui.ui.ui.compact.grid > .row > .column {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
-}
-
-.ui.ui.ui.compact.grid > * {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
-}
-
-/* Row */
-
-.ui.ui.ui.compact.grid > .row {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-}
-
-/* Columns */
-
-.ui.ui.ui.compact.grid > .column:not(.row) {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-}
-
-/* Relaxed + Celled */
-
-.ui.compact.relaxed.celled.grid > .column:not(.row),
-.ui.compact.relaxed.celled.grid > .row > .column {
-  padding: 0.75em;
-}
-
-.ui.compact[class*="very relaxed"].celled.grid > .column:not(.row),
-.ui.compact[class*="very relaxed"].celled.grid > .row > .column {
-  padding: 1em;
-}
-
-/*-----------------
-      Very compact
-  -----------------*/
-
-.ui.ui.ui[class*="very compact"].grid > .column:not(.row),
-.ui.ui.ui[class*="very compact"].grid > .row > .column {
-  padding-left: 0.25rem;
-  padding-right: 0.25rem;
-}
-
-.ui.ui.ui[class*="very compact"].grid > * {
-  padding-left: 0.25rem;
-  padding-right: 0.25rem;
-}
-
-/* Row */
-
-.ui.ui.ui[class*="very compact"].grid > .row {
-  padding-top: 0.25rem;
-  padding-bottom: 0.25rem;
-  padding-left: 0.75rem;
-  padding-right: 0.75rem;
-}
-
-/* Columns */
-
-.ui.ui.ui[class*="very compact"].grid > .column:not(.row) {
-  padding-top: 0.25rem;
-  padding-bottom: 0.25rem;
-}
-
-/* Relaxed + Celled */
-
-.ui[class*="very compact"].relaxed.celled.grid > .column:not(.row),
-.ui[class*="very compact"].relaxed.celled.grid > .row > .column {
-  padding: 0.375em;
-}
-
-.ui[class*="very compact"][class*="very relaxed"].celled.grid > .column:not(.row),
-.ui[class*="very compact"][class*="very relaxed"].celled.grid > .row > .column {
-  padding: 0.5em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 6e2facf822..bd2ba15c62 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -28,7 +28,6 @@
     "dimmer",
     "dropdown",
     "form",
-    "grid",
     "header",
     "input",
     "label",

From 76ec5410510f09b3ea2bfd2602fcb8f3251087b6 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 21 Mar 2024 07:02:53 +0800
Subject: [PATCH 452/679] Fix and rewrite markup anchor processing (#29931)

Fix #29877

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/markup/anchors.js | 82 ++++++++++++++++++++++--------------
 1 file changed, 50 insertions(+), 32 deletions(-)

diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
index 6cf83eb428..dac877fd99 100644
--- a/web_src/js/markup/anchors.js
+++ b/web_src/js/markup/anchors.js
@@ -1,50 +1,68 @@
 import {svg} from '../svg.js';
 
-const headingSelector = '.markup h1, .markup h2, .markup h3, .markup h4, .markup h5, .markup h6';
-
 // scroll to anchor while respecting the `user-content` prefix that exists on the target
-function scrollToAnchor(hash, initial) {
+function scrollToAnchor(encodedId, initial) {
   // abort if the browser has already scrolled to another anchor during page load
-  if (initial && document.querySelector(':target')) return;
-  if (hash?.length <= 1) return;
-  const id = decodeURIComponent(hash.substring(1));
-  const el = document.getElementById(`user-content-${id}`);
-  if (el) {
-    el.scrollIntoView();
-  } else if (id.startsWith('user-content-')) { // compat for links with old 'user-content-' prefixed hashes
+  if (!encodedId || (initial && document.querySelector(':target'))) return;
+  const id = decodeURIComponent(encodedId);
+  let el = document.getElementById(`user-content-${id}`);
+
+  // check for matching user-generated `a[name]`
+  if (!el) {
+    const nameAnchors = document.getElementsByName(`user-content-${id}`);
+    if (nameAnchors.length) {
+      el = nameAnchors[0];
+    }
+  }
+
+  // compat for links with old 'user-content-' prefixed hashes
+  if (!el && id.startsWith('user-content-')) {
     const el = document.getElementById(id);
     if (el) el.scrollIntoView();
   }
+
+  if (el) {
+    el.scrollIntoView();
+  }
 }
 
 export function initMarkupAnchors() {
-  if (!document.querySelector('.markup')) return;
+  const markupEls = document.querySelectorAll('.markup');
+  if (!markupEls.length) return;
 
-  // create link icons for markup headings, the resulting link href will remove `user-content-`
-  for (const heading of document.querySelectorAll(headingSelector)) {
-    const originalId = heading.id.replace(/^user-content-/, '');
-    const a = document.createElement('a');
-    a.classList.add('anchor');
-    a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
-    a.innerHTML = svg('octicon-link');
-    a.addEventListener('click', (e) => {
-      scrollToAnchor(e.currentTarget.getAttribute('href'), false);
-    });
-    heading.prepend(a);
-  }
+  for (const markupEl of markupEls) {
+    // create link icons for markup headings, the resulting link href will remove `user-content-`
+    for (const heading of markupEl.querySelectorAll(`:is(h1, h2, h3, h4, h5, h6`)) {
+      const originalId = heading.id.replace(/^user-content-/, '');
+      const a = document.createElement('a');
+      a.classList.add('anchor');
+      a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
+      a.innerHTML = svg('octicon-link');
+      heading.prepend(a);
+    }
 
-  // handle user-defined `name` anchors like `[Link](#link)` linking to `<a name="link"></a>Link`
-  for (const a of document.querySelectorAll('.markup a[href^="#"]')) {
-    const href = a.getAttribute('href');
-    if (!href.startsWith('#user-content-')) continue;
-    const originalId = href.replace(/^#user-content-/, '');
-    a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
-    if (a.closest('.markup').querySelectorAll(`a[name="${originalId}"]`).length !== 1) {
+    // remove `user-content-` prefix from links so they don't show in url bar when clicked
+    for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
+      const href = a.getAttribute('href');
+      if (!href.startsWith('#user-content-')) continue;
+      const originalId = href.replace(/^#user-content-/, '');
+      a.setAttribute('href', `#${originalId}`);
+    }
+
+    // add `user-content-` prefix to user-generated `a[name]` link targets
+    // TODO: this prefix should be added in backend instead
+    for (const a of markupEl.querySelectorAll('a[name]')) {
+      const name = a.getAttribute('name');
+      if (!name) continue;
+      a.setAttribute('name', `user-content-${a.name}`);
+    }
+
+    for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
       a.addEventListener('click', (e) => {
-        scrollToAnchor(e.currentTarget.getAttribute('href'), false);
+        scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1), false);
       });
     }
   }
 
-  scrollToAnchor(window.location.hash, true);
+  scrollToAnchor(window.location.hash.substring(1), true);
 }

From 444460ea807c6a669d1a1467510868335abee5fb Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 21 Mar 2024 00:48:04 +0100
Subject: [PATCH 453/679] Misc color tweaks (#29943)

Minor color tweaks:

- Better text contrasts
- Better distinguish nav and header wrapper in light theme
- Input boxes are now white on light theme
- Slightly darker dark theme background

<img width="503" alt="Screenshot 2024-03-20 at 19 31 54"
src="https://github.com/go-gitea/gitea/assets/115237/c7802a84-2386-4332-bd91-f419473ff644">
<img width="510" alt="Screenshot 2024-03-20 at 19 32 24"
src="https://github.com/go-gitea/gitea/assets/115237/21d3529e-6e0a-413a-9e9e-a03bea2405eb">
---
 web_src/css/themes/theme-gitea-dark.css  | 12 ++++++------
 web_src/css/themes/theme-gitea-light.css | 18 +++++++++---------
 2 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index 4e38d75f65..9cf8907e45 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -180,16 +180,16 @@
   --color-orange-badge-hover-bg: #f2711c4d;
   --color-git: #f05133;
   /* target-based colors */
-  --color-body: #1e2224;
+  --color-body: #1c1f25;
   --color-box-header: #1a1d1f;
   --color-box-body: #14171a;
   --color-box-body-highlight: #121517;
   --color-text-dark: #f8f8f9;
-  --color-text: #ced2d5;
-  --color-text-light: #bec4c8;
-  --color-text-light-1: #acb3b8;
-  --color-text-light-2: #8d969c;
-  --color-text-light-3: #747f87;
+  --color-text: #d1d5d8;
+  --color-text-light: #bdc3c7;
+  --color-text-light-1: #a8afb5;
+  --color-text-light-2: #929ba2;
+  --color-text-light-3: #7c8790;
   --color-footer: var(--color-nav-bg);
   --color-timeline: #353c42;
   --color-input-text: var(--color-text-dark);
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index eded03e371..2ac83eefed 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -184,20 +184,20 @@
   --color-box-header: #f1f3f5;
   --color-box-body: #ffffff;
   --color-box-body-highlight: #f4faff;
-  --color-text-dark: #03080d;
-  --color-text: #1c2126;
-  --color-text-light: #3c434a;
-  --color-text-light-1: #4b5259;
-  --color-text-light-2: #6a7178;
-  --color-text-light-3: #899097;
+  --color-text-dark: #01050a;
+  --color-text: #181c21;
+  --color-text-light: #30363b;
+  --color-text-light-1: #40474d;
+  --color-text-light-2: #5b6167;
+  --color-text-light-3: #747c84;
   --color-footer: var(--color-nav-bg);
   --color-timeline: #d0d7de;
   --color-input-text: var(--color-text-dark);
-  --color-input-background: #f8f9fb;
+  --color-input-background: #fff;
   --color-input-toggle-background: #d0d7de;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: #fafbfc;
+  --color-header-wrapper: #f9fafb;
   --color-light: #00001706;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
   --color-light-border: #0000171d;
@@ -224,7 +224,7 @@
   --color-reaction-active-bg: var(--color-primary-light-6);
   --color-tooltip-text: #fbfdff;
   --color-tooltip-bg: #000017f0;
-  --color-nav-bg: #f8f9fb;
+  --color-nav-bg: #f6f7fa;
   --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
   --color-label-text: var(--color-text);

From d4cd988c187f277f5fdc22c9ce3923324805a304 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 21 Mar 2024 03:08:06 +0100
Subject: [PATCH 454/679] Remove codecov badge (#29950)

It's been broken since the migration to actions, so lets remove it.
---
 README.md    | 1 -
 README_ZH.md | 1 -
 2 files changed, 2 deletions(-)

diff --git a/README.md b/README.md
index 90474cbd94..f579449174 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
 
 [![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
 [![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
-[![](https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg)](https://app.codecov.io/gh/go-gitea/gitea "Codecov")
 [![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
 [![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
 [![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
diff --git a/README_ZH.md b/README_ZH.md
index deebb40cfb..726c4273a6 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -2,7 +2,6 @@
 
 [![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
 [![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
-[![](https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg)](https://app.codecov.io/gh/go-gitea/gitea "Codecov")
 [![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
 [![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
 [![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")

From 3ee39db34efd532626d710de6717bf3c6255c10e Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 21 Mar 2024 03:17:59 +0100
Subject: [PATCH 455/679] Exclude `routers/private/tests` from air (#29949)

Exclude this and reformat the toml option to multiline.

I wasn't able to get `exclude_regex` to work so it would include a
`tests` directory anywhere. I think that option only works on files.
---
 .air.toml | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/.air.toml b/.air.toml
index d13f8c4f99..de97bd8b29 100644
--- a/.air.toml
+++ b/.air.toml
@@ -8,6 +8,15 @@ delay = 1000
 include_ext = ["go", "tmpl"]
 include_file = ["main.go"]
 include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
-exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"]
+exclude_dir = [
+  "models/fixtures",
+  "models/migrations/fixtures",
+  "modules/avatar/identicon/testdata",
+  "modules/avatar/testdata",
+  "modules/git/tests",
+  "modules/migration/file_format_testdata",
+  "routers/private/tests",
+  "services/gitdiff/testdata",
+]
 exclude_regex = ["_test.go$", "_gen.go$"]
 stop_on_error = true

From b150ff0bab3fc6c419edf1569a0271ebcb9734fa Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Thu, 21 Mar 2024 15:01:35 +0800
Subject: [PATCH 456/679] Cancel previous runs of the same PR automatically
 (#29961)

Follow #25716. Also cancel previous runs for `pull_request_sync`.

It's not a bug since it original PR said "if the event is push".

The main change is
https://github.com/go-gitea/gitea/pull/29961/files#diff-08adda3f8ae0360937f46abb1f4418603bd3518522baa356be11c6c7ac4abcc3.

And also rename `CancelRunningJobs` to `CancelPreviousJobs` to make it
more clear.
---
 models/actions/run.go               |  9 +++++----
 models/actions/schedule.go          |  4 ++--
 services/actions/notifier_helper.go | 10 +++++-----
 services/actions/schedule_tasks.go  |  4 ++--
 services/repository/branch.go       |  8 ++++----
 5 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/models/actions/run.go b/models/actions/run.go
index 7b3125949b..fa9db0b554 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -170,15 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
 	return err
 }
 
-// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow.
-func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
-	// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'.
+// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
+// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
+func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
+	// Find all runs in the specified repository, reference, and workflow with non-final status
 	runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
 		RepoID:       repoID,
 		Ref:          ref,
 		WorkflowID:   workflowID,
 		TriggerEvent: event,
-		Status:       []Status{StatusRunning, StatusWaiting},
+		Status:       []Status{StatusRunning, StatusWaiting, StatusBlocked},
 	})
 	if err != nil {
 		return err
diff --git a/models/actions/schedule.go b/models/actions/schedule.go
index d450e7aa07..3646a046a0 100644
--- a/models/actions/schedule.go
+++ b/models/actions/schedule.go
@@ -127,14 +127,14 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) er
 		return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
 	}
 	// cancel running cron jobs of this repository and delete old schedules
-	if err := CancelRunningJobs(
+	if err := CancelPreviousJobs(
 		ctx,
 		repo.ID,
 		repo.DefaultBranch,
 		"",
 		webhook_module.HookEventSchedule,
 	); err != nil {
-		return fmt.Errorf("CancelRunningJobs: %v", err)
+		return fmt.Errorf("CancelPreviousJobs: %v", err)
 	}
 	return nil
 }
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index fafb6ab40e..66a19844c2 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -317,17 +317,17 @@ func handleWorkflows(
 			continue
 		}
 
-		// cancel running jobs if the event is push
-		if run.Event == webhook_module.HookEventPush {
-			// cancel running jobs of the same workflow
-			if err := actions_model.CancelRunningJobs(
+		// cancel running jobs if the event is push or pull_request_sync
+		if run.Event == webhook_module.HookEventPush ||
+			run.Event == webhook_module.HookEventPullRequestSync {
+			if err := actions_model.CancelPreviousJobs(
 				ctx,
 				run.RepoID,
 				run.Ref,
 				run.WorkflowID,
 				run.Event,
 			); err != nil {
-				log.Error("CancelRunningJobs: %v", err)
+				log.Error("CancelPreviousJobs: %v", err)
 			}
 		}
 
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 79dd84e0cc..59862fd0d8 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -55,14 +55,14 @@ func startTasks(ctx context.Context) error {
 			// cancel running jobs if the event is push
 			if row.Schedule.Event == webhook_module.HookEventPush {
 				// cancel running jobs of the same workflow
-				if err := actions_model.CancelRunningJobs(
+				if err := actions_model.CancelPreviousJobs(
 					ctx,
 					row.RepoID,
 					row.Schedule.Ref,
 					row.Schedule.WorkflowID,
 					webhook_module.HookEventSchedule,
 				); err != nil {
-					log.Error("CancelRunningJobs: %v", err)
+					log.Error("CancelPreviousJobs: %v", err)
 				}
 			}
 
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 0353c75fe9..229ac54f30 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -410,14 +410,14 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
 				log.Error("DeleteCronTaskByRepo: %v", err)
 			}
 			// cancel running cron jobs of this repository and delete old schedules
-			if err := actions_model.CancelRunningJobs(
+			if err := actions_model.CancelPreviousJobs(
 				ctx,
 				repo.ID,
 				from,
 				"",
 				webhook_module.HookEventSchedule,
 			); err != nil {
-				log.Error("CancelRunningJobs: %v", err)
+				log.Error("CancelPreviousJobs: %v", err)
 			}
 
 			err2 = gitrepo.SetDefaultBranch(ctx, repo, to)
@@ -575,14 +575,14 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
 			log.Error("DeleteCronTaskByRepo: %v", err)
 		}
 		// cancel running cron jobs of this repository and delete old schedules
-		if err := actions_model.CancelRunningJobs(
+		if err := actions_model.CancelPreviousJobs(
 			ctx,
 			repo.ID,
 			oldDefaultBranchName,
 			"",
 			webhook_module.HookEventSchedule,
 		); err != nil {
-			log.Error("CancelRunningJobs: %v", err)
+			log.Error("CancelPreviousJobs: %v", err)
 		}
 
 		if err := gitrepo.SetDefaultBranch(ctx, repo, newBranchName); err != nil {

From 82db9a2ba77d2a6c470b62be3c82b73c0a544fcc Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 21 Mar 2024 16:48:08 +0800
Subject: [PATCH 457/679] Fix the bug that user may logout if he switch pages
 too fast (#29962)

This PR fixed a bug when the user switching pages too fast, he will
logout automatically.

The reason is that when the error is context cancelled, the previous
code think user hasn't login then the session will be deleted. Now it
will return the errors but not think it's not login.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/auth/session.go | 26 +++++++++-----------------
 1 file changed, 9 insertions(+), 17 deletions(-)

diff --git a/services/auth/session.go b/services/auth/session.go
index d13813dcbe..35d97e42da 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -4,7 +4,6 @@
 package auth
 
 import (
-	"context"
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
@@ -29,40 +28,33 @@ func (s *Session) Name() string {
 // object for that uid.
 // Returns nil if there is no user uid stored in the session.
 func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
-	user := SessionUser(req.Context(), sess)
-	if user != nil {
-		return user, nil
-	}
-	return nil, nil
-}
-
-// SessionUser returns the user object corresponding to the "uid" session variable.
-func SessionUser(ctx context.Context, sess SessionStore) *user_model.User {
 	if sess == nil {
-		return nil
+		return nil, nil
 	}
 
 	// Get user ID
 	uid := sess.Get("uid")
 	if uid == nil {
-		return nil
+		return nil, nil
 	}
 	log.Trace("Session Authorization: Found user[%d]", uid)
 
 	id, ok := uid.(int64)
 	if !ok {
-		return nil
+		return nil, nil
 	}
 
 	// Get user object
-	user, err := user_model.GetUserByID(ctx, id)
+	user, err := user_model.GetUserByID(req.Context(), id)
 	if err != nil {
 		if !user_model.IsErrUserNotExist(err) {
-			log.Error("GetUserById: %v", err)
+			log.Error("GetUserByID: %v", err)
+			// Return the err as-is to keep current signed-in session, in case the err is something like context.Canceled. Otherwise non-existing user (nil, nil) will make the caller clear the signed-in session.
+			return nil, err
 		}
-		return nil
+		return nil, nil
 	}
 
 	log.Trace("Session Authorization: Logged in user %-v", user)
-	return user
+	return user, nil
 }

From 1a4f693f9f9723c181b747cb6d658aa37118005a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 21 Mar 2024 11:16:11 +0100
Subject: [PATCH 458/679] Fix JS error and improve error message styles
 (#29963)

Fixes: https://github.com/go-gitea/gitea/issues/29956. This error
exposed a existing bug in the code, it was just never noticed because
the jQuery expression evaluated without error before while the new one
doesn't.

Also improves error message styles:

Before:
<img width="1338" alt="Screenshot 2024-03-21 at 09 16 07"
src="https://github.com/go-gitea/gitea/assets/115237/1cc1ef89-ad94-491e-bbca-75387f7547a0">

After:
<img width="1335" alt="Screenshot 2024-03-21 at 09 15 44"
src="https://github.com/go-gitea/gitea/assets/115237/312efc79-5353-4e2a-a703-1bccd3c01736">
---
 web_src/js/bootstrap.js            | 2 +-
 web_src/js/features/repo-common.js | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
index 698d17fa36..3034478190 100644
--- a/web_src/js/bootstrap.js
+++ b/web_src/js/bootstrap.js
@@ -23,7 +23,7 @@ export function showGlobalErrorMessage(msg) {
   let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
   if (!msgDiv) {
     const el = document.createElement('div');
-    el.innerHTML = `<div class="ui container negative message center aligned js-global-error" style="white-space: pre-line;"></div>`;
+    el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`;
     msgDiv = el.childNodes[0];
   }
   // merge duplicated messages into "the message (count)" format
diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js
index 9e11ffe197..669b47a9c5 100644
--- a/web_src/js/features/repo-common.js
+++ b/web_src/js/features/repo-common.js
@@ -76,6 +76,8 @@ export function initRepoCommonBranchOrTagDropdown(selector) {
 
 export function initRepoCommonFilterSearchDropdown(selector) {
   const $dropdown = $(selector);
+  if (!$dropdown.length) return;
+
   $dropdown.dropdown({
     fullTextSearch: 'exact',
     selectOnKeydown: false,

From 0b4ff15356769db092fd7718da553e8a216c32fa Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Thu, 21 Mar 2024 18:38:27 +0800
Subject: [PATCH 459/679] Solving the issue of UI disruption when the review is
 deleted without refreshing (#29951)

**After deleting the review and refreshing, the display is normal.
However, Without refreshing, the interface will be broken**


https://github.com/go-gitea/gitea/assets/37935145/f5cb19a6-eb26-47b0-b8ee-15b575bbe1ac

**after**


https://github.com/go-gitea/gitea/assets/37935145/aa65922c-2ebf-4fce-ad91-35661f70329a
---
 web_src/js/features/repo-issue.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index bca062bcc7..ad2956a600 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -162,7 +162,8 @@ export function initRepoIssueCommentDelete() {
         const response = await POST($this.data('url'));
         if (!response.ok) throw new Error('Failed to delete comment');
         const $conversationHolder = $this.closest('.conversation-holder');
-
+        const $parentTimelineItem = $this.closest('.timeline-item');
+        const $parentTimelineGroup = $this.closest('.timeline-item-group');
         // Check if this was a pending comment.
         if ($conversationHolder.find('.pending-label').length) {
           const $counter = $('#review-box .review-comments-counter');
@@ -185,6 +186,11 @@ export function initRepoIssueCommentDelete() {
           }
           $conversationHolder.remove();
         }
+        // Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
+        if (!$parentTimelineGroup.find('.timeline-item.comment').length && !$parentTimelineItem.find('.conversation-holder').length) {
+          const $timelineAvatar = $parentTimelineGroup.find('.timeline-avatar');
+          $timelineAvatar.removeClass('timeline-avatar-offset');
+        }
       } catch (error) {
         console.error(error);
       }

From 01500957c29f6bfa2396b8457dbb0645edaafa99 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 21 Mar 2024 20:02:34 +0800
Subject: [PATCH 460/679] Refactor URL detection (#29960)

"Redirect" functions should only redirect if the target is for current Gitea site.
---
 modules/httplib/url.go               | 40 +++++++++++----
 modules/httplib/url_test.go          | 77 ++++++++++++++++++++--------
 routers/common/redirect.go           |  2 +-
 routers/web/auth/auth.go             |  6 +--
 routers/web/auth/oauth.go            |  2 +-
 routers/web/auth/password.go         |  2 +-
 routers/web/repo/repo.go             |  2 +-
 routers/web/web.go                   |  2 +-
 services/context/context_response.go |  6 +--
 9 files changed, 96 insertions(+), 43 deletions(-)

diff --git a/modules/httplib/url.go b/modules/httplib/url.go
index 14b95898f5..b679b44500 100644
--- a/modules/httplib/url.go
+++ b/modules/httplib/url.go
@@ -8,20 +8,40 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 )
 
-// IsRiskyRedirectURL returns true if the URL is considered risky for redirects
-func IsRiskyRedirectURL(s string) bool {
+func urlIsRelative(s string, u *url.URL) bool {
 	// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
 	// Therefore we should ignore these redirect locations to prevent open redirects
 	if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') {
-		return true
+		return false
 	}
-
-	u, err := url.Parse(s)
-	if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(s), strings.ToLower(setting.AppURL))) {
-		return true
-	}
-
-	return false
+	return u != nil && u.Scheme == "" && u.Host == ""
+}
+
+// IsRelativeURL detects if a URL is relative (no scheme or host)
+func IsRelativeURL(s string) bool {
+	u, err := url.Parse(s)
+	return err == nil && urlIsRelative(s, u)
+}
+
+func IsCurrentGiteaSiteURL(s string) bool {
+	u, err := url.Parse(s)
+	if err != nil {
+		return false
+	}
+	if u.Path != "" {
+		u.Path = "/" + util.PathJoinRelX(u.Path)
+		if !strings.HasSuffix(u.Path, "/") {
+			u.Path += "/"
+		}
+	}
+	if urlIsRelative(s, u) {
+		return u.Path == "" || strings.HasPrefix(strings.ToLower(u.Path), strings.ToLower(setting.AppSubURL+"/"))
+	}
+	if u.Path == "" {
+		u.Path = "/"
+	}
+	return strings.HasPrefix(strings.ToLower(u.String()), strings.ToLower(setting.AppURL))
 }
diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go
index 72033b1208..9b7b242298 100644
--- a/modules/httplib/url_test.go
+++ b/modules/httplib/url_test.go
@@ -7,32 +7,65 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
 )
 
-func TestIsRiskyRedirectURL(t *testing.T) {
-	setting.AppURL = "http://localhost:3000/"
-	tests := []struct {
-		input string
-		want  bool
-	}{
-		{"", false},
-		{"foo", false},
-		{"/", false},
-		{"/foo?k=%20#abc", false},
-
-		{"//", true},
-		{"\\\\", true},
-		{"/\\", true},
-		{"\\/", true},
-		{"mail:a@b.com", true},
-		{"https://test.com", true},
-		{setting.AppURL + "/foo", false},
+func TestIsRelativeURL(t *testing.T) {
+	defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
+	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+	rel := []string{
+		"",
+		"foo",
+		"/",
+		"/foo?k=%20#abc",
 	}
-	for _, tt := range tests {
-		t.Run(tt.input, func(t *testing.T) {
-			assert.Equal(t, tt.want, IsRiskyRedirectURL(tt.input))
-		})
+	for _, s := range rel {
+		assert.True(t, IsRelativeURL(s), "rel = %q", s)
+	}
+	abs := []string{
+		"//",
+		"\\\\",
+		"/\\",
+		"\\/",
+		"mailto:a@b.com",
+		"https://test.com",
+	}
+	for _, s := range abs {
+		assert.False(t, IsRelativeURL(s), "abs = %q", s)
 	}
 }
+
+func TestIsCurrentGiteaSiteURL(t *testing.T) {
+	defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
+	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+	good := []string{
+		"?key=val",
+		"/sub",
+		"/sub/",
+		"/sub/foo",
+		"/sub/foo/",
+		"http://localhost:3000/sub?key=val",
+		"http://localhost:3000/sub/",
+	}
+	for _, s := range good {
+		assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s)
+	}
+	bad := []string{
+		"/",
+		"//",
+		"\\\\",
+		"/foo",
+		"http://localhost:3000/sub/..",
+		"http://localhost:3000/other",
+		"http://other/",
+	}
+	for _, s := range bad {
+		assert.False(t, IsCurrentGiteaSiteURL(s), "bad = %q", s)
+	}
+
+	setting.AppURL = "http://localhost:3000/"
+	setting.AppSubURL = ""
+	assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val"))
+}
diff --git a/routers/common/redirect.go b/routers/common/redirect.go
index 9bf2025e19..34044e814b 100644
--- a/routers/common/redirect.go
+++ b/routers/common/redirect.go
@@ -17,7 +17,7 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) {
 	// The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2",
 	// then frontend needs this delegate to redirect to the new location with hash correctly.
 	redirect := req.PostFormValue("redirect")
-	if httplib.IsRiskyRedirectURL(redirect) {
+	if !httplib.IsCurrentGiteaSiteURL(redirect) {
 		resp.WriteHeader(http.StatusBadRequest)
 		return
 	}
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index da6bef207a..ab81740e3f 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -133,7 +133,7 @@ func RedirectAfterLogin(ctx *context.Context) {
 	if setting.LandingPageURL == setting.LandingPageLogin {
 		nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
 	}
-	ctx.RedirectToFirst(redirectTo, nextRedirectTo)
+	ctx.RedirectToCurrentSite(redirectTo, nextRedirectTo)
 }
 
 func CheckAutoLogin(ctx *context.Context) bool {
@@ -371,7 +371,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
 	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
 		if obeyRedirect {
-			ctx.RedirectToFirst(redirectTo)
+			ctx.RedirectToCurrentSite(redirectTo)
 		}
 		return redirectTo
 	}
@@ -808,7 +808,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
 	ctx.Flash.Success(ctx.Tr("auth.account_activated"))
 	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
-		ctx.RedirectToFirst(redirectTo)
+		ctx.RedirectToCurrentSite(redirectTo)
 		return
 	}
 
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index d5ca7397f0..3189d1372e 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -1157,7 +1157,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
 
 		if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
 			middleware.DeleteRedirectToCookie(ctx.Resp)
-			ctx.RedirectToFirst(redirectTo)
+			ctx.RedirectToCurrentSite(redirectTo)
 			return
 		}
 
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index c9e0386041..3af8b7edf2 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -314,7 +314,7 @@ func MustChangePasswordPost(ctx *context.Context) {
 
 	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
-		ctx.RedirectToFirst(redirectTo)
+		ctx.RedirectToCurrentSite(redirectTo)
 		return
 	}
 
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 7f9bf3210a..0490feb621 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -371,7 +371,7 @@ func Action(ctx *context.Context) {
 		return
 	}
 
-	ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
+	ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
 }
 
 func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
diff --git a/routers/web/web.go b/routers/web/web.go
index fc1432873f..3d790d1621 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -174,7 +174,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
 
 		// Redirect to dashboard (or alternate location) if user tries to visit any non-login page.
 		if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
-			ctx.RedirectToFirst(ctx.FormString("redirect_to"))
+			ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"))
 			return
 		}
 
diff --git a/services/context/context_response.go b/services/context/context_response.go
index 372b4cb38b..d7fd18acac 100644
--- a/services/context/context_response.go
+++ b/services/context/context_response.go
@@ -44,14 +44,14 @@ func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
 	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
 }
 
-// RedirectToFirst redirects to first not empty URL
-func (ctx *Context) RedirectToFirst(location ...string) {
+// RedirectToCurrentSite redirects to first not empty URL which belongs to current site
+func (ctx *Context) RedirectToCurrentSite(location ...string) {
 	for _, loc := range location {
 		if len(loc) == 0 {
 			continue
 		}
 
-		if httplib.IsRiskyRedirectURL(loc) {
+		if !httplib.IsCurrentGiteaSiteURL(loc) {
 			continue
 		}
 

From 62f8174aa2fae1481c7e17a6afcb731a5b178cd0 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 21 Mar 2024 21:13:08 +0800
Subject: [PATCH 461/679] Performance improvements for pull request list page
 (#29900)

This PR will avoid load pullrequest.Issue twice in pull request list
page. It will reduce x times database queries for those WIP pull
requests.

Partially fix #29585

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 models/activities/notification_list.go | 29 ++++++++++++++++++++++++++
 models/issues/issue.go                 | 14 -------------
 models/issues/issue_list.go            |  3 +++
 models/issues/pull_list.go             |  9 ++++++++
 models/issues/review.go                |  9 ++++----
 modules/util/slice.go                  | 11 +++++++++-
 routers/api/v1/repo/issue.go           |  5 +++--
 routers/api/v1/repo/issue_pin.go       | 16 +++++---------
 routers/web/user/notification.go       |  6 ++++++
 services/convert/notification.go       |  5 +++--
 services/pull/review.go                |  4 ++--
 templates/shared/issueicon.tmpl        | 16 +++++++-------
 tests/integration/pull_merge_test.go   |  4 ++--
 tests/integration/pull_update_test.go  |  5 ++---
 14 files changed, 86 insertions(+), 50 deletions(-)

diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go
index 957f9456e7..5858933391 100644
--- a/models/activities/notification_list.go
+++ b/models/activities/notification_list.go
@@ -14,6 +14,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 )
@@ -470,3 +471,31 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
 	}
 	return failures, nil
 }
+
+// LoadIssuePullRequests loads all issues' pull requests if possible
+func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error {
+	issues := make(map[int64]*issues_model.Issue, len(nl))
+	for _, notification := range nl {
+		if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil {
+			issues[notification.Issue.ID] = notification.Issue
+		}
+	}
+
+	if len(issues) == 0 {
+		return nil
+	}
+
+	pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues))
+	if err != nil {
+		return err
+	}
+
+	for _, pull := range pulls {
+		if issue := issues[pull.IssueID]; issue != nil {
+			issue.PullRequest = pull
+			issue.PullRequest.Issue = issue
+		}
+	}
+
+	return nil
+}
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 563a780dcb..87c1c86eb1 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -193,20 +193,6 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
 	return issue.Repo.IsTimetrackerEnabled(ctx)
 }
 
-// GetPullRequest returns the issue pull request
-func (issue *Issue) GetPullRequest(ctx context.Context) (pr *PullRequest, err error) {
-	if !issue.IsPull {
-		return nil, fmt.Errorf("Issue is not a pull request")
-	}
-
-	pr, err = GetPullRequestByIssueID(ctx, issue.ID)
-	if err != nil {
-		return nil, err
-	}
-	pr.Issue = issue
-	return pr, err
-}
-
 // LoadPoster loads poster
 func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
 	if issue.Poster == nil && issue.PosterID != 0 {
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index 41a90d133d..218891ad35 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -370,6 +370,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error {
 
 	for _, issue := range issues {
 		issue.PullRequest = pullRequestMaps[issue.ID]
+		if issue.PullRequest != nil {
+			issue.PullRequest.Issue = issue
+		}
 	}
 	return nil
 }
diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index c209386e2e..2ee69cd323 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -212,3 +212,12 @@ func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bo
 		Limit(1).
 		Get(new(Issue))
 }
+
+// GetPullRequestByIssueIDs returns all pull requests by issue ids
+func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) {
+	prs := make([]*PullRequest, 0, len(issueIDs))
+	return prs, db.GetEngine(ctx).
+		Where("issue_id > 0").
+		In("issue_id", issueIDs).
+		Find(&prs)
+}
diff --git a/models/issues/review.go b/models/issues/review.go
index fc110630e0..70aba0f94d 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -239,11 +239,11 @@ type CreateReviewOptions struct {
 
 // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
 func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) {
-	pr, err := GetPullRequestByIssueID(ctx, issue.ID)
-	if err != nil {
+	if err := issue.LoadPullRequest(ctx); err != nil {
 		return false, err
 	}
 
+	pr := issue.PullRequest
 	rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
 	if err != nil {
 		return false, err
@@ -271,11 +271,10 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.
 
 // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
 func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) {
-	pr, err := GetPullRequestByIssueID(ctx, issue.ID)
-	if err != nil {
+	if err := issue.LoadPullRequest(ctx); err != nil {
 		return false, err
 	}
-	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, issue.PullRequest.BaseRepoID, issue.PullRequest.BaseBranch)
 	if err != nil {
 		return false, err
 	}
diff --git a/modules/util/slice.go b/modules/util/slice.go
index f00e84bf06..9c878c24be 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -54,7 +54,7 @@ func Sorted[S ~[]E, E cmp.Ordered](values S) S {
 	return values
 }
 
-// TODO: Replace with "maps.Values" once available
+// TODO: Replace with "maps.Values" once available, current it only in golang.org/x/exp/maps but not in standard library
 func ValuesOfMap[K comparable, V any](m map[K]V) []V {
 	values := make([]V, 0, len(m))
 	for _, v := range m {
@@ -62,3 +62,12 @@ func ValuesOfMap[K comparable, V any](m map[K]V) []V {
 	}
 	return values
 }
+
+// TODO: Replace with "maps.Keys" once available, current it only in golang.org/x/exp/maps but not in standard library
+func KeysOfMap[K comparable, V any](m map[K]V) []K {
+	keys := make([]K, 0, len(m))
+	for k := range m {
+		keys = append(keys, k)
+	}
+	return keys
+}
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 61a318baab..6934b34b24 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -872,10 +872,11 @@ func EditIssue(ctx *context.APIContext) {
 	}
 	if form.State != nil {
 		if issue.IsPull {
-			if pr, err := issue.GetPullRequest(ctx); err != nil {
+			if err := issue.LoadPullRequest(ctx); err != nil {
 				ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
 				return
-			} else if pr.HasMerged {
+			}
+			if issue.PullRequest.HasMerged {
 				ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
 				return
 			}
diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go
index ff1135862b..8fcf670fd0 100644
--- a/routers/api/v1/repo/issue_pin.go
+++ b/routers/api/v1/repo/issue_pin.go
@@ -240,18 +240,12 @@ func ListPinnedPullRequests(ctx *context.APIContext) {
 	}
 
 	apiPrs := make([]*api.PullRequest, len(issues))
+	if err := issues.LoadPullRequests(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadPullRequests", err)
+		return
+	}
 	for i, currentIssue := range issues {
-		pr, err := currentIssue.GetPullRequest(ctx)
-		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
-			return
-		}
-
-		if err = pr.LoadIssue(ctx); err != nil {
-			ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
-			return
-		}
-
+		pr := currentIssue.PullRequest
 		if err = pr.LoadAttributes(ctx); err != nil {
 			ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 			return
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 438462371b..28f9846d6b 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -144,6 +144,12 @@ func getNotifications(ctx *context.Context) {
 		ctx.ServerError("LoadIssues", err)
 		return
 	}
+
+	if err = notifications.LoadIssuePullRequests(ctx); err != nil {
+		ctx.ServerError("LoadIssuePullRequests", err)
+		return
+	}
+
 	notifications = notifications.Without(failures)
 	failCount += len(failures)
 
diff --git a/services/convert/notification.go b/services/convert/notification.go
index 0b97530d8b..41063cf399 100644
--- a/services/convert/notification.go
+++ b/services/convert/notification.go
@@ -61,8 +61,9 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
 				result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx)
 			}
 
-			pr, _ := n.Issue.GetPullRequest(ctx)
-			if pr != nil && pr.HasMerged {
+			if err := n.Issue.LoadPullRequest(ctx); err == nil &&
+				n.Issue.PullRequest != nil &&
+				n.Issue.PullRequest.HasMerged {
 				result.Subject.State = "merged"
 			}
 		}
diff --git a/services/pull/review.go b/services/pull/review.go
index 90d07c8358..8900ae2ab1 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -268,11 +268,11 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
 
 // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
 func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) {
-	pr, err := issue.GetPullRequest(ctx)
-	if err != nil {
+	if err := issue.LoadPullRequest(ctx); err != nil {
 		return nil, nil, err
 	}
 
+	pr := issue.PullRequest
 	var stale bool
 	if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
 		stale = false
diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl
index 089e80bd8b..a62714e988 100644
--- a/templates/shared/issueicon.tmpl
+++ b/templates/shared/issueicon.tmpl
@@ -1,15 +1,15 @@
 {{if .IsPull}}
-	{{if and .PullRequest .PullRequest.HasMerged}}
-		{{svg "octicon-git-merge" 16 "text purple"}}
-	{{else if and (.GetPullRequest ctx) (.GetPullRequest ctx).HasMerged}}
-		{{svg "octicon-git-merge" 16 "text purple"}}
+	{{if not .PullRequest}}
+		No PullRequest
 	{{else}}
 		{{if .IsClosed}}
-			{{svg "octicon-git-pull-request" 16 "text red"}}
+			{{if .PullRequest.HasMerged}}
+				{{svg "octicon-git-merge" 16 "text purple"}}
+			{{else}}
+				{{svg "octicon-git-pull-request" 16 "text red"}}
+			{{end}}
 		{{else}}
-			{{if and .PullRequest (.PullRequest.IsWorkInProgress ctx)}}
-				{{svg "octicon-git-pull-request-draft" 16 "text grey"}}
-			{{else if and (.GetPullRequest ctx) ((.GetPullRequest ctx).IsWorkInProgress ctx)}}
+			{{if .PullRequest.IsWorkInProgress ctx}}
 				{{svg "octicon-git-pull-request-draft" 16 "text grey"}}
 			{{else}}
 				{{svg "octicon-git-pull-request" 16 "text green"}}
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index a04b4c98cd..daf411f452 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -516,8 +516,8 @@ func TestConflictChecking(t *testing.T) {
 		assert.NoError(t, err)
 
 		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
-		conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
-		assert.NoError(t, err)
+		assert.NoError(t, issue.LoadPullRequest(db.DefaultContext))
+		conflictingPR := issue.PullRequest
 
 		// Ensure conflictedFiles is populated.
 		assert.Len(t, conflictingPR.ConflictedFiles, 1)
diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go
index 078253ffb0..5ae241f3af 100644
--- a/tests/integration/pull_update_test.go
+++ b/tests/integration/pull_update_test.go
@@ -177,8 +177,7 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
 	assert.NoError(t, err)
 
 	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
-	pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
-	assert.NoError(t, err)
+	assert.NoError(t, issue.LoadPullRequest(db.DefaultContext))
 
-	return pr
+	return issue.PullRequest
 }

From 82979588f4d8699097451ebb70c56a4bdd090c52 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 21 Mar 2024 15:05:24 +0100
Subject: [PATCH 462/679] Switch to happy-dom for testing (#29948)

Use `happy-dom` again in vitest as it has caught up recently to `jsdom`
in terms of features and it is a much more lightweight solution.

I encountered [one
bug](https://github.com/capricorn86/happy-dom/issues/1342), but it's an
easy workaround until fixed.

I regenerated the lockfile to get rid of the transitive dependencies so
that's why the diff also has some upgrades in it.

In total, this change removes 39 npm dependencies.
---
 package-lock.json | 1383 +++++++++++----------------------------------
 package.json      |    2 +-
 vitest.config.js  |    2 +-
 web_src/js/svg.js |    2 +-
 4 files changed, 323 insertions(+), 1066 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 1ec1a62105..ef1164cac3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -85,7 +85,7 @@
         "eslint-plugin-vue": "9.23.0",
         "eslint-plugin-vue-scoped-css": "2.7.2",
         "eslint-plugin-wc": "2.0.4",
-        "jsdom": "24.0.0",
+        "happy-dom": "14.2.0",
         "markdownlint-cli": "0.39.0",
         "postcss-html": "1.6.0",
         "stylelint": "16.2.1",
@@ -130,81 +130,17 @@
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
-      "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
+      "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
       "dependencies": {
-        "@babel/highlight": "^7.23.4",
-        "chalk": "^2.4.2"
+        "@babel/highlight": "^7.24.2",
+        "picocolors": "^1.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/code-frame/node_modules/ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dependencies": {
-        "color-convert": "^1.9.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
-    },
-    "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dependencies": {
-        "has-flag": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/@babel/helper-validator-identifier": {
       "version": "7.22.20",
       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
@@ -214,13 +150,14 @@
       }
     },
     "node_modules/@babel/highlight": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
-      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
+      "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0"
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -296,9 +233,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
-      "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
+      "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -307,9 +244,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
-      "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
+      "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -2204,9 +2141,9 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.5",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz",
-      "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==",
+      "version": "8.56.6",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.6.tgz",
+      "integrity": "sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==",
       "dependencies": {
         "@types/estree": "*",
         "@types/json-schema": "*"
@@ -2256,9 +2193,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.27",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz",
-      "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==",
+      "version": "20.11.30",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
+      "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -2301,16 +2238,16 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz",
-      "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz",
+      "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "7.2.0",
-        "@typescript-eslint/type-utils": "7.2.0",
-        "@typescript-eslint/utils": "7.2.0",
-        "@typescript-eslint/visitor-keys": "7.2.0",
+        "@typescript-eslint/scope-manager": "7.3.1",
+        "@typescript-eslint/type-utils": "7.3.1",
+        "@typescript-eslint/utils": "7.3.1",
+        "@typescript-eslint/visitor-keys": "7.3.1",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
@@ -2319,7 +2256,7 @@
         "ts-api-utils": "^1.0.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2336,19 +2273,19 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz",
-      "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz",
+      "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.2.0",
-        "@typescript-eslint/types": "7.2.0",
-        "@typescript-eslint/typescript-estree": "7.2.0",
-        "@typescript-eslint/visitor-keys": "7.2.0",
+        "@typescript-eslint/scope-manager": "7.3.1",
+        "@typescript-eslint/types": "7.3.1",
+        "@typescript-eslint/typescript-estree": "7.3.1",
+        "@typescript-eslint/visitor-keys": "7.3.1",
         "debug": "^4.3.4"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2364,16 +2301,16 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz",
-      "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz",
+      "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.2.0",
-        "@typescript-eslint/visitor-keys": "7.2.0"
+        "@typescript-eslint/types": "7.3.1",
+        "@typescript-eslint/visitor-keys": "7.3.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2381,18 +2318,18 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz",
-      "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz",
+      "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.2.0",
-        "@typescript-eslint/utils": "7.2.0",
+        "@typescript-eslint/typescript-estree": "7.3.1",
+        "@typescript-eslint/utils": "7.3.1",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2408,12 +2345,12 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz",
-      "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz",
+      "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==",
       "dev": true,
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2421,13 +2358,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz",
-      "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz",
+      "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.2.0",
-        "@typescript-eslint/visitor-keys": "7.2.0",
+        "@typescript-eslint/types": "7.3.1",
+        "@typescript-eslint/visitor-keys": "7.3.1",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2436,7 +2373,7 @@
         "ts-api-utils": "^1.0.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2449,21 +2386,21 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz",
-      "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz",
+      "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "7.2.0",
-        "@typescript-eslint/types": "7.2.0",
-        "@typescript-eslint/typescript-estree": "7.2.0",
+        "@typescript-eslint/scope-manager": "7.3.1",
+        "@typescript-eslint/types": "7.3.1",
+        "@typescript-eslint/typescript-estree": "7.3.1",
         "semver": "^7.5.4"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2474,16 +2411,16 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz",
-      "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz",
+      "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.2.0",
+        "@typescript-eslint/types": "7.3.1",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2978,18 +2915,6 @@
         "webpack": ">=5"
       }
     },
-    "node_modules/agent-base": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
-      "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
-      "dev": true,
-      "dependencies": {
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
     "node_modules/ajv": {
       "version": "8.12.0",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@@ -3146,15 +3071,16 @@
       }
     },
     "node_modules/array-includes": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
-      "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
+      "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "get-intrinsic": "^1.2.1",
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
         "is-string": "^1.0.7"
       },
       "engines": {
@@ -3173,35 +3099,17 @@
         "node": ">=8"
       }
     },
-    "node_modules/array.prototype.filter": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz",
-      "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "es-array-method-boxes-properly": "^1.0.0",
-        "is-string": "^1.0.7"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
     "node_modules/array.prototype.findlastindex": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz",
-      "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz",
+      "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.3",
+        "es-abstract": "^1.23.2",
         "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
         "es-shim-unscopables": "^1.0.2"
       },
       "engines": {
@@ -3332,21 +3240,6 @@
         "astring": "bin/astring"
       }
     },
-    "node_modules/asynciterator.prototype": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
-      "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==",
-      "dev": true,
-      "dependencies": {
-        "has-symbols": "^1.0.3"
-      }
-    },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "dev": true
-    },
     "node_modules/atob": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -3582,9 +3475,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001597",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
-      "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
+      "version": "1.0.30001599",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
+      "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
       "funding": [
         {
           "type": "opencollective",
@@ -3868,18 +3761,6 @@
       "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
       "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
     },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dev": true,
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
     "node_modules/commander": {
       "version": "8.3.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -3909,12 +3790,12 @@
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
     "node_modules/core-js-compat": {
-      "version": "3.36.0",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
-      "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
+      "version": "3.36.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
+      "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.22.3"
+        "browserslist": "^4.23.0"
       },
       "funding": {
         "type": "opencollective",
@@ -4106,18 +3987,6 @@
       "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
       "dev": true
     },
-    "node_modules/cssstyle": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
-      "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==",
-      "dev": true,
-      "dependencies": {
-        "rrweb-cssom": "^0.6.0"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/csstype": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -4580,17 +4449,55 @@
       "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
       "dev": true
     },
-    "node_modules/data-urls": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
-      "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+    "node_modules/data-view-buffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
+      "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
       "dev": true,
       "dependencies": {
-        "whatwg-mimetype": "^4.0.0",
-        "whatwg-url": "^14.0.0"
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
       },
       "engines": {
-        "node": ">=18"
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/data-view-byte-length": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
+      "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/data-view-byte-offset": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
+      "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/dayjs": {
@@ -4614,12 +4521,6 @@
         }
       }
     },
-    "node_modules/decimal.js": {
-      "version": "10.4.3",
-      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
-      "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
-      "dev": true
-    },
     "node_modules/decode-named-character-reference": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
@@ -4710,15 +4611,6 @@
         "robust-predicates": "^3.0.2"
       }
     },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
     "node_modules/dependency-graph": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
@@ -4829,9 +4721,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.0.9",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz",
-      "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ=="
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.10.tgz",
+      "integrity": "sha512-WZDL8ZHTliEVP3Lk4phtvjg8SNQ3YMc5WVstxE8cszKZrFjzI4PF4ZTIk9VGAc9vZADO7uGO2V/ZiStcRSAT4Q=="
     },
     "node_modules/domutils": {
       "version": "3.1.0",
@@ -4874,9 +4766,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.706",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.706.tgz",
-      "integrity": "sha512-fO01fufoGd6jKK3HR8ofBapF3ZPfgxNJ/ua9xQAhFu93TwWIs4d+weDn3kje3GB4S7aGUTfk5nvdU5F7z5mF9Q=="
+      "version": "1.4.713",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.713.tgz",
+      "integrity": "sha512-vDarADhwntXiULEdmWd77g2dV6FrNGa8ecAC29MZ4TwPut2fvosD0/5sJd1qWNNe8HcJFAC+F5Lf9jW1NPtWmw=="
     },
     "node_modules/elkjs": {
       "version": "0.9.2",
@@ -4947,17 +4839,21 @@
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.22.5",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz",
-      "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==",
+      "version": "1.23.2",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz",
+      "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==",
       "dev": true,
       "dependencies": {
         "array-buffer-byte-length": "^1.0.1",
         "arraybuffer.prototype.slice": "^1.0.3",
         "available-typed-arrays": "^1.0.7",
         "call-bind": "^1.0.7",
+        "data-view-buffer": "^1.0.1",
+        "data-view-byte-length": "^1.0.1",
+        "data-view-byte-offset": "^1.0.0",
         "es-define-property": "^1.0.0",
         "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
         "es-set-tostringtag": "^2.0.3",
         "es-to-primitive": "^1.2.1",
         "function.prototype.name": "^1.1.6",
@@ -4968,10 +4864,11 @@
         "has-property-descriptors": "^1.0.2",
         "has-proto": "^1.0.3",
         "has-symbols": "^1.0.3",
-        "hasown": "^2.0.1",
+        "hasown": "^2.0.2",
         "internal-slot": "^1.0.7",
         "is-array-buffer": "^3.0.4",
         "is-callable": "^1.2.7",
+        "is-data-view": "^1.0.1",
         "is-negative-zero": "^2.0.3",
         "is-regex": "^1.1.4",
         "is-shared-array-buffer": "^1.0.3",
@@ -4982,17 +4879,17 @@
         "object-keys": "^1.1.1",
         "object.assign": "^4.1.5",
         "regexp.prototype.flags": "^1.5.2",
-        "safe-array-concat": "^1.1.0",
+        "safe-array-concat": "^1.1.2",
         "safe-regex-test": "^1.0.3",
-        "string.prototype.trim": "^1.2.8",
-        "string.prototype.trimend": "^1.0.7",
+        "string.prototype.trim": "^1.2.9",
+        "string.prototype.trimend": "^1.0.8",
         "string.prototype.trimstart": "^1.0.7",
         "typed-array-buffer": "^1.0.2",
         "typed-array-byte-length": "^1.0.1",
         "typed-array-byte-offset": "^1.0.2",
         "typed-array-length": "^1.0.5",
         "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.14"
+        "which-typed-array": "^1.1.15"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5023,12 +4920,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/es-array-method-boxes-properly": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
-      "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
-      "dev": true
-    },
     "node_modules/es-define-property": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -5051,35 +4942,46 @@
       }
     },
     "node_modules/es-iterator-helpers": {
-      "version": "1.0.17",
-      "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz",
-      "integrity": "sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==",
+      "version": "1.0.18",
+      "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz",
+      "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==",
       "dev": true,
       "dependencies": {
-        "asynciterator.prototype": "^1.0.0",
         "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.4",
+        "es-abstract": "^1.23.0",
         "es-errors": "^1.3.0",
-        "es-set-tostringtag": "^2.0.2",
+        "es-set-tostringtag": "^2.0.3",
         "function-bind": "^1.1.2",
         "get-intrinsic": "^1.2.4",
         "globalthis": "^1.0.3",
         "has-property-descriptors": "^1.0.2",
-        "has-proto": "^1.0.1",
+        "has-proto": "^1.0.3",
         "has-symbols": "^1.0.3",
         "internal-slot": "^1.0.7",
         "iterator.prototype": "^1.1.2",
-        "safe-array-concat": "^1.1.0"
+        "safe-array-concat": "^1.1.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/es-module-lexer": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
-      "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.2.tgz",
+      "integrity": "sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw=="
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+      "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
     },
     "node_modules/es-set-tostringtag": {
       "version": "2.0.3",
@@ -6131,25 +6033,6 @@
         }
       }
     },
-    "node_modules/fetch-ponyfill/node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
-    },
-    "node_modules/fetch-ponyfill/node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
-    },
-    "node_modules/fetch-ponyfill/node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
     "node_modules/file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -6241,20 +6124,6 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/form-data": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-      "dev": true,
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
     "node_modules/fs-extra": {
       "version": "10.1.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -6632,6 +6501,20 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/happy-dom": {
+      "version": "14.2.0",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.2.0.tgz",
+      "integrity": "sha512-vTqF/9MEkRKgYy5eKq9W0uiNmkgnVAmJhRwn8x8fQBR7lc4C84859jLhgZ1lR4Gi/t70oSdgvtLpxlHjgdJrAw==",
+      "dev": true,
+      "dependencies": {
+        "entities": "^4.5.0",
+        "webidl-conversions": "^7.0.0",
+        "whatwg-mimetype": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
     "node_modules/has-bigints": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -6736,18 +6619,6 @@
         "node": ">=14"
       }
     },
-    "node_modules/html-encoding-sniffer": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
-      "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
-      "dev": true,
-      "dependencies": {
-        "whatwg-encoding": "^3.1.1"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/html-tags": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@@ -6784,32 +6655,6 @@
       "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.11.tgz",
       "integrity": "sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw=="
     },
-    "node_modules/http-proxy-agent": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
-      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
-      "dev": true,
-      "dependencies": {
-        "agent-base": "^7.1.0",
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/https-proxy-agent": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
-      "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
-      "dev": true,
-      "dependencies": {
-        "agent-base": "^7.0.2",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
     "node_modules/human-signals": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@@ -7101,6 +6946,21 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-data-view": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
+      "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
+      "dev": true,
+      "dependencies": {
+        "is-typed-array": "^1.1.13"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-date-object": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
@@ -7568,55 +7428,6 @@
         "node": ">=12.0.0"
       }
     },
-    "node_modules/jsdom": {
-      "version": "24.0.0",
-      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz",
-      "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==",
-      "dev": true,
-      "dependencies": {
-        "cssstyle": "^4.0.1",
-        "data-urls": "^5.0.0",
-        "decimal.js": "^10.4.3",
-        "form-data": "^4.0.0",
-        "html-encoding-sniffer": "^4.0.0",
-        "http-proxy-agent": "^7.0.0",
-        "https-proxy-agent": "^7.0.2",
-        "is-potential-custom-element-name": "^1.0.1",
-        "nwsapi": "^2.2.7",
-        "parse5": "^7.1.2",
-        "rrweb-cssom": "^0.6.0",
-        "saxes": "^6.0.0",
-        "symbol-tree": "^3.2.4",
-        "tough-cookie": "^4.1.3",
-        "w3c-xmlserializer": "^5.0.0",
-        "webidl-conversions": "^7.0.0",
-        "whatwg-encoding": "^3.1.1",
-        "whatwg-mimetype": "^4.0.0",
-        "whatwg-url": "^14.0.0",
-        "ws": "^8.16.0",
-        "xml-name-validator": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "peerDependencies": {
-        "canvas": "^2.11.2"
-      },
-      "peerDependenciesMeta": {
-        "canvas": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/jsdom/node_modules/xml-name-validator": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
-      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
-      "dev": true,
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/jsep": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.8.tgz",
@@ -8956,25 +8767,6 @@
         }
       }
     },
-    "node_modules/node-fetch/node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
-    },
-    "node_modules/node-fetch/node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
-    },
-    "node_modules/node-fetch/node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
     "node_modules/node-releases": {
       "version": "2.0.14",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -9066,12 +8858,6 @@
         "url": "https://github.com/fb55/nth-check?sponsor=1"
       }
     },
-    "node_modules/nwsapi": {
-      "version": "2.2.7",
-      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
-      "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==",
-      "dev": true
-    },
     "node_modules/obj-props": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/obj-props/-/obj-props-1.4.0.tgz",
@@ -9134,28 +8920,29 @@
       }
     },
     "node_modules/object.entries": {
-      "version": "1.1.7",
-      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz",
-      "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==",
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
+      "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/object.fromentries": {
-      "version": "2.0.7",
-      "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
-      "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+      "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -9165,27 +8952,28 @@
       }
     },
     "node_modules/object.groupby": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz",
-      "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+      "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
       "dev": true,
       "dependencies": {
-        "array.prototype.filter": "^1.0.3",
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.3",
-        "es-errors": "^1.0.0"
+        "es-abstract": "^1.23.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/object.values": {
-      "version": "1.1.7",
-      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
-      "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
+      "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -9311,18 +9099,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/parse5": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
-      "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
-      "dev": true,
-      "dependencies": {
-        "entities": "^4.4.0"
-      },
-      "funding": {
-        "url": "https://github.com/inikulin/parse5?sponsor=1"
-      }
-    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -9949,12 +9725,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/psl": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
-      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
-      "dev": true
-    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -9972,12 +9742,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/querystringify": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
-      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
-      "dev": true
-    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -10156,16 +9920,16 @@
       }
     },
     "node_modules/reflect.getprototypeof": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz",
-      "integrity": "sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
+      "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.3",
-        "es-errors": "^1.0.0",
-        "get-intrinsic": "^1.2.3",
+        "es-abstract": "^1.23.1",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
         "globalthis": "^1.0.3",
         "which-builtin-type": "^1.1.3"
       },
@@ -10259,12 +10023,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/requires-port": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
-      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
-      "dev": true
-    },
     "node_modules/reserved": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/reserved/-/reserved-0.1.2.tgz",
@@ -10369,12 +10127,6 @@
         "fsevents": "~2.3.2"
       }
     },
-    "node_modules/rrweb-cssom": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
-      "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
-      "dev": true
-    },
     "node_modules/run-con": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz",
@@ -10499,18 +10251,6 @@
       "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
       "dev": true
     },
-    "node_modules/saxes": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
-      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
-      "dev": true,
-      "dependencies": {
-        "xmlchars": "^2.2.0"
-      },
-      "engines": {
-        "node": ">=v12.22.7"
-      }
-    },
     "node_modules/schema-utils": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@@ -10720,12 +10460,12 @@
       }
     },
     "node_modules/solid-js": {
-      "version": "1.8.15",
-      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.15.tgz",
-      "integrity": "sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==",
+      "version": "1.8.16",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.16.tgz",
+      "integrity": "sha512-rja94MNU9flF3qQRLNsu60QHKBDKBkVE1DldJZPIfn2ypIn3NV2WpSbGTQIvsyGPBo+9E2IMjwqnqpbgfWuzeg==",
       "dependencies": {
         "csstype": "^3.1.0",
-        "seroval": "^1.0.3",
+        "seroval": "^1.0.4",
         "seroval-plugins": "^1.0.3"
       }
     },
@@ -10748,9 +10488,9 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -10904,14 +10644,15 @@
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     "node_modules/string.prototype.trim": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
-      "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
+      "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.0",
+        "es-object-atoms": "^1.0.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -10921,14 +10662,14 @@
       }
     },
     "node_modules/string.prototype.trimend": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
-      "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
+      "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -11386,12 +11127,6 @@
       "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.12.0.tgz",
       "integrity": "sha512-Rt1xUpbHulJVGbiQjq9yy9/r/0Pg6TmpcG+fXTaMePDc8z5WUw4LfaWts5qcNv/8ewPvBIbY7DKq7qReIKNCCQ=="
     },
-    "node_modules/symbol-tree": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
-      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
-      "dev": true
-    },
     "node_modules/sync-fetch": {
       "version": "0.4.5",
       "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz",
@@ -11724,41 +11459,10 @@
       "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
       "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ=="
     },
-    "node_modules/tough-cookie": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
-      "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
-      "dev": true,
-      "dependencies": {
-        "psl": "^1.1.33",
-        "punycode": "^2.1.1",
-        "universalify": "^0.2.0",
-        "url-parse": "^1.5.3"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/tough-cookie/node_modules/universalify": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
-      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
-      "dev": true,
-      "engines": {
-        "node": ">= 4.0.0"
-      }
-    },
     "node_modules/tr46": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
-      "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
-      "dev": true,
-      "dependencies": {
-        "punycode": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=18"
-      }
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
     },
     "node_modules/tributejs": {
       "version": "5.1.3",
@@ -11927,9 +11631,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.4.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
-      "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
+      "version": "5.4.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
+      "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
       "devOptional": true,
       "peer": true,
       "bin": {
@@ -11952,9 +11656,9 @@
       "dev": true
     },
     "node_modules/ufo": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz",
-      "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==",
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
+      "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==",
       "dev": true
     },
     "node_modules/uint8-to-base64": {
@@ -12058,16 +11762,6 @@
       "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
       "dev": true
     },
-    "node_modules/url-parse": {
-      "version": "1.5.10",
-      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
-      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
-      "dev": true,
-      "dependencies": {
-        "querystringify": "^2.1.1",
-        "requires-port": "^1.0.0"
-      }
-    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -12131,14 +11825,14 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.1.6",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz",
-      "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==",
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.2.tgz",
+      "integrity": "sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==",
       "dev": true,
       "dependencies": {
-        "esbuild": "^0.19.3",
-        "postcss": "^8.4.35",
-        "rollup": "^4.2.0"
+        "esbuild": "^0.20.1",
+        "postcss": "^8.4.36",
+        "rollup": "^4.13.0"
       },
       "bin": {
         "vite": "bin/vite.js"
@@ -12213,418 +11907,12 @@
       "integrity": "sha512-KRCIFX3PWVUuEjpi9O7EKLT9E27OqOA3RimIvVx6cziLAUxvnk2VvHQfMrP+mKkqyqqSmnnYyTig3OyDnK/zlA==",
       "dev": true
     },
-    "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
-      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "aix"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/android-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
-      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/android-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
-      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/android-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
-      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
-      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
-      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
-      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
-      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
-      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
-      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
-      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
-      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
-      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
-      "cpu": [
-        "mips64el"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
-      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
-      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
-      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
-      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
-      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
-      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
-      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
-      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
     "node_modules/vite/node_modules/@types/estree": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
       "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
       "dev": true
     },
-    "node_modules/vite/node_modules/esbuild": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
-      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.19.12",
-        "@esbuild/android-arm": "0.19.12",
-        "@esbuild/android-arm64": "0.19.12",
-        "@esbuild/android-x64": "0.19.12",
-        "@esbuild/darwin-arm64": "0.19.12",
-        "@esbuild/darwin-x64": "0.19.12",
-        "@esbuild/freebsd-arm64": "0.19.12",
-        "@esbuild/freebsd-x64": "0.19.12",
-        "@esbuild/linux-arm": "0.19.12",
-        "@esbuild/linux-arm64": "0.19.12",
-        "@esbuild/linux-ia32": "0.19.12",
-        "@esbuild/linux-loong64": "0.19.12",
-        "@esbuild/linux-mips64el": "0.19.12",
-        "@esbuild/linux-ppc64": "0.19.12",
-        "@esbuild/linux-riscv64": "0.19.12",
-        "@esbuild/linux-s390x": "0.19.12",
-        "@esbuild/linux-x64": "0.19.12",
-        "@esbuild/netbsd-x64": "0.19.12",
-        "@esbuild/openbsd-x64": "0.19.12",
-        "@esbuild/sunos-x64": "0.19.12",
-        "@esbuild/win32-arm64": "0.19.12",
-        "@esbuild/win32-ia32": "0.19.12",
-        "@esbuild/win32-x64": "0.19.12"
-      }
-    },
     "node_modules/vite/node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -12639,6 +11927,34 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/vite/node_modules/postcss": {
+      "version": "8.4.38",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.2.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
     "node_modules/vite/node_modules/rollup": {
       "version": "4.13.0",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
@@ -12843,27 +12159,6 @@
         "vue": "^3.2.29"
       }
     },
-    "node_modules/w3c-xmlserializer": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
-      "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
-      "dev": true,
-      "dependencies": {
-        "xml-name-validator": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
-      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
-      "dev": true,
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/watchpack": {
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
@@ -13088,40 +12383,29 @@
         "node": ">=10.13.0"
       }
     },
-    "node_modules/whatwg-encoding": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
-      "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
-      "dev": true,
-      "dependencies": {
-        "iconv-lite": "0.6.3"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/whatwg-mimetype": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
-      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+      "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
       "dev": true,
       "engines": {
-        "node": ">=18"
+        "node": ">=12"
       }
     },
     "node_modules/whatwg-url": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
-      "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
-      "dev": true,
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
       "dependencies": {
-        "tr46": "^5.0.0",
-        "webidl-conversions": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=18"
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
       }
     },
+    "node_modules/whatwg-url/node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -13344,27 +12628,6 @@
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
-    "node_modules/ws": {
-      "version": "8.16.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
-      "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.0.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": ">=5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/xml-name-validator": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@@ -13374,12 +12637,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/xmlchars": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
-      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
-      "dev": true
-    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 1be87e8b39..ced10f9c99 100644
--- a/package.json
+++ b/package.json
@@ -84,7 +84,7 @@
     "eslint-plugin-vue": "9.23.0",
     "eslint-plugin-vue-scoped-css": "2.7.2",
     "eslint-plugin-wc": "2.0.4",
-    "jsdom": "24.0.0",
+    "happy-dom": "14.2.0",
     "markdownlint-cli": "0.39.0",
     "postcss-html": "1.6.0",
     "stylelint": "16.2.1",
diff --git a/vitest.config.js b/vitest.config.js
index be6c0eadfa..ea0fafeee8 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -6,7 +6,7 @@ export default defineConfig({
   test: {
     include: ['web_src/**/*.test.js'],
     setupFiles: ['web_src/js/vitest.setup.js'],
-    environment: 'jsdom',
+    environment: 'happy-dom',
     testTimeout: 20000,
     open: false,
     allowOnly: true,
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 471b5136bd..6ad06f599d 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -205,7 +205,7 @@ export const SvgIcon = {
 
     // make the <SvgIcon class="foo" class-name="bar"> classes work together
     const classes = [];
-    for (const cls of svgOuter.classList) {
+    for (const cls of svgOuter.classList.values()) {
       classes.push(cls);
     }
     // TODO: drop the `className/class-name` prop in the future, only use "class" prop

From cdb4d1a8db096d60dba04728924dab85def45b19 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 21 Mar 2024 23:07:35 +0800
Subject: [PATCH 463/679] Refactor StringsToInt64s (#29967)

And close #27176
---
 models/issues/pull_list.go                 |  9 +++------
 models/issues/pull_test.go                 |  2 --
 modules/base/tool.go                       | 13 ++++++++-----
 modules/base/tool_test.go                  |  7 ++++---
 options/locale/locale_en-US.ini            |  1 +
 routers/api/v1/repo/pull.go                |  9 +++++++--
 routers/web/repo/issue.go                  |  3 +--
 routers/web/user/home.go                   |  7 ++-----
 routers/web/user/notification.go           |  3 +--
 templates/repo/issue/list.tmpl             |  1 +
 templates/repo/issue/milestone_issues.tmpl |  1 +
 templates/user/dashboard/issues.tmpl       |  1 +
 12 files changed, 30 insertions(+), 27 deletions(-)

diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index 2ee69cd323..de3eceed37 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -11,7 +11,6 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
 
@@ -23,7 +22,7 @@ type PullRequestsOptions struct {
 	db.ListOptions
 	State       string
 	SortType    string
-	Labels      []string
+	Labels      []int64
 	MilestoneID int64
 }
 
@@ -36,11 +35,9 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
 		sess.And("issue.is_closed=?", opts.State == "closed")
 	}
 
-	if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil {
-		return nil, err
-	} else if len(labelIDs) > 0 {
+	if len(opts.Labels) > 0 {
 		sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
-			In("issue_label.label_id", labelIDs)
+			In("issue_label.label_id", opts.Labels)
 	}
 
 	if opts.MilestoneID > 0 {
diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go
index 3a30b2f3de..675c90527d 100644
--- a/models/issues/pull_test.go
+++ b/models/issues/pull_test.go
@@ -66,7 +66,6 @@ func TestPullRequestsNewest(t *testing.T) {
 		},
 		State:    "open",
 		SortType: "newest",
-		Labels:   []string{},
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3, count)
@@ -113,7 +112,6 @@ func TestPullRequestsOldest(t *testing.T) {
 		},
 		State:    "open",
 		SortType: "oldest",
-		Labels:   []string{},
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3, count)
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 168a2220b2..40785e74e8 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -150,13 +150,16 @@ func TruncateString(str string, limit int) string {
 
 // StringsToInt64s converts a slice of string to a slice of int64.
 func StringsToInt64s(strs []string) ([]int64, error) {
-	ints := make([]int64, len(strs))
-	for i := range strs {
-		n, err := strconv.ParseInt(strs[i], 10, 64)
+	if strs == nil {
+		return nil, nil
+	}
+	ints := make([]int64, 0, len(strs))
+	for _, s := range strs {
+		n, err := strconv.ParseInt(s, 10, 64)
 		if err != nil {
-			return ints, err
+			return nil, err
 		}
-		ints[i] = n
+		ints = append(ints, n)
 	}
 	return ints, nil
 }
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index d28deb593d..f21b89c74c 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -138,12 +138,13 @@ func TestStringsToInt64s(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(t, expected, result)
 	}
+	testSuccess(nil, nil)
 	testSuccess([]string{}, []int64{})
 	testSuccess([]string{"-1234"}, []int64{-1234})
-	testSuccess([]string{"1", "4", "16", "64", "256"},
-		[]int64{1, 4, 16, 64, 256})
+	testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
 
-	_, err := StringsToInt64s([]string{"-1", "a", "$"})
+	ints, err := StringsToInt64s([]string{"-1", "a"})
+	assert.Len(t, ints, 0)
 	assert.Error(t, err)
 }
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6622a25efd..49a564a890 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -114,6 +114,7 @@ loading = Loading…
 error = Error
 error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
 go_back = Go Back
+invalid_data = Invalid data: %v
 
 never = Never
 unknown = Unknown
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index fc47656072..e43366ff14 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -21,6 +21,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
@@ -96,13 +97,17 @@ func ListPullRequests(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
+	labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "PullRequests", err)
+		return
+	}
 	listOptions := utils.GetListOptions(ctx)
-
 	prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
 		ListOptions: listOptions,
 		State:       ctx.FormTrim("state"),
 		SortType:    ctx.FormTrim("sort"),
-		Labels:      ctx.FormStrings("labels"),
+		Labels:      labelIDs,
 		MilestoneID: ctx.FormInt64("milestone"),
 	})
 	if err != nil {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index fb03ed9d9c..930a71d35f 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -187,8 +187,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
 	if len(selectLabels) > 0 {
 		labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
 		if err != nil {
-			ctx.ServerError("StringsToInt64s", err)
-			return
+			ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
 		}
 	}
 
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 465de500a0..ff6c2a6c36 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -529,17 +529,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	// Get IDs for labels (a filter option for issues/pulls).
 	// Required for IssuesOptions.
-	var labelIDs []int64
 	selectedLabels := ctx.FormString("labels")
 	if len(selectedLabels) > 0 && selectedLabels != "0" {
 		var err error
-		labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
+		opts.LabelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
 		if err != nil {
-			ctx.ServerError("StringsToInt64s", err)
-			return
+			ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
 		}
 	}
-	opts.LabelIDs = labelIDs
 
 	// ------------------------------
 	// Get issues as defined by opts.
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 28f9846d6b..ae0132e6e2 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -268,8 +268,7 @@ func NotificationSubscriptions(ctx *context.Context) {
 		var err error
 		labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
 		if err != nil {
-			ctx.ServerError("StringsToInt64s", err)
-			return
+			ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
 		}
 	}
 
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 62c1d00f00..45bddefa42 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -2,6 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository issue-list">
 	{{template "repo/header" .}}
 	<div class="ui container">
+	{{template "base/alert" .}}
 
 	{{if .PinnedIssues}}
 		<div id="issue-pins" {{if .IsRepoAdmin}}data-is-repo-admin{{end}}>
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index 35a8a77680..507c3ce37a 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -2,6 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository milestone-issue-list">
 	{{template "repo/header" .}}
 	<div class="ui container">
+		{{template "base/alert" .}}
 		<div class="gt-df">
 			<h1 class="gt-mb-3">{{.Milestone.Name}}</h1>
 			{{if not .Repository.IsArchived}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 88afcf58ec..5080821dd1 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -2,6 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content dashboard issues">
 	{{template "user/dashboard/navbar" .}}
 	<div class="ui container">
+		{{template "base/alert" .}}
 		<div class="flex-container">
 			<div class="flex-container-nav">
 				<div class="ui secondary vertical filter menu tw-bg-transparent">

From 4bef1fb3e49127316596cef1d3ca103a199c0536 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Fri, 22 Mar 2024 00:48:13 +0900
Subject: [PATCH 464/679] Update register application URL for GitLab (#29959)

Fix #26593
The old URL was updated 7 years ago. Maybe no need to hold it any more.

![image](https://github.com/go-gitea/gitea/assets/18380374/95a0b364-832b-4f10-800e-21845d64df1a)
---
 options/locale/locale_en-US.ini | 2 +-
 templates/admin/auth/new.tmpl   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 49a564a890..08b87134fd 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3093,7 +3093,7 @@ auths.tip.nextcloud = Register a new OAuth consumer on your instance using the f
 auths.tip.dropbox = Create a new application at https://www.dropbox.com/developers/apps
 auths.tip.facebook = Register a new application at https://developers.facebook.com/apps and add the product "Facebook Login"
 auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new
-auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications
+auths.tip.gitlab_new = Register a new application on https://gitlab.com/-/profile/applications
 auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at https://console.developers.google.com/
 auths.tip.openid_connect = Use the OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) to specify the endpoints
 auths.tip.twitter = Go to https://dev.twitter.com/apps, create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index f32f77d5dc..1e7e0d9b35 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -99,7 +99,7 @@
 				<li>GitHub</li>
 				<span>{{ctx.Locale.Tr "admin.auths.tip.github"}}</span>
 				<li>GitLab</li>
-				<span>{{ctx.Locale.Tr "admin.auths.tip.gitlab"}}</span>
+				<span>{{ctx.Locale.Tr "admin.auths.tip.gitlab_new"}}</span>
 				<li>Google</li>
 				<span>{{ctx.Locale.Tr "admin.auths.tip.google_plus"}}</span>
 				<li>OpenID Connect</li>

From d6fed9ab88b13e124c5e59ceac5b21a3af52ad24 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 21 Mar 2024 17:31:15 +0100
Subject: [PATCH 465/679] Fix various loading states, remove `.loading` class
 (#29920)

Various code was using fomantic `loading` class which I think got broken
a while ago and rendered only a full circle. Fix those to use
`is-loading`.

Before:
<img width="295" alt="Screenshot 2024-03-19 at 22 56 26"
src="https://github.com/go-gitea/gitea/assets/115237/dbe83395-5db4-4868-90bc-3613866a35f0">

After:
<img width="60" alt="Screenshot 2024-03-19 at 22 54 35"
src="https://github.com/go-gitea/gitea/assets/115237/8ac19b7e-035a-4c6d-850b-53a234ef69c2">
<img width="294" alt="Screenshot 2024-03-19 at 22 54 56"
src="https://github.com/go-gitea/gitea/assets/115237/34e819d7-25f7-43a1-9d48-4a68dcd2b6ad">
<img width="320" alt="Screenshot 2024-03-19 at 22 55 16"
src="https://github.com/go-gitea/gitea/assets/115237/05127544-47ff-4e18-9fd8-c84e44c374f8">
<img width="153" alt="Screenshot 2024-03-19 at 23 01 43"
src="https://github.com/go-gitea/gitea/assets/115237/a33248c6-b11d-40ff-82d8-f5a3d85b55aa">
<img width="1300" alt="Screenshot 2024-03-19 at 23 56 25"
src="https://github.com/go-gitea/gitea/assets/115237/562ca876-b5d5-4295-961e-9d2cdab31ab0">
<img width="136" alt="Screenshot 2024-03-20 at 00 00 38"
src="https://github.com/go-gitea/gitea/assets/115237/44838ac4-67f3-4fec-a8e3-978cc5dbdb72">
---
 templates/admin/notice.tmpl                  |  2 +-
 templates/repo/graph.tmpl                    |  2 +-
 templates/repo/settings/webhook/history.tmpl |  4 +++-
 web_src/css/base.css                         | 14 --------------
 web_src/css/features/gitgraph.css            |  6 ------
 web_src/js/features/admin/common.js          |  2 +-
 web_src/js/features/comp/WebHookEditor.js    |  2 +-
 web_src/js/features/repo-common.js           | 10 ++++++----
 web_src/js/features/repo-issue.js            | 10 +++++-----
 9 files changed, 18 insertions(+), 34 deletions(-)

diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index 26462596bc..f7d77eab1d 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -50,7 +50,7 @@
 									</div>
 								</div>
 								<button class="ui small teal button" id="delete-selection" data-link="{{.Link}}/delete" data-redirect="?page={{.Page.Paginater.Current}}">
-									{{ctx.Locale.Tr "admin.notices.delete_selected"}}
+									<span class="text">{{ctx.Locale.Tr "admin.notices.delete_selected"}}</span>
 								</button>
 							</th>
 						</tr>
diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl
index 37305d278a..67804f117d 100644
--- a/templates/repo/graph.tmpl
+++ b/templates/repo/graph.tmpl
@@ -50,7 +50,7 @@
 				</div>
 			</h2>
 			<div class="ui dividing"></div>
-			<div class="ui segment loading gt-hidden" id="loading-indicator"></div>
+			<div class="is-loading tw-py-32 gt-hidden" id="loading-indicator"></div>
 			{{template "repo/graph/svgcontainer" .}}
 			{{template "repo/graph/commits" .}}
 		</div>
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl
index 4e0f0e9c3e..9f7a7816ea 100644
--- a/templates/repo/settings/webhook/history.tmpl
+++ b/templates/repo/settings/webhook/history.tmpl
@@ -6,7 +6,9 @@
 			<div class="ui right">
 				<!-- the button is wrapped with a span because the tooltip doesn't show on hover if we put data-tooltip-content directly on the button -->
 				<span data-tooltip-content="{{if or $isNew .Webhook.IsActive}}{{ctx.Locale.Tr "repo.settings.webhook.test_delivery_desc"}}{{else}}{{ctx.Locale.Tr "repo.settings.webhook.test_delivery_desc_disabled"}}{{end}}">
-					<button class="ui teal tiny button{{if not (or $isNew .Webhook.IsActive)}} disabled{{end}}" id="test-delivery" data-link="{{.Link}}/test" data-redirect="{{.Link}}">{{ctx.Locale.Tr "repo.settings.webhook.test_delivery"}}</button>
+					<button class="ui teal tiny button{{if not (or $isNew .Webhook.IsActive)}} disabled{{end}}" id="test-delivery" data-link="{{.Link}}/test" data-redirect="{{.Link}}">
+						<span class="text">{{ctx.Locale.Tr "repo.settings.webhook.test_delivery"}}</span>
+					</button>
 			</span>
 			</div>
 		{{end}}
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 58332fb21c..6280652086 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -710,16 +710,6 @@ img.ui.avatar,
   background: var(--color-active);
 }
 
-.ui.loading.segment::before,
-.ui.loading.form::before {
-  background: none;
-}
-
-.ui.loading.form > *,
-.ui.loading.segment > * {
-  opacity: 0.35;
-}
-
 .ui.form .fields.error .field textarea,
 .ui.form .fields.error .field select,
 .ui.form .fields.error .field input:not([type]),
@@ -811,10 +801,6 @@ input:-webkit-autofill:active,
   opacity: var(--opacity-disabled);
 }
 
-.ui.loading.loading.input > i.icon svg {
-  visibility: hidden;
-}
-
 .text.primary {
   color: var(--color-primary) !important;
 }
diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css
index 795e1f2d61..6a04c44e51 100644
--- a/web_src/css/features/gitgraph.css
+++ b/web_src/css/features/gitgraph.css
@@ -4,12 +4,6 @@
   min-height: 350px;
 }
 
-#git-graph-container > .ui.segment.loading {
-  border: 0;
-  z-index: 1;
-  min-height: 246px;
-}
-
 #git-graph-container h2 {
   display: flex;
   justify-content: space-between;
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 31d840c3e1..0c65f04ab8 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -208,7 +208,7 @@ export function initAdminCommon() {
     $('#delete-selection').on('click', async function (e) {
       e.preventDefault();
       const $this = $(this);
-      $this.addClass('loading disabled');
+      $this.addClass('is-loading disabled');
       const data = new FormData();
       $checkboxes.each(function () {
         if ($(this).checkbox('is checked')) {
diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js
index b7ca5a0fcf..d74b59fd2a 100644
--- a/web_src/js/features/comp/WebHookEditor.js
+++ b/web_src/js/features/comp/WebHookEditor.js
@@ -35,7 +35,7 @@ export function initCompWebHookEditor() {
 
   // Test delivery
   document.getElementById('test-delivery')?.addEventListener('click', async function () {
-    this.classList.add('loading', 'disabled');
+    this.classList.add('is-loading', 'disabled');
     await POST(this.getAttribute('data-link'));
     setTimeout(() => {
       window.location.href = this.getAttribute('data-redirect');
diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js
index 669b47a9c5..2c5746c738 100644
--- a/web_src/js/features/repo-common.js
+++ b/web_src/js/features/repo-common.js
@@ -3,18 +3,20 @@ import {hideElem, showElem} from '../utils/dom.js';
 import {POST} from '../modules/fetch.js';
 
 async function getArchive($target, url, first) {
+  const dropdownBtn = $target[0].closest('.ui.dropdown.button');
+
   try {
+    dropdownBtn.classList.add('is-loading');
     const response = await POST(url);
     if (response.status === 200) {
       const data = await response.json();
       if (!data) {
         // XXX Shouldn't happen?
-        $target.closest('.dropdown').children('i').removeClass('loading');
+        dropdownBtn.classList.remove('is-loading');
         return;
       }
 
       if (!data.complete) {
-        $target.closest('.dropdown').children('i').addClass('loading');
         // Wait for only three quarters of a second initially, in case it's
         // quickly archived.
         setTimeout(() => {
@@ -22,12 +24,12 @@ async function getArchive($target, url, first) {
         }, first ? 750 : 2000);
       } else {
         // We don't need to continue checking.
-        $target.closest('.dropdown').children('i').removeClass('loading');
+        dropdownBtn.classList.remove('is-loading');
         window.location.href = url;
       }
     }
   } catch {
-    $target.closest('.dropdown').children('i').removeClass('loading');
+    dropdownBtn.classList.remove('is-loading');
   }
 }
 
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index ad2956a600..c91dd06ac9 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -43,14 +43,14 @@ export function initRepoIssueTimeTracking() {
 
 async function updateDeadline(deadlineString) {
   hideElem($('#deadline-err-invalid-date'));
-  $('#deadline-loader').addClass('loading');
+  $('#deadline-loader').addClass('is-loading');
 
   let realDeadline = null;
   if (deadlineString !== '') {
     const newDate = Date.parse(deadlineString);
 
     if (Number.isNaN(newDate)) {
-      $('#deadline-loader').removeClass('loading');
+      $('#deadline-loader').removeClass('is-loading');
       showElem($('#deadline-err-invalid-date'));
       return false;
     }
@@ -69,7 +69,7 @@ async function updateDeadline(deadlineString) {
     }
   } catch (error) {
     console.error(error);
-    $('#deadline-loader').removeClass('loading');
+    $('#deadline-loader').removeClass('is-loading');
     showElem($('#deadline-err-invalid-date'));
   }
 }
@@ -237,14 +237,14 @@ export function initRepoPullRequestUpdate() {
     e.preventDefault();
     const $this = $(this);
     const redirect = $this.data('redirect');
-    $this.addClass('loading');
+    $this.addClass('is-loading');
     let response;
     try {
       response = await POST($this.data('do'));
     } catch (error) {
       console.error(error);
     } finally {
-      $this.removeClass('loading');
+      $this.removeClass('is-loading');
     }
     let data;
     try {

From bfa160fc98a23923b6ce1cd4d99e8970d937d6ec Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 22 Mar 2024 01:04:03 +0800
Subject: [PATCH 466/679] Refactor repo header/list (#29969)

1. Use general "mobile-only" and "not-mobile" CSS styles, remove some`@media (max-width: 767.98px)` tricks
2. Use `CountFmt` for repo list, just like the repo header (and it matches GitHub, to avoid big numbers bloat the page)
---
 options/locale/locale_en-US.ini  |  4 +---
 templates/admin/repo/list.tmpl   |  6 +++---
 templates/base/head_navbar.tmpl  | 10 +++++-----
 templates/explore/repo_list.tmpl | 17 ++++++++++++-----
 templates/repo/header.tmpl       | 30 ++++++++++++++++--------------
 web_src/css/base.css             |  7 -------
 web_src/css/helpers.css          | 12 ++++++++++++
 web_src/css/modules/navbar.css   |  7 -------
 web_src/css/repo/header.css      | 23 ++---------------------
 9 files changed, 51 insertions(+), 65 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 08b87134fd..3383bc0bcc 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1063,6 +1063,7 @@ watchers = Watchers
 stargazers = Stargazers
 stars_remove_warning = This will remove all stars from this repository.
 forks = Forks
+stars = Stars
 reactions_more = and %d more
 unit_disabled = The site administrator has disabled this repository section.
 language_other = Other
@@ -2966,9 +2967,6 @@ repos.unadopted.no_more = No more unadopted repositories found
 repos.owner = Owner
 repos.name = Name
 repos.private = Private
-repos.watches = Watches
-repos.stars = Stars
-repos.forks = Forks
 repos.issues = Issues
 repos.size = Size
 repos.lfs_size = LFS Size
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index e977c8307c..4b27d87a45 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -19,13 +19,13 @@
 							{{ctx.Locale.Tr "admin.repos.name"}}
 							{{SortArrow "alphabetically" "reversealphabetically" $.SortType false}}
 						</th>
-						<th>{{ctx.Locale.Tr "admin.repos.watches"}}</th>
+						<th>{{ctx.Locale.Tr "repo.watchers"}}</th>
 						<th  data-sortt-asc="moststars" data-sortt-desc="feweststars">
-							{{ctx.Locale.Tr "admin.repos.stars"}}
+							{{ctx.Locale.Tr "repo.stars"}}
 							{{SortArrow "moststars" "feweststars" $.SortType false}}
 						</th>
 						<th  data-sortt-asc="mostforks" data-sortt-desc="fewestforks">
-							{{ctx.Locale.Tr "admin.repos.forks"}}
+							{{ctx.Locale.Tr "repo.forks"}}
 							{{SortArrow "mostforks" "fewestforks" $.SortType false}}
 						</th>
 						<th>{{ctx.Locale.Tr "admin.repos.issues"}}</th>
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 4f48dc82c3..ffa2ef844f 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -11,7 +11,7 @@
 		</a>
 
 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
-		<div class="ui secondary menu item navbar-mobile-right">
+		<div class="ui secondary menu item navbar-mobile-right only-mobile">
 			{{if .IsSigned}}
 			<a id="mobile-notifications-icon" class="item tw-w-auto gt-p-3" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 				<div class="tw-relative">
@@ -58,7 +58,7 @@
 			<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
 				<span class="text gt-df gt-ac">
 					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
-					<span class="mobile-only gt-ml-3">{{.SignedUser.Name}}</span>
+					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
@@ -80,7 +80,7 @@
 					{{svg "octicon-stopwatch"}}
 					<span class="header-stopwatch-dot"></span>
 				</div>
-				<span class="mobile-only gt-ml-3">{{ctx.Locale.Tr "active_stopwatch"}}</span>
+				<span class="only-mobile gt-ml-3">{{ctx.Locale.Tr "active_stopwatch"}}</span>
 			</a>
 			<div class="active-stopwatch-popup item tippy-target gt-p-3">
 				<div class="gt-df gt-ac">
@@ -122,7 +122,7 @@
 				<span class="text">
 					{{svg "octicon-plus"}}
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
-					<span class="mobile-only">{{ctx.Locale.Tr "create_new"}}</span>
+					<span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span>
 				</span>
 				<div class="menu">
 					<a class="item" href="{{AppSubUrl}}/repo/create">
@@ -144,7 +144,7 @@
 			<div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
 				<span class="text gt-df gt-ac">
 					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
-					<span class="mobile-only gt-ml-3">{{.SignedUser.Name}}</span>
+					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index 7de3df5bee..4c0f93ed2e 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -30,16 +30,23 @@
 							{{end}}
 						</span>
 					</div>
-					<div class="flex-item-trailing">
+					<div class="flex-item-trailing muted-links">
 						{{if .PrimaryLanguage}}
-							<a class="muted" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
-								<span class="flex-text-inline"><i class="color-icon gt-mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span>
+							<a class="flex-text-inline" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
+								<i class="color-icon gt-mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>
+								{{.PrimaryLanguage.Language}}
 							</a>
 						{{end}}
 						{{if not $.DisableStars}}
-							<a class="text grey flex-text-inline" href="{{.Link}}/stars">{{svg "octicon-star" 16}}{{.NumStars}}</a>
+							<a class="flex-text-inline" href="{{.Link}}/stars">
+								<span aria-label="{{ctx.Locale.Tr "repo.stars"}}">{{svg "octicon-star" 16}}</span>
+								<span {{if ge .NumStars 1000}}data-tooltip-content="{{.NumStars}}"{{end}}>{{CountFmt .NumStars}}</span>
+							</a>
 						{{end}}
-						<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
+						<a class="flex-text-inline" href="{{.Link}}/forks">
+							<span aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span>
+							<span {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}>{{CountFmt .NumForks}}</span>
+						</a>
 					</div>
 				</div>
 				{{$description := .DescriptionHTML $.Context}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 6e0a9985f7..e4d39839fc 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -3,29 +3,31 @@
 	<div class="ui container">
 		<div class="repo-header">
 			<div class="flex-item gt-ac">
-				<div class="flex-item-leading">{{template "repo/icon" .}}</div>
+				<div class="flex-item-leading">
+					{{template "repo/icon" .}}
+				</div>
 				<div class="flex-item-main">
 					<div class="flex-item-title gt-font-18">
-						<a class="muted gt-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/
-						<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a></div>
+						<a class="muted gt-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a>
+					</div>
 				</div>
 				<div class="flex-item-trailing">
 					{{if .IsArchived}}
-						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
-						<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.archived"}}">{{svg "octicon-archive" 18}}</div>
+						<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
+						<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.archived"}}">{{svg "octicon-archive" 18}}</div>
 					{{end}}
 					{{if .IsPrivate}}
-						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
-						<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.private"}}">{{svg "octicon-lock" 18}}</div>
+						<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.private"}}</span>
+						<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.private"}}">{{svg "octicon-lock" 18}}</div>
 					{{else}}
 						{{if .Owner.Visibility.IsPrivate}}
-							<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.internal"}}</span>
-							<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div>
+							<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.internal"}}</span>
+							<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div>
 						{{end}}
 					{{end}}
 					{{if .IsTemplate}}
-						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.template"}}</span>
-						<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div>
+						<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.template"}}</span>
+						<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div>
 					{{end}}
 					{{if eq .ObjectFormatName "sha256"}}
 						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.sha256"}}</span>
@@ -76,7 +78,7 @@
 							<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
 								{{if not $.CanSignedUserFork}}
 									{{if gt (len $.UserAndOrgForks) 1}}
-										data-modal="#fork-repo-modal"
+										href="#" data-modal="#fork-repo-modal"
 									{{else if eq (len $.UserAndOrgForks) 1}}
 										href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
 									{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
@@ -84,10 +86,10 @@
 								{{else if not $.UserAndOrgForks}}
 									href="{{$.RepoLink}}/fork"
 								{{else}}
-									data-modal="#fork-repo-modal"
+									href="#" data-modal="#fork-repo-modal"
 								{{end}}
 							>
-								{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
+								{{svg "octicon-repo-forked"}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.fork"}}</span>
 							</a>
 							<div class="ui small modal" id="fork-repo-modal">
 								<div class="header">
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 6280652086..50f8cc8059 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1252,13 +1252,6 @@ strong.attention-caution, svg.attention-caution {
   text-align: center;
 }
 
-@media (max-width: 767.98px) {
-  /* double selector so it wins over .gt-df etc */
-  .not-mobile.not-mobile {
-    display: none !important;
-  }
-}
-
 overflow-menu {
   margin-bottom: 15px !important;
   border-bottom: 1px solid var(--color-secondary) !important;
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 6fc84d743c..a3817be223 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -199,3 +199,15 @@ only use:
 * showElem/hideElem/toggleElem functions in "utils/dom.js"
 */
 .gt-hidden.gt-hidden { display: none !important; }
+
+@media (max-width: 767.98px) {
+  /* double selector so it wins over .tw-flex (old .gt-df) etc */
+  .not-mobile.not-mobile {
+    display: none !important;
+  }
+}
+@media (min-width: 767.98px) {
+  .only-mobile.only-mobile {
+    display: none !important;
+  }
+}
diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index b6fd2ff20a..88f4c57043 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -108,13 +108,6 @@
   }
 }
 
-@media (min-width: 767.98px) {
-  #navbar .navbar-mobile-right,
-  #navbar .mobile-only {
-    display: none;
-  }
-}
-
 #navbar a.item .notification_count {
   color: var(--color-nav-bg);
   padding: 0 3.75px;
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index e998bb9a73..13fb40e35d 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -1,4 +1,4 @@
-.fork-flag {
+.header-wrapper .fork-flag {
   margin-top: 0.5rem;
   font-size: 12px;
 }
@@ -15,11 +15,8 @@
   padding: 0;
 }
 
-.repo-header .btn.interact-bg:hover {
-  text-decoration: none;
-}
-
 .repo-header .flex-item-main {
+  flex: 0;
   flex-basis: unset;
 }
 
@@ -27,10 +24,6 @@
   flex-wrap: nowrap;
 }
 
-.repo-header .flex-item-trailing .repo-icon {
-  display: none;
-}
-
 .repo-buttons {
   align-items: center;
   display: flex;
@@ -75,15 +68,3 @@
   padding-top: 12px;
   background-color: var(--color-header-wrapper);
 }
-
-@media (max-width: 767.98px) {
-  .repo-header .flex-item {
-    flex-grow: 1;
-  }
-  .repo-header .flex-item-trailing .label {
-    display: none;
-  }
-  .repo-header .flex-item-trailing .repo-icon {
-    display: initial;
-  }
-}

From ca4107dc96e5110dec6a8732e7caa3b071222dcf Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 22 Mar 2024 04:32:40 +0800
Subject: [PATCH 467/679] Refactor external URL detection (#29973)

Follow #29960, `IsExternalURL` is not needed anymore.
Add some tests for `RedirectToCurrentSite`
---
 modules/httplib/url.go           |  8 ++++---
 modules/httplib/url_test.go      |  5 ++++
 routers/utils/utils.go           | 16 -------------
 routers/utils/utils_test.go      | 39 --------------------------------
 routers/web/auth/auth.go         |  4 ++--
 routers/web/auth/password.go     |  3 +--
 services/context/context_test.go | 27 ++++++++++++++++++++++
 7 files changed, 40 insertions(+), 62 deletions(-)

diff --git a/modules/httplib/url.go b/modules/httplib/url.go
index b679b44500..903799cb68 100644
--- a/modules/httplib/url.go
+++ b/modules/httplib/url.go
@@ -32,9 +32,11 @@ func IsCurrentGiteaSiteURL(s string) bool {
 		return false
 	}
 	if u.Path != "" {
-		u.Path = "/" + util.PathJoinRelX(u.Path)
-		if !strings.HasSuffix(u.Path, "/") {
-			u.Path += "/"
+		cleanedPath := util.PathJoinRelX(u.Path)
+		if cleanedPath == "" || cleanedPath == "." {
+			u.Path = "/"
+		} else {
+			u.Path += "/" + cleanedPath + "/"
 		}
 	}
 	if urlIsRelative(s, u) {
diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go
index 9b7b242298..9bf09bcf2f 100644
--- a/modules/httplib/url_test.go
+++ b/modules/httplib/url_test.go
@@ -53,6 +53,8 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
 		assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s)
 	}
 	bad := []string{
+		".",
+		"foo",
 		"/",
 		"//",
 		"\\\\",
@@ -67,5 +69,8 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
 
 	setting.AppURL = "http://localhost:3000/"
 	setting.AppSubURL = ""
+	assert.False(t, IsCurrentGiteaSiteURL("//"))
+	assert.False(t, IsCurrentGiteaSiteURL("\\\\"))
+	assert.False(t, IsCurrentGiteaSiteURL("http://localhost"))
 	assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val"))
 }
diff --git a/routers/utils/utils.go b/routers/utils/utils.go
index 1f4d11fd3c..3035073d5c 100644
--- a/routers/utils/utils.go
+++ b/routers/utils/utils.go
@@ -5,26 +5,10 @@ package utils
 
 import (
 	"html"
-	"net/url"
 	"strings"
-
-	"code.gitea.io/gitea/modules/setting"
 )
 
 // SanitizeFlashErrorString will sanitize a flash error string
 func SanitizeFlashErrorString(x string) string {
 	return strings.ReplaceAll(html.EscapeString(x), "\n", "<br>")
 }
-
-// IsExternalURL checks if rawURL points to an external URL like http://example.com
-func IsExternalURL(rawURL string) bool {
-	parsed, err := url.Parse(rawURL)
-	if err != nil {
-		return true
-	}
-	appURL, _ := url.Parse(setting.AppURL)
-	if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(appURL.Host, "www.", "", 1) {
-		return true
-	}
-	return false
-}
diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go
index 440aad87c6..6e7f3c33cd 100644
--- a/routers/utils/utils_test.go
+++ b/routers/utils/utils_test.go
@@ -5,47 +5,8 @@ package utils
 
 import (
 	"testing"
-
-	"code.gitea.io/gitea/modules/setting"
-
-	"github.com/stretchr/testify/assert"
 )
 
-func TestIsExternalURL(t *testing.T) {
-	setting.AppURL = "https://try.gitea.io/"
-	type test struct {
-		Expected bool
-		RawURL   string
-	}
-	newTest := func(expected bool, rawURL string) test {
-		return test{Expected: expected, RawURL: rawURL}
-	}
-	for _, test := range []test{
-		newTest(false,
-			"https://try.gitea.io"),
-		newTest(true,
-			"https://example.com/"),
-		newTest(true,
-			"//example.com"),
-		newTest(true,
-			"http://example.com"),
-		newTest(false,
-			"a/"),
-		newTest(false,
-			"https://try.gitea.io/test?param=false"),
-		newTest(false,
-			"test?param=false"),
-		newTest(false,
-			"//try.gitea.io/test?param=false"),
-		newTest(false,
-			"/hey/hey/hey#3244"),
-		newTest(true,
-			"://missing protocol scheme"),
-	} {
-		assert.Equal(t, test.Expected, IsExternalURL(test.RawURL))
-	}
-}
-
 func TestSanitizeFlashErrorString(t *testing.T) {
 	tests := []struct {
 		name string
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index ab81740e3f..8b5cd986b8 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -17,6 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/eventsource"
+	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/session"
@@ -25,7 +26,6 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/routers/utils"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/context"
@@ -368,7 +368,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
 		return setting.AppSubURL + "/"
 	}
 
-	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+	if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(redirectTo) {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
 		if obeyRedirect {
 			ctx.RedirectToCurrentSite(redirectTo)
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 3af8b7edf2..f6b76c1ffd 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -18,7 +18,6 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/routers/utils"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -312,7 +311,7 @@ func MustChangePasswordPost(ctx *context.Context) {
 
 	log.Trace("User updated password: %s", ctx.Doer.Name)
 
-	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+	if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
 		ctx.RedirectToCurrentSite(redirectTo)
 		return
diff --git a/services/context/context_test.go b/services/context/context_test.go
index 033ce2ef0a..984593398d 100644
--- a/services/context/context_test.go
+++ b/services/context/context_test.go
@@ -6,9 +6,11 @@ package context
 import (
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"testing"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -22,3 +24,28 @@ func TestRemoveSessionCookieHeader(t *testing.T) {
 	assert.Len(t, w.Header().Values("Set-Cookie"), 1)
 	assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
 }
+
+func TestRedirectToCurrentSite(t *testing.T) {
+	defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
+	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+	cases := []struct {
+		location string
+		want     string
+	}{
+		{"/", "/sub/"},
+		{"http://localhost:3000/sub?k=v", "http://localhost:3000/sub?k=v"},
+		{"http://other", "/sub/"},
+	}
+	for _, c := range cases {
+		t.Run(c.location, func(t *testing.T) {
+			req := &http.Request{URL: &url.URL{Path: "/"}}
+			resp := httptest.NewRecorder()
+			base, baseCleanUp := NewBaseContext(resp, req)
+			defer baseCleanUp()
+			ctx := NewWebContext(base, nil, nil)
+			ctx.RedirectToCurrentSite(c.location)
+			redirect := test.RedirectURL(resp)
+			assert.Equal(t, c.want, redirect)
+		})
+	}
+}

From ef33dcf946cc9754b51c955975d67f871702b958 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 22 Mar 2024 00:58:14 +0100
Subject: [PATCH 468/679] remove PATH and GOPATH modification in Makefile
 (#29978)

We don't need these modifications anymore since all tool
dependencies run via `go run`.
---
 Makefile | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/Makefile b/Makefile
index 1cddad1e93..236f115a2f 100644
--- a/Makefile
+++ b/Makefile
@@ -42,9 +42,6 @@ DOCKER_TAG ?= latest
 DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
 
 ifeq ($(HAS_GO), yes)
-	GOPATH ?= $(shell $(GO) env GOPATH)
-	export PATH := $(GOPATH)/bin:$(PATH)
-
 	CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
 	CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
 endif

From 2f060c5834d81f0317c795fc281f9a07e03e5962 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Fri, 22 Mar 2024 11:19:17 +0800
Subject: [PATCH 469/679] Fix bugs in rerunning jobs (#29955)

Fix #28761
Fix #27884
Fix #28093

## Changes

### Rerun all jobs
When rerun all jobs, status of the jobs with `needs` will be set to
`blocked` instead of `waiting`. Therefore, these jobs will not run until
the required jobs are completed.

### Rerun a single job
When a single job is rerun, its dependents should also be rerun, just
like GitHub does
(https://github.com/go-gitea/gitea/issues/28761#issuecomment-2008620820).
In this case, only the specified job will be set to `waiting`, its
dependents will be set to `blocked` to wait the job.

### Show warning if every job has `needs`
If every job in a workflow has `needs`, all jobs will be blocked and no
job can be run. So I add a warning message.

<img
src="https://github.com/go-gitea/gitea/assets/15528715/88f43511-2360-465d-be96-ee92b57ff67b"
width="480px" />
---
 options/locale/locale_en-US.ini     |  1 +
 routers/web/repo/actions/actions.go | 10 +++++-
 routers/web/repo/actions/view.go    | 26 +++++++++++++---
 services/actions/rerun.go           | 38 +++++++++++++++++++++++
 services/actions/rerun_test.go      | 48 +++++++++++++++++++++++++++++
 5 files changed, 117 insertions(+), 6 deletions(-)
 create mode 100644 services/actions/rerun.go
 create mode 100644 services/actions/rerun_test.go

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 3383bc0bcc..4c52c4eeed 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3626,6 +3626,7 @@ runs.scheduled = Scheduled
 runs.pushed_by = pushed by
 runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
 runs.no_matching_online_runner_helper = No matching online runner with label: %s
+runs.no_job_without_needs = The workflow must contain at least one job without dependencies.
 runs.actor = Actor
 runs.status = Status
 runs.actors_no_select = All actors
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index f27329aa0f..6059ad1414 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -104,8 +104,13 @@ func List(ctx *context.Context) {
 				workflows = append(workflows, workflow)
 				continue
 			}
-			// Check whether have matching runner
+			// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
+			hasJobWithoutNeeds := false
+			// Check whether have matching runner and a job without "needs"
 			for _, j := range wf.Jobs {
+				if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
+					hasJobWithoutNeeds = true
+				}
 				runsOnList := j.RunsOn()
 				for _, ro := range runsOnList {
 					if strings.Contains(ro, "${{") {
@@ -123,6 +128,9 @@ func List(ctx *context.Context) {
 					break
 				}
 			}
+			if !hasJobWithoutNeeds {
+				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
+			}
 			workflows = append(workflows, workflow)
 		}
 	}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 3f8030e40d..41989589be 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -303,12 +303,25 @@ func Rerun(ctx *context_module.Context) {
 		return
 	}
 
-	if jobIndexStr != "" {
-		jobs = []*actions_model.ActionRunJob{job}
+	if jobIndexStr == "" { // rerun all jobs
+		for _, j := range jobs {
+			// if the job has needs, it should be set to "blocked" status to wait for other jobs
+			shouldBlock := len(j.Needs) > 0
+			if err := rerunJob(ctx, j, shouldBlock); err != nil {
+				ctx.Error(http.StatusInternalServerError, err.Error())
+				return
+			}
+		}
+		ctx.JSON(http.StatusOK, struct{}{})
+		return
 	}
 
-	for _, j := range jobs {
-		if err := rerunJob(ctx, j); err != nil {
+	rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
+
+	for _, j := range rerunJobs {
+		// jobs other than the specified one should be set to "blocked" status
+		shouldBlock := j.JobID != job.JobID
+		if err := rerunJob(ctx, j, shouldBlock); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
 			return
 		}
@@ -317,7 +330,7 @@ func Rerun(ctx *context_module.Context) {
 	ctx.JSON(http.StatusOK, struct{}{})
 }
 
-func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
+func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
 	status := job.Status
 	if !status.IsDone() {
 		return nil
@@ -325,6 +338,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro
 
 	job.TaskID = 0
 	job.Status = actions_model.StatusWaiting
+	if shouldBlock {
+		job.Status = actions_model.StatusBlocked
+	}
 	job.Started = 0
 	job.Stopped = 0
 
diff --git a/services/actions/rerun.go b/services/actions/rerun.go
new file mode 100644
index 0000000000..60f6650905
--- /dev/null
+++ b/services/actions/rerun.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/modules/container"
+)
+
+// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
+func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
+	rerunJobs := []*actions_model.ActionRunJob{job}
+	rerunJobsIDSet := make(container.Set[string])
+	rerunJobsIDSet.Add(job.JobID)
+
+	for {
+		found := false
+		for _, j := range allJobs {
+			if rerunJobsIDSet.Contains(j.JobID) {
+				continue
+			}
+			for _, need := range j.Needs {
+				if rerunJobsIDSet.Contains(need) {
+					found = true
+					rerunJobs = append(rerunJobs, j)
+					rerunJobsIDSet.Add(j.JobID)
+					break
+				}
+			}
+		}
+		if !found {
+			break
+		}
+	}
+
+	return rerunJobs
+}
diff --git a/services/actions/rerun_test.go b/services/actions/rerun_test.go
new file mode 100644
index 0000000000..a98de7b788
--- /dev/null
+++ b/services/actions/rerun_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"testing"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetAllRerunJobs(t *testing.T) {
+	job1 := &actions_model.ActionRunJob{JobID: "job1"}
+	job2 := &actions_model.ActionRunJob{JobID: "job2", Needs: []string{"job1"}}
+	job3 := &actions_model.ActionRunJob{JobID: "job3", Needs: []string{"job2"}}
+	job4 := &actions_model.ActionRunJob{JobID: "job4", Needs: []string{"job2", "job3"}}
+
+	jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
+
+	testCases := []struct {
+		job       *actions_model.ActionRunJob
+		rerunJobs []*actions_model.ActionRunJob
+	}{
+		{
+			job1,
+			[]*actions_model.ActionRunJob{job1, job2, job3, job4},
+		},
+		{
+			job2,
+			[]*actions_model.ActionRunJob{job2, job3, job4},
+		},
+		{
+			job3,
+			[]*actions_model.ActionRunJob{job3, job4},
+		},
+		{
+			job4,
+			[]*actions_model.ActionRunJob{job4},
+		},
+	}
+
+	for _, tc := range testCases {
+		rerunJobs := GetAllRerunJobs(tc.job, jobs)
+		assert.ElementsMatch(t, tc.rerunJobs, rerunJobs)
+	}
+}

From 226231ea27d4f2b0f09fa4efb39501507613b284 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 22 Mar 2024 19:17:30 +0800
Subject: [PATCH 470/679] Fix some pending problems (#29985)

These changes are quite independent and trivial, so I don't want to open
too many PRs.

* https://github.com/go-gitea/gitea/pull/29882#discussion_r1529607091
    * the `f.Close` should be called properly
* the error message could be more meaningful
(https://github.com/go-gitea/gitea/pull/29882#pullrequestreview-1942557935)
*
https://github.com/go-gitea/gitea/pull/29859#pullrequestreview-1942324716
    * the new translation strings don't take arguments
* https://github.com/go-gitea/gitea/pull/28710#discussion_r1443778807
    * stale for long time
*  #28140
    * a form was forgotten to be changed to work with backend code
---
 models/asymkey/ssh_key_authorized_keys.go     |  9 ++++-----
 modules/actions/log.go                        |  2 +-
 modules/git/commit.go                         |  5 ++---
 modules/git/repo_stats.go                     |  6 +++---
 modules/markup/csv/csv.go                     |  5 ++---
 routers/web/repo/compare.go                   |  5 ++---
 routers/web/repo/editor.go                    |  4 ++--
 .../asymkey/ssh_key_authorized_principals.go  |  9 ++++-----
 services/doctor/authorizedkeys.go             |  6 +++---
 templates/repo/issue/view_content/pull.tmpl   | 20 +++++++++----------
 templates/status/404.tmpl                     | 12 ++++-------
 .../js/components/PullRequestMergeForm.vue    |  1 +
 12 files changed, 37 insertions(+), 47 deletions(-)

diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go
index 7621994866..2e4cd62e5c 100644
--- a/models/asymkey/ssh_key_authorized_keys.go
+++ b/models/asymkey/ssh_key_authorized_keys.go
@@ -139,6 +139,8 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
 		if err != nil {
 			return err
 		}
+		defer f.Close()
+
 		scanner := bufio.NewScanner(f)
 		for scanner.Scan() {
 			line := scanner.Text()
@@ -148,15 +150,12 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
 			}
 			_, err = t.WriteString(line + "\n")
 			if err != nil {
-				f.Close()
 				return err
 			}
 		}
-		err = scanner.Err()
-		if err != nil {
-			return fmt.Errorf("scan: %w", err)
+		if err = scanner.Err(); err != nil {
+			return fmt.Errorf("RegeneratePublicKeys scan: %w", err)
 		}
-		f.Close()
 	}
 	return nil
 }
diff --git a/modules/actions/log.go b/modules/actions/log.go
index cdf18646aa..c38082b5dc 100644
--- a/modules/actions/log.go
+++ b/modules/actions/log.go
@@ -100,7 +100,7 @@ func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limi
 	}
 
 	if err := scanner.Err(); err != nil {
-		return nil, fmt.Errorf("scan: %w", err)
+		return nil, fmt.Errorf("ReadLogs scan: %w", err)
 	}
 
 	return rows, nil
diff --git a/modules/git/commit.go b/modules/git/commit.go
index 789a2e8f69..ef2676762c 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -397,9 +397,8 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) {
 			}
 		}
 	}
-	err = scanner.Err()
-	if err != nil {
-		return nil, fmt.Errorf("scan: %w", err)
+	if err = scanner.Err(); err != nil {
+		return nil, fmt.Errorf("GetSubModules scan: %w", err)
 	}
 
 	return c.submoduleCache, nil
diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go
index ce82946873..83220104bd 100644
--- a/modules/git/repo_stats.go
+++ b/modules/git/repo_stats.go
@@ -124,9 +124,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
 					}
 				}
 			}
-			err = scanner.Err()
-			if err != nil {
-				return fmt.Errorf("scan: %w", err)
+			if err = scanner.Err(); err != nil {
+				_ = stdoutReader.Close()
+				return fmt.Errorf("GetCodeActivityStats scan: %w", err)
 			}
 			a := make([]*CodeActivityAuthor, 0, len(authors))
 			for _, v := range authors {
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 50bb918442..1dd26eb8ac 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -124,9 +124,8 @@ func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
 			return err
 		}
 	}
-	err = scan.Err()
-	if err != nil {
-		return fmt.Errorf("scan: %w", err)
+	if err = scan.Err(); err != nil {
+		return fmt.Errorf("fallbackRender scan: %w", err)
 	}
 
 	_, err = tmpBlock.WriteString("</pre>")
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index bf42b77b66..7b5243e6b7 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -980,9 +980,8 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu
 		}
 		diffLines = append(diffLines, diffLine)
 	}
-	err = scanner.Err()
-	if err != nil {
-		return nil, fmt.Errorf("scan: %w", err)
+	if err = scanner.Err(); err != nil {
+		return nil, fmt.Errorf("getExcerptLines scan: %w", err)
 	}
 	return diffLines, nil
 }
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 29395b4013..082666276a 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -333,9 +333,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 				ctx.Error(http.StatusInternalServerError, err.Error())
 			}
 		} else if models.IsErrCommitIDDoesNotMatch(err) {
-			ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form)
+			ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form)
 		} else if git.IsErrPushOutOfDate(err) {
-			ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
+			ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form)
 		} else if git.IsErrPushRejected(err) {
 			errPushRej := err.(*git.ErrPushRejected)
 			if len(errPushRej.Message) == 0 {
diff --git a/services/asymkey/ssh_key_authorized_principals.go b/services/asymkey/ssh_key_authorized_principals.go
index 822dd0ffe7..2838bb5fc7 100644
--- a/services/asymkey/ssh_key_authorized_principals.go
+++ b/services/asymkey/ssh_key_authorized_principals.go
@@ -109,6 +109,8 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
 		if err != nil {
 			return err
 		}
+		defer f.Close()
+
 		scanner := bufio.NewScanner(f)
 		for scanner.Scan() {
 			line := scanner.Text()
@@ -118,15 +120,12 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
 			}
 			_, err = t.WriteString(line + "\n")
 			if err != nil {
-				f.Close()
 				return err
 			}
 		}
-		err = scanner.Err()
-		if err != nil {
-			return fmt.Errorf("scan: %w", err)
+		if err = scanner.Err(); err != nil {
+			return fmt.Errorf("regeneratePrincipalKeys scan: %w", err)
 		}
-		f.Close()
 	}
 	return nil
 }
diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go
index bc0266c4bc..8d6fc9cb5e 100644
--- a/services/doctor/authorizedkeys.go
+++ b/services/doctor/authorizedkeys.go
@@ -51,11 +51,11 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
 		}
 		linesInAuthorizedKeys.Add(line)
 	}
-	err = scanner.Err()
-	if err != nil {
+	if err = scanner.Err(); err != nil {
 		return fmt.Errorf("scan: %w", err)
 	}
-	f.Close()
+	// although there is a "defer close" above, here close explicitly before the generating, because it needs to open the file for writing again
+	_ = f.Close()
 
 	// now we regenerate and check if there are any lines missing
 	regenerated := &bytes.Buffer{}
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index c8e8038438..aac30180df 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -374,17 +374,15 @@
 			*/}}
 			{{if and $.StillCanManualMerge (not $showGeneralMergeForm)}}
 				<div class="divider"></div>
-				<div class="ui form">
-					<form action="{{.Link}}/merge" method="post">
-						{{.CsrfTokenHtml}}
-						<div class="field">
-							<input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}">
-						</div>
-						<button class="ui red button" type="submit" name="do" value="manually-merged">
-							{{ctx.Locale.Tr "repo.pulls.merge_manually"}}
-						</button>
-					</form>
-				</div>
+				<form class="ui form form-fetch-action" action="{{.Link}}/merge" method="post">{{/* another similar form is in PullRequestMergeForm.vue*/}}
+					{{.CsrfTokenHtml}}
+					<div class="field">
+						<input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}">
+					</div>
+					<button class="ui red button" type="submit" name="do" value="manually-merged">
+						{{ctx.Locale.Tr "repo.pulls.merge_manually"}}
+					</button>
+				</form>
 			{{end}}
 
 			{{if and .Issue.PullRequest.HeadRepo (not .Issue.PullRequest.HasMerged) (not .Issue.IsClosed)}}
diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl
index f1f1199665..78f149e67b 100644
--- a/templates/status/404.tmpl
+++ b/templates/status/404.tmpl
@@ -1,14 +1,10 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content ui container center tw-w-screen {{if .IsRepo}}repository{{end}}">
+<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
 	{{if .IsRepo}}{{template "repo/header" .}}{{end}}
-	<div class="ui container center">
-		<p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/404.png" alt="404"></p>
+	<div class="ui container tw-text-center">
+		<img class="tw-max-w-[80vw] tw-py-16" src="{{AssetUrlPrefix}}/img/404.png" alt="404">
 		<p>{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404"}}{{end}}</p>
-		{{if .NotFoundGoBackURL}}<a class="ui button green" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>{{end}}
-
-		<div class="divider"></div>
-		<br>
-		{{if .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
+		{{if .NotFoundGoBackURL}}<a class="ui button" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>{{end}}
 	</div>
 </div>
 {{template "base/footer" .}}
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index b0b10b6252..170d0d85c6 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -94,6 +94,7 @@ export default {
     <!-- eslint-disable-next-line vue/no-v-html -->
     <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"/>
 
+    <!-- another similar form is in pull.tmpl (manual merge)-->
     <form class="ui form form-fetch-action" v-if="showActionForm" :action="mergeForm.baseLink+'/merge'" method="post">
       <input type="hidden" name="_csrf" :value="csrfToken">
       <input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID">

From bf34723491dcbb45dee7888c574e295cae6096be Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Fri, 22 Mar 2024 19:22:36 +0800
Subject: [PATCH 471/679] Fix: Abnormal strings appear when comments are saved
 after editing (#29991)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Partially resolved(The second problem):
[#29986](https://github.com/go-gitea/gitea/issues/29986)

**Before**
HTML strings appear when comments are saved after editing


![image](https://github.com/go-gitea/gitea/assets/37935145/c356d99a-8473-4cc5-8e38-1b207ccd8b12)


**After**



https://github.com/go-gitea/gitea/assets/37935145/525601f9-3ee1-4266-9105-36d82b91b1c8
---
 web_src/js/features/repo-legacy.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 5afb407223..18978e9e29 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -436,7 +436,7 @@ async function onEditContent(event) {
       const $content = $segment;
       if (!$content.find('.dropzone-attachments').length) {
         if (data.attachments !== '') {
-          $content[0].append(data.attachments);
+          $content[0].insertAdjacentHTML('beforeend', data.attachments);
         }
       } else if (data.attachments === '') {
         $content.find('.dropzone-attachments').remove();

From 6845717158991d41fc52690de857e2a4987f1c5c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 22 Mar 2024 12:47:50 +0100
Subject: [PATCH 472/679] Remove fomantic site module (#29980)

Had to fiddle a bit with the css ordering, but seems to work well now
and should render exactly like before. Some of the CSS may be
unnecessary, but I kept it for now.
---
 web_src/css/base.css                |  88 ++++-
 web_src/fomantic/build/semantic.css | 177 ----------
 web_src/fomantic/build/semantic.js  | 494 ----------------------------
 web_src/fomantic/semantic.json      |   1 -
 4 files changed, 79 insertions(+), 681 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 50f8cc8059..71e61eeb41 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -37,6 +37,23 @@
   border-color: currentcolor;
 }
 
+html, body {
+  height: 100%;
+  font-size: 14px;
+}
+
+body {
+  line-height: 1.4285rem;
+  font-family: var(--fonts-regular);
+  color: var(--color-text);
+  background-color: var(--color-body);
+  tab-size: var(--tab-size);
+  display: flex;
+  flex-direction: column;
+  overflow-x: visible;
+  overflow-wrap: break-word;
+}
+
 textarea {
   font-family: var(--fonts-regular);
 }
@@ -60,13 +77,65 @@ h6 {
   font-weight: var(--font-weight-semibold);
 }
 
-body {
-  color: var(--color-text);
-  background-color: var(--color-body);
-  tab-size: var(--tab-size);
-  display: flex;
-  flex-direction: column;
-  overflow-wrap: break-word;
+h1,
+h2,
+h3,
+h4,
+h5 {
+  line-height: 1.28571429;
+  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
+  font-weight: var(--font-weight-medium);
+  padding: 0;
+}
+
+h1 {
+  min-height: 1rem;
+  font-size: 2rem;
+}
+
+h2 {
+  font-size: 1.71428571rem;
+}
+
+h3 {
+  font-size: 1.28571429rem;
+}
+
+h4 {
+  font-size: 1.07142857rem;
+}
+
+h5 {
+  font-size: 1rem;
+}
+
+h1:first-child,
+h2:first-child,
+h3:first-child,
+h4:first-child,
+h5:first-child {
+  margin-top: 0;
+}
+
+h1:last-child,
+h2:last-child,
+h3:last-child,
+h4:last-child,
+h5:last-child {
+  margin-bottom: 0;
+}
+
+p {
+  margin: 0 0 1em;
+  line-height: 1.4285;
+}
+
+p:first-child {
+  margin-top: 0;
+}
+
+p:last-child {
+  margin-bottom: 0;
 }
 
 table {
@@ -121,8 +190,8 @@ progress::-moz-progress-bar {
 }
 
 ::selection {
-  background: var(--color-primary-light-1) !important;
-  color: var(--color-white) !important;
+  background: var(--color-primary-light-1);
+  color: var(--color-white);
 }
 
 ::placeholder,
@@ -146,6 +215,7 @@ progress::-moz-progress-bar {
 a {
   color: var(--color-primary);
   cursor: pointer;
+  text-decoration: none;
   text-decoration-skip-ink: all;
 }
 
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 538dfefdc1..099bb94c39 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -14952,183 +14952,6 @@ Floated Menu / Item
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Site
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-             Page
-*******************************/
-
-html,
-body {
-  height: 100%;
-}
-
-html {
-  font-size: 14px;
-}
-
-body {
-  margin: 0;
-  padding: 0;
-  overflow-x: visible;
-  min-width: 320px;
-  background: #FFFFFF;
-  font-family: var(--fonts-regular);
-  font-size: 14px;
-  line-height: 1.4285em;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*******************************
-             Headers
-*******************************/
-
-h1,
-h2,
-h3,
-h4,
-h5 {
-  font-family: var(--fonts-regular);
-  line-height: 1.28571429em;
-  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
-  font-weight: 500;
-  padding: 0;
-}
-
-h1 {
-  min-height: 1rem;
-  font-size: 2rem;
-}
-
-h2 {
-  font-size: 1.71428571rem;
-}
-
-h3 {
-  font-size: 1.28571429rem;
-}
-
-h4 {
-  font-size: 1.07142857rem;
-}
-
-h5 {
-  font-size: 1rem;
-}
-
-h1:first-child,
-h2:first-child,
-h3:first-child,
-h4:first-child,
-h5:first-child {
-  margin-top: 0;
-}
-
-h1:last-child,
-h2:last-child,
-h3:last-child,
-h4:last-child,
-h5:last-child {
-  margin-bottom: 0;
-}
-
-/*******************************
-             Text
-*******************************/
-
-p {
-  margin: 0 0 1em;
-  line-height: 1.4285em;
-}
-
-p:first-child {
-  margin-top: 0;
-}
-
-p:last-child {
-  margin-bottom: 0;
-}
-
-/*-------------------
-        Links
---------------------*/
-
-a {
-  color: #4183C4;
-  text-decoration: none;
-}
-
-a:hover {
-  color: #1e70bf;
-  text-decoration: underline;
-}
-
-/*******************************
-         Scrollbars
-*******************************/
-
-/*******************************
-          Highlighting
-*******************************/
-
-/* Site */
-
-::-webkit-selection {
-  background-color: #CCE2FF;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-::-moz-selection {
-  background-color: #CCE2FF;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-::selection {
-  background-color: #CCE2FF;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/* Form */
-
-textarea::-webkit-selection,
-input::-webkit-selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-textarea::-moz-selection,
-input::-moz-selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-textarea::-moz-selection,
-input::-moz-selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-textarea::selection,
-input::selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*******************************
-        Global Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js
index 2a05d94d72..1199e9c82f 100644
--- a/web_src/fomantic/build/semantic.js
+++ b/web_src/fomantic/build/semantic.js
@@ -11864,500 +11864,6 @@ $.fn.search.settings = {
   }
 };
 
-})( jQuery, window, document );
-
-/*!
- * # Fomantic-UI - Site
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-$.isFunction = $.isFunction || function(obj) {
-    return typeof obj === "function" && typeof obj.nodeType !== "number";
-};
-
-$.site = $.fn.site = function(parameters) {
-  var
-    time           = new Date().getTime(),
-    performance    = [],
-
-    query          = arguments[0],
-    methodInvoked  = (typeof query == 'string'),
-    queryArguments = [].slice.call(arguments, 1),
-
-    settings        = ( $.isPlainObject(parameters) )
-      ? $.extend(true, {}, $.site.settings, parameters)
-      : $.extend({}, $.site.settings),
-
-    namespace       = settings.namespace,
-    error           = settings.error,
-
-    moduleNamespace = 'module-' + namespace,
-
-    $document       = $(document),
-    $module         = $document,
-    element         = this,
-    instance        = $module.data(moduleNamespace),
-
-    module,
-    returnedValue
-  ;
-  module = {
-
-    initialize: function() {
-      module.instantiate();
-    },
-
-    instantiate: function() {
-      module.verbose('Storing instance of site', module);
-      instance = module;
-      $module
-        .data(moduleNamespace, module)
-      ;
-    },
-
-    normalize: function() {
-      module.fix.console();
-      module.fix.requestAnimationFrame();
-    },
-
-    fix: {
-      console: function() {
-        module.debug('Normalizing window.console');
-        if (console === undefined || console.log === undefined) {
-          module.verbose('Console not available, normalizing events');
-          module.disable.console();
-        }
-        if (typeof console.group == 'undefined' || typeof console.groupEnd == 'undefined' || typeof console.groupCollapsed == 'undefined') {
-          module.verbose('Console group not available, normalizing events');
-          window.console.group = function() {};
-          window.console.groupEnd = function() {};
-          window.console.groupCollapsed = function() {};
-        }
-        if (typeof console.markTimeline == 'undefined') {
-          module.verbose('Mark timeline not available, normalizing events');
-          window.console.markTimeline = function() {};
-        }
-      },
-      consoleClear: function() {
-        module.debug('Disabling programmatic console clearing');
-        window.console.clear = function() {};
-      },
-      requestAnimationFrame: function() {
-        module.debug('Normalizing requestAnimationFrame');
-        if(window.requestAnimationFrame === undefined) {
-          module.debug('RequestAnimationFrame not available, normalizing event');
-          window.requestAnimationFrame = window.requestAnimationFrame
-            || window.mozRequestAnimationFrame
-            || window.webkitRequestAnimationFrame
-            || window.msRequestAnimationFrame
-            || function(callback) { setTimeout(callback, 0); }
-          ;
-        }
-      }
-    },
-
-    moduleExists: function(name) {
-      return ($.fn[name] !== undefined && $.fn[name].settings !== undefined);
-    },
-
-    enabled: {
-      modules: function(modules) {
-        var
-          enabledModules = []
-        ;
-        modules = modules || settings.modules;
-        $.each(modules, function(index, name) {
-          if(module.moduleExists(name)) {
-            enabledModules.push(name);
-          }
-        });
-        return enabledModules;
-      }
-    },
-
-    disabled: {
-      modules: function(modules) {
-        var
-          disabledModules = []
-        ;
-        modules = modules || settings.modules;
-        $.each(modules, function(index, name) {
-          if(!module.moduleExists(name)) {
-            disabledModules.push(name);
-          }
-        });
-        return disabledModules;
-      }
-    },
-
-    change: {
-      setting: function(setting, value, modules, modifyExisting) {
-        modules = (typeof modules === 'string')
-          ? (modules === 'all')
-            ? settings.modules
-            : [modules]
-          : modules || settings.modules
-        ;
-        modifyExisting = (modifyExisting !== undefined)
-          ? modifyExisting
-          : true
-        ;
-        $.each(modules, function(index, name) {
-          var
-            namespace = (module.moduleExists(name))
-              ? $.fn[name].settings.namespace || false
-              : true,
-            $existingModules
-          ;
-          if(module.moduleExists(name)) {
-            module.verbose('Changing default setting', setting, value, name);
-            $.fn[name].settings[setting] = value;
-            if(modifyExisting && namespace) {
-              $existingModules = $(':data(module-' + namespace + ')');
-              if($existingModules.length > 0) {
-                module.verbose('Modifying existing settings', $existingModules);
-                $existingModules[name]('setting', setting, value);
-              }
-            }
-          }
-        });
-      },
-      settings: function(newSettings, modules, modifyExisting) {
-        modules = (typeof modules === 'string')
-          ? [modules]
-          : modules || settings.modules
-        ;
-        modifyExisting = (modifyExisting !== undefined)
-          ? modifyExisting
-          : true
-        ;
-        $.each(modules, function(index, name) {
-          var
-            $existingModules
-          ;
-          if(module.moduleExists(name)) {
-            module.verbose('Changing default setting', newSettings, name);
-            $.extend(true, $.fn[name].settings, newSettings);
-            if(modifyExisting && namespace) {
-              $existingModules = $(':data(module-' + namespace + ')');
-              if($existingModules.length > 0) {
-                module.verbose('Modifying existing settings', $existingModules);
-                $existingModules[name]('setting', newSettings);
-              }
-            }
-          }
-        });
-      }
-    },
-
-    enable: {
-      console: function() {
-        module.console(true);
-      },
-      debug: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Enabling debug for modules', modules);
-        module.change.setting('debug', true, modules, modifyExisting);
-      },
-      verbose: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Enabling verbose debug for modules', modules);
-        module.change.setting('verbose', true, modules, modifyExisting);
-      }
-    },
-    disable: {
-      console: function() {
-        module.console(false);
-      },
-      debug: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Disabling debug for modules', modules);
-        module.change.setting('debug', false, modules, modifyExisting);
-      },
-      verbose: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Disabling verbose debug for modules', modules);
-        module.change.setting('verbose', false, modules, modifyExisting);
-      }
-    },
-
-    console: function(enable) {
-      if(enable) {
-        if(instance.cache.console === undefined) {
-          module.error(error.console);
-          return;
-        }
-        module.debug('Restoring console function');
-        window.console = instance.cache.console;
-      }
-      else {
-        module.debug('Disabling console function');
-        instance.cache.console = window.console;
-        window.console = {
-          clear          : function(){},
-          error          : function(){},
-          group          : function(){},
-          groupCollapsed : function(){},
-          groupEnd       : function(){},
-          info           : function(){},
-          log            : function(){},
-          markTimeline   : function(){},
-          warn           : function(){}
-        };
-      }
-    },
-
-    destroy: function() {
-      module.verbose('Destroying previous site for', $module);
-      $module
-        .removeData(moduleNamespace)
-      ;
-    },
-
-    cache: {},
-
-    setting: function(name, value) {
-      if( $.isPlainObject(name) ) {
-        $.extend(true, settings, name);
-      }
-      else if(value !== undefined) {
-        settings[name] = value;
-      }
-      else {
-        return settings[name];
-      }
-    },
-    internal: function(name, value) {
-      if( $.isPlainObject(name) ) {
-        $.extend(true, module, name);
-      }
-      else if(value !== undefined) {
-        module[name] = value;
-      }
-      else {
-        return module[name];
-      }
-    },
-    debug: function() {
-      if(settings.debug) {
-        if(settings.performance) {
-          module.performance.log(arguments);
-        }
-        else {
-          module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
-          module.debug.apply(console, arguments);
-        }
-      }
-    },
-    verbose: function() {
-      if(settings.verbose && settings.debug) {
-        if(settings.performance) {
-          module.performance.log(arguments);
-        }
-        else {
-          module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
-          module.verbose.apply(console, arguments);
-        }
-      }
-    },
-    error: function() {
-      module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
-      module.error.apply(console, arguments);
-    },
-    performance: {
-      log: function(message) {
-        var
-          currentTime,
-          executionTime,
-          previousTime
-        ;
-        if(settings.performance) {
-          currentTime   = new Date().getTime();
-          previousTime  = time || currentTime;
-          executionTime = currentTime - previousTime;
-          time          = currentTime;
-          performance.push({
-            'Element'        : element,
-            'Name'           : message[0],
-            'Arguments'      : [].slice.call(message, 1) || '',
-            'Execution Time' : executionTime
-          });
-        }
-        clearTimeout(module.performance.timer);
-        module.performance.timer = setTimeout(module.performance.display, 500);
-      },
-      display: function() {
-        var
-          title = settings.name + ':',
-          totalTime = 0
-        ;
-        time = false;
-        clearTimeout(module.performance.timer);
-        $.each(performance, function(index, data) {
-          totalTime += data['Execution Time'];
-        });
-        title += ' ' + totalTime + 'ms';
-        if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
-          console.groupCollapsed(title);
-          if(console.table) {
-            console.table(performance);
-          }
-          else {
-            $.each(performance, function(index, data) {
-              console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
-            });
-          }
-          console.groupEnd();
-        }
-        performance = [];
-      }
-    },
-    invoke: function(query, passedArguments, context) {
-      var
-        object = instance,
-        maxDepth,
-        found,
-        response
-      ;
-      passedArguments = passedArguments || queryArguments;
-      context         = element         || context;
-      if(typeof query == 'string' && object !== undefined) {
-        query    = query.split(/[\. ]/);
-        maxDepth = query.length - 1;
-        $.each(query, function(depth, value) {
-          var camelCaseValue = (depth != maxDepth)
-            ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
-            : query
-          ;
-          if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
-            object = object[camelCaseValue];
-          }
-          else if( object[camelCaseValue] !== undefined ) {
-            found = object[camelCaseValue];
-            return false;
-          }
-          else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
-            object = object[value];
-          }
-          else if( object[value] !== undefined ) {
-            found = object[value];
-            return false;
-          }
-          else {
-            module.error(error.method, query);
-            return false;
-          }
-        });
-      }
-      if ( $.isFunction( found ) ) {
-        response = found.apply(context, passedArguments);
-      }
-      else if(found !== undefined) {
-        response = found;
-      }
-      if(Array.isArray(returnedValue)) {
-        returnedValue.push(response);
-      }
-      else if(returnedValue !== undefined) {
-        returnedValue = [returnedValue, response];
-      }
-      else if(response !== undefined) {
-        returnedValue = response;
-      }
-      return found;
-    }
-  };
-
-  if(methodInvoked) {
-    if(instance === undefined) {
-      module.initialize();
-    }
-    module.invoke(query);
-  }
-  else {
-    if(instance !== undefined) {
-      module.destroy();
-    }
-    module.initialize();
-  }
-  return (returnedValue !== undefined)
-    ? returnedValue
-    : this
-  ;
-};
-
-$.site.settings = {
-
-  name        : 'Site',
-  namespace   : 'site',
-
-  error : {
-    console : 'Console cannot be restored, most likely it was overwritten outside of module',
-    method : 'The method you called is not defined.'
-  },
-
-  debug       : false,
-  verbose     : false,
-  performance : true,
-
-  modules: [
-    'accordion',
-    'api',
-    'calendar',
-    'checkbox',
-    'dimmer',
-    'dropdown',
-    'embed',
-    'form',
-    'modal',
-    'nag',
-    'popup',
-    'slider',
-    'rating',
-    'shape',
-    'sidebar',
-    'state',
-    'sticky',
-    'tab',
-    'toast',
-    'transition',
-    'visibility',
-    'visit'
-  ],
-
-  siteNamespace   : 'site',
-  namespaceStub   : {
-    cache     : {},
-    config    : {},
-    sections  : {},
-    section   : {},
-    utilities : {}
-  }
-
-};
-
-// allows for selection of elements with data attributes
-$.extend($.expr[ ":" ], {
-  data: ($.expr.createPseudo)
-    ? $.expr.createPseudo(function(dataName) {
-        return function(elem) {
-          return !!$.data(elem, dataName);
-        };
-      })
-    : function(elem, i, match) {
-      // support: jQuery < 1.8
-      return !!$.data(elem, match[ 3 ]);
-    }
-});
-
-
 })( jQuery, window, document );
 
 /*!
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index bd2ba15c62..367bdf3642 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -36,7 +36,6 @@
     "modal",
     "search",
     "segment",
-    "site",
     "tab",
     "table"
   ]

From 2ff213bbc1ce467c7047e17c65693d82ba87bd25 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 22 Mar 2024 20:16:23 +0800
Subject: [PATCH 473/679] Refactor markdown attention render (#29984)

Follow #29833 and add tests
---
 modules/markup/markdown/goldmark.go           | 87 +++++--------------
 modules/markup/markdown/markdown.go           |  2 +-
 modules/markup/markdown/markdown_test.go      | 36 ++++++++
 .../markup/markdown/transform_blockquote.go   | 67 ++++++++++++++
 modules/svg/svg.go                            | 18 +++-
 5 files changed, 145 insertions(+), 65 deletions(-)
 create mode 100644 modules/markup/markdown/transform_blockquote.go

diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index bdb7748247..b61299c480 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -27,7 +27,21 @@ import (
 )
 
 // ASTTransformer is a default transformer of the goldmark tree.
-type ASTTransformer struct{}
+type ASTTransformer struct {
+	AttentionTypes container.Set[string]
+}
+
+func NewASTTransformer() *ASTTransformer {
+	return &ASTTransformer{
+		AttentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
+	}
+}
+
+func (g *ASTTransformer) applyElementDir(n ast.Node) {
+	if markup.DefaultProcessorHelper.ElementDir != "" {
+		n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
+	}
+}
 
 // Transform transforms the given AST tree.
 func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
@@ -45,12 +59,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 		tocMode = rc.TOC
 	}
 
-	applyElementDir := func(n ast.Node) {
-		if markup.DefaultProcessorHelper.ElementDir != "" {
-			n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
-		}
-	}
-
 	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
 		if !entering {
 			return ast.WalkContinue, nil
@@ -72,9 +80,9 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 				header.ID = util.BytesToReadOnlyString(id.([]byte))
 			}
 			tocList = append(tocList, header)
-			applyElementDir(v)
+			g.applyElementDir(v)
 		case *ast.Paragraph:
-			applyElementDir(v)
+			g.applyElementDir(v)
 		case *ast.Image:
 			// Images need two things:
 			//
@@ -174,7 +182,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 					v.AppendChild(v, newChild)
 				}
 			}
-			applyElementDir(v)
+			g.applyElementDir(v)
 		case *ast.Text:
 			if v.SoftLineBreak() && !v.HardLineBreak() {
 				if ctx.Metas["mode"] != "document" {
@@ -189,51 +197,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 				v.AppendChild(v, NewColorPreview(colorContent))
 			}
 		case *ast.Blockquote:
-			// We only want attention blockquotes when the AST looks like:
-			// Text: "["
-			// Text: "!TYPE"
-			// Text(SoftLineBreak): "]"
-
-			// grab these nodes and make sure we adhere to the attention blockquote structure
-			firstParagraph := v.FirstChild()
-			if firstParagraph.ChildCount() < 3 {
-				return ast.WalkContinue, nil
-			}
-			firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text)
-			if !ok || string(firstTextNode.Segment.Value(reader.Source())) != "[" {
-				return ast.WalkContinue, nil
-			}
-			secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text)
-			if !ok || !attentionTypeRE.MatchString(string(secondTextNode.Segment.Value(reader.Source()))) {
-				return ast.WalkContinue, nil
-			}
-			thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text)
-			if !ok || string(thirdTextNode.Segment.Value(reader.Source())) != "]" {
-				return ast.WalkContinue, nil
-			}
-
-			// grab attention type from markdown source
-			attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNode.Segment.Value(reader.Source())), "!"))
-
-			// color the blockquote
-			v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
-
-			// create an emphasis to make it bold
-			attentionParagraph := ast.NewParagraph()
-			emphasis := ast.NewEmphasis(2)
-			emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
-
-			// capitalize first letter
-			attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:]))
-
-			// replace the ![TYPE] with a dedicated paragraph of icon+Type
-			emphasis.AppendChild(emphasis, attentionText)
-			attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
-			attentionParagraph.AppendChild(attentionParagraph, emphasis)
-			firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
-			firstParagraph.RemoveChild(firstParagraph, firstTextNode)
-			firstParagraph.RemoveChild(firstParagraph, secondTextNode)
-			firstParagraph.RemoveChild(firstParagraph, thirdTextNode)
+			return g.transformBlockquote(v, reader)
 		}
 		return ast.WalkContinue, nil
 	})
@@ -268,7 +232,7 @@ func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
 	return p.GenerateWithDefault(value, dft)
 }
 
-// Generate generates a new element id.
+// GenerateWithDefault generates a new element id.
 func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
 	result := common.CleanValue(value)
 	if len(result) == 0 {
@@ -303,7 +267,8 @@ func newPrefixedIDs() *prefixedIDs {
 // in the gitea form.
 func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
 	r := &HTMLRenderer{
-		Config: html.NewConfig(),
+		Config:      html.NewConfig(),
+		reValidName: regexp.MustCompile("^[a-z ]+$"),
 	}
 	for _, opt := range opts {
 		opt.SetHTMLOption(&r.Config)
@@ -315,6 +280,7 @@ func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
 // renders gitea specific features.
 type HTMLRenderer struct {
 	html.Config
+	reValidName *regexp.Regexp
 }
 
 // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
@@ -442,11 +408,6 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
 	return ast.WalkContinue, nil
 }
 
-var (
-	validNameRE     = regexp.MustCompile("^[a-z ]+$")
-	attentionTypeRE = regexp.MustCompile("^!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)$")
-)
-
 func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	if !entering {
 		return ast.WalkContinue, nil
@@ -461,7 +422,7 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
 		return ast.WalkContinue, nil
 	}
 
-	if !validNameRE.MatchString(name) {
+	if !r.reValidName.MatchString(name) {
 		// skip this
 		return ast.WalkContinue, nil
 	}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 4cca71d511..db4e5706f6 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -126,7 +126,7 @@ func SpecializedMarkdown() goldmark.Markdown {
 				parser.WithAttribute(),
 				parser.WithAutoHeadingID(),
 				parser.WithASTTransformers(
-					util.Prioritized(&ASTTransformer{}, 10000),
+					util.Prioritized(NewASTTransformer(), 10000),
 				),
 			),
 			goldmark.WithRendererOptions(
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index a12bd4f9e7..ebac3fbe9e 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -16,9 +16,12 @@ import (
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/svg"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
 )
 
 const (
@@ -957,3 +960,36 @@ space</p>
 		assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
 	}
 }
+
+func TestAttention(t *testing.T) {
+	defer svg.MockIcon("octicon-info")()
+	defer svg.MockIcon("octicon-light-bulb")()
+	defer svg.MockIcon("octicon-report")()
+	defer svg.MockIcon("octicon-alert")()
+	defer svg.MockIcon("octicon-stop")()
+
+	renderAttention := func(attention, icon string) string {
+		tmpl := `<blockquote class="attention-header attention-{attention}"><p><svg class="attention-icon attention-{attention} svg {icon}" width="16" height="16"></svg><strong class="attention-{attention}">{Attention}</strong></p>`
+		tmpl = strings.ReplaceAll(tmpl, "{attention}", attention)
+		tmpl = strings.ReplaceAll(tmpl, "{icon}", icon)
+		tmpl = strings.ReplaceAll(tmpl, "{Attention}", cases.Title(language.English).String(attention))
+		return tmpl
+	}
+
+	test := func(input, expected string) {
+		result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, input)
+		assert.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
+	}
+
+	test(`
+> [!NOTE]
+> text
+`, renderAttention("note", "octicon-info")+"\n<p>text</p>\n</blockquote>")
+
+	test(`> [!note]`, renderAttention("note", "octicon-info")+"\n</blockquote>")
+	test(`> [!tip]`, renderAttention("tip", "octicon-light-bulb")+"\n</blockquote>")
+	test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>")
+	test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
+	test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\n</blockquote>")
+}
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
new file mode 100644
index 0000000000..d685cfd1c5
--- /dev/null
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -0,0 +1,67 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"strings"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
+)
+
+func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
+	// We only want attention blockquotes when the AST looks like:
+	// > Text("[") Text("!TYPE") Text("]")
+
+	// grab these nodes and make sure we adhere to the attention blockquote structure
+	firstParagraph := v.FirstChild()
+	g.applyElementDir(firstParagraph)
+	if firstParagraph.ChildCount() < 3 {
+		return ast.WalkContinue, nil
+	}
+	node1, ok1 := firstParagraph.FirstChild().(*ast.Text)
+	node2, ok2 := node1.NextSibling().(*ast.Text)
+	node3, ok3 := node2.NextSibling().(*ast.Text)
+	if !ok1 || !ok2 || !ok3 {
+		return ast.WalkContinue, nil
+	}
+	val1 := string(node1.Segment.Value(reader.Source()))
+	val2 := string(node2.Segment.Value(reader.Source()))
+	val3 := string(node3.Segment.Value(reader.Source()))
+	if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") {
+		return ast.WalkContinue, nil
+	}
+
+	// grab attention type from markdown source
+	attentionType := strings.ToLower(val2[1:])
+	if !g.AttentionTypes.Contains(attentionType) {
+		return ast.WalkContinue, nil
+	}
+
+	// color the blockquote
+	v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
+
+	// create an emphasis to make it bold
+	attentionParagraph := ast.NewParagraph()
+	g.applyElementDir(attentionParagraph)
+	emphasis := ast.NewEmphasis(2)
+	emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
+
+	attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType)))
+
+	// replace the ![TYPE] with a dedicated paragraph of icon+Type
+	emphasis.AppendChild(emphasis, attentionAstString)
+	attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
+	attentionParagraph.AppendChild(attentionParagraph, emphasis)
+	firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
+	firstParagraph.RemoveChild(firstParagraph, node1)
+	firstParagraph.RemoveChild(firstParagraph, node2)
+	firstParagraph.RemoveChild(firstParagraph, node3)
+	if firstParagraph.ChildCount() == 0 {
+		firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
+	}
+	return ast.WalkContinue, nil
+}
diff --git a/modules/svg/svg.go b/modules/svg/svg.go
index 016e1dc08b..8132978cac 100644
--- a/modules/svg/svg.go
+++ b/modules/svg/svg.go
@@ -41,6 +41,21 @@ func Init() error {
 	return nil
 }
 
+func MockIcon(icon string) func() {
+	if svgIcons == nil {
+		svgIcons = make(map[string]string)
+	}
+	orig, exist := svgIcons[icon]
+	svgIcons[icon] = fmt.Sprintf(`<svg class="svg %s" width="%d" height="%d"></svg>`, icon, defaultSize, defaultSize)
+	return func() {
+		if exist {
+			svgIcons[icon] = orig
+		} else {
+			delete(svgIcons, icon)
+		}
+	}
+}
+
 // RenderHTML renders icons - arguments icon name (string), size (int), class (string)
 func RenderHTML(icon string, others ...any) template.HTML {
 	size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...)
@@ -55,5 +70,6 @@ func RenderHTML(icon string, others ...any) template.HTML {
 		}
 		return template.HTML(svgStr)
 	}
-	return ""
+	// during test (or something wrong happens), there is no SVG loaded, so use a dummy span to tell that the icon is missing
+	return template.HTML(fmt.Sprintf("<span>%s(%d/%s)</span>", template.HTMLEscapeString(icon), size, template.HTMLEscapeString(class)))
 }

From 29118743a58cf3172bddb6a4fa287484c62b4eb1 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 22 Mar 2024 13:28:38 +0100
Subject: [PATCH 474/679] Small refactors in anchors.js (#29947)

Some minor refactors, remove unnecessary `:is` selector and move the
`:target` check out of the function. Might as well backport for the rare
browser that does not support `:is`.
---
 web_src/js/markup/anchors.js | 40 +++++++++++++++++++-----------------
 1 file changed, 21 insertions(+), 19 deletions(-)

diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
index dac877fd99..0e2c92713a 100644
--- a/web_src/js/markup/anchors.js
+++ b/web_src/js/markup/anchors.js
@@ -1,29 +1,30 @@
 import {svg} from '../svg.js';
 
+const addPrefix = (str) => `user-content-${str}`;
+const removePrefix = (str) => str.replace(/^user-content-/, '');
+const hasPrefix = (str) => str.startsWith('user-content-');
+
 // scroll to anchor while respecting the `user-content` prefix that exists on the target
-function scrollToAnchor(encodedId, initial) {
-  // abort if the browser has already scrolled to another anchor during page load
-  if (!encodedId || (initial && document.querySelector(':target'))) return;
+function scrollToAnchor(encodedId) {
+  if (!encodedId) return;
   const id = decodeURIComponent(encodedId);
-  let el = document.getElementById(`user-content-${id}`);
+  const prefixedId = addPrefix(id);
+  let el = document.getElementById(prefixedId);
 
   // check for matching user-generated `a[name]`
   if (!el) {
-    const nameAnchors = document.getElementsByName(`user-content-${id}`);
+    const nameAnchors = document.getElementsByName(prefixedId);
     if (nameAnchors.length) {
       el = nameAnchors[0];
     }
   }
 
   // compat for links with old 'user-content-' prefixed hashes
-  if (!el && id.startsWith('user-content-')) {
-    const el = document.getElementById(id);
-    if (el) el.scrollIntoView();
+  if (!el && hasPrefix(id)) {
+    return document.getElementById(id)?.scrollIntoView();
   }
 
-  if (el) {
-    el.scrollIntoView();
-  }
+  el?.scrollIntoView();
 }
 
 export function initMarkupAnchors() {
@@ -32,11 +33,10 @@ export function initMarkupAnchors() {
 
   for (const markupEl of markupEls) {
     // create link icons for markup headings, the resulting link href will remove `user-content-`
-    for (const heading of markupEl.querySelectorAll(`:is(h1, h2, h3, h4, h5, h6`)) {
-      const originalId = heading.id.replace(/^user-content-/, '');
+    for (const heading of markupEl.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
       const a = document.createElement('a');
       a.classList.add('anchor');
-      a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
+      a.setAttribute('href', `#${encodeURIComponent(removePrefix(heading.id))}`);
       a.innerHTML = svg('octicon-link');
       heading.prepend(a);
     }
@@ -45,8 +45,7 @@ export function initMarkupAnchors() {
     for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
       const href = a.getAttribute('href');
       if (!href.startsWith('#user-content-')) continue;
-      const originalId = href.replace(/^#user-content-/, '');
-      a.setAttribute('href', `#${originalId}`);
+      a.setAttribute('href', `#${removePrefix(href.substring(1))}`);
     }
 
     // add `user-content-` prefix to user-generated `a[name]` link targets
@@ -54,15 +53,18 @@ export function initMarkupAnchors() {
     for (const a of markupEl.querySelectorAll('a[name]')) {
       const name = a.getAttribute('name');
       if (!name) continue;
-      a.setAttribute('name', `user-content-${a.name}`);
+      a.setAttribute('name', addPrefix(a.name));
     }
 
     for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
       a.addEventListener('click', (e) => {
-        scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1), false);
+        scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1));
       });
     }
   }
 
-  scrollToAnchor(window.location.hash.substring(1), true);
+  // scroll to anchor unless the browser has already scrolled somewhere during page load
+  if (!document.querySelector(':target')) {
+    scrollToAnchor(window.location.hash?.substring(1));
+  }
 }

From f8ab9dafb7a173a35e9308f8f784735b0f822439 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 22 Mar 2024 20:53:52 +0800
Subject: [PATCH 475/679] Use db.ListOptionsAll instead of
 db.ListOptions{ListAll: true} (#29995)

---
 .../indexer/issues/internal/tests/tests.go    | 48 +++++++------------
 modules/indexer/issues/util.go                |  4 +-
 routers/web/admin/users.go                    |  8 +---
 routers/web/repo/commit.go                    |  2 +-
 routers/web/repo/compare.go                   | 12 ++---
 routers/web/repo/fork.go                      |  6 +--
 routers/web/repo/pull.go                      |  6 +--
 routers/web/repo/release.go                   |  2 +-
 routers/web/repo/repo.go                      |  8 +---
 routers/web/repo/view.go                      |  2 +-
 services/actions/commit_status.go             |  2 +-
 services/pull/commit_status.go                |  2 +-
 services/pull/pull.go                         |  2 +-
 services/pull/review.go                       | 14 ++----
 services/repository/adopt.go                  |  6 +--
 15 files changed, 43 insertions(+), 81 deletions(-)

diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 91aafd589c..2209377c2f 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -515,10 +515,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCreatedDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCreatedDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCreatedDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -533,10 +531,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByUpdatedDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByUpdatedDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByUpdatedDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -551,10 +547,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCommentsDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCommentsDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCommentsDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -569,10 +563,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByDeadlineDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByDeadlineDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByDeadlineDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -587,10 +579,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCreatedAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCreatedAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCreatedAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -605,10 +595,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByUpdatedAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByUpdatedAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByUpdatedAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -623,10 +611,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCommentsAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCommentsAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCommentsAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -641,10 +627,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByDeadlineAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByDeadlineAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByDeadlineAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go
index 510b4060b2..9861c808dc 100644
--- a/modules/indexer/issues/util.go
+++ b/modules/indexer/issues/util.go
@@ -61,9 +61,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
 	)
 	{
 		reviews, err := issue_model.FindReviews(ctx, issue_model.FindReviewOptions{
-			ListOptions: db.ListOptions{
-				ListAll: true,
-			},
+			ListOptions:  db.ListOptionsAll,
 			IssueID:      issueID,
 			OfficialOnly: false,
 		})
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index 6dfcfc3d9a..b93668c5a2 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -275,9 +275,7 @@ func ViewUser(ctx *context.Context) {
 	}
 
 	repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions: db.ListOptionsAll,
 		OwnerID:     u.ID,
 		OrderBy:     db.SearchOrderByAlphabetically,
 		Private:     true,
@@ -300,9 +298,7 @@ func ViewUser(ctx *context.Context) {
 	ctx.Data["EmailsTotal"] = len(emails)
 
 	orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions:    db.ListOptionsAll,
 		UserID:         u.ID,
 		IncludePrivate: true,
 	})
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index d66de782f4..8543fa44cc 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -351,7 +351,7 @@ func Diff(ctx *context.Context) {
 	ctx.Data["Commit"] = commit
 	ctx.Data["Diff"] = diff
 
-	statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptions{ListAll: true})
+	statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
 	if err != nil {
 		log.Error("GetLatestCommitStatus: %v", err)
 	}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 7b5243e6b7..cfb0e859bd 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -697,10 +697,8 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
 	defer gitRepo.Close()
 
 	branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: repo.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		RepoID:          repo.ID,
+		ListOptions:     db.ListOptionsAll,
 		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
@@ -754,10 +752,8 @@ func CompareDiff(ctx *context.Context) {
 	}
 
 	headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: ci.HeadRepo.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		RepoID:          ci.HeadRepo.ID,
+		ListOptions:     db.ListOptionsAll,
 		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
index 60e37476ee..27e42a8f98 100644
--- a/routers/web/repo/fork.go
+++ b/routers/web/repo/fork.go
@@ -101,10 +101,8 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
 	}
 
 	branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: ctx.Repo.Repository.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		RepoID:          ctx.Repo.Repository.ID,
+		ListOptions:     db.ListOptionsAll,
 		IsDeletedBranch: optional.Some(false),
 		// Add it as the first option
 		ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 447781602d..2422be39b8 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -278,7 +278,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
 
 	if len(compareInfo.Commits) != 0 {
 		sha := compareInfo.Commits[0].ID.String()
-		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptions{ListAll: true})
+		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
 		if err != nil {
 			ctx.ServerError("GetLatestCommitStatus", err)
 			return nil
@@ -340,7 +340,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 			ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
 			return nil
 		}
-		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
+		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
 		if err != nil {
 			ctx.ServerError("GetLatestCommitStatus", err)
 			return nil
@@ -432,7 +432,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 		return nil
 	}
 
-	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
+	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
 	if err != nil {
 		ctx.ServerError("GetLatestCommitStatus", err)
 		return nil
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index dbc190928f..7ba23f0701 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -136,7 +136,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
 		}
 
 		if canReadActions {
-			statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptions{ListAll: true})
+			statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
 			if err != nil {
 				return nil, err
 			}
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 0490feb621..4e448933c7 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -688,9 +688,7 @@ func GetBranchesList(ctx *context.Context) {
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
 		IsDeletedBranch: optional.Some(false),
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions:     db.ListOptionsAll,
 	}
 	branches, err := git_model.FindBranchNames(ctx, branchOpts)
 	if err != nil {
@@ -723,9 +721,7 @@ func PrepareBranchList(ctx *context.Context) {
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
 		IsDeletedBranch: optional.Some(false),
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions:     db.ListOptionsAll,
 	}
 	brs, err := git_model.FindBranchNames(ctx, branchOpts)
 	if err != nil {
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 712d12705e..b9e623919a 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -359,7 +359,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
 		ctx.Data["LatestCommitVerification"] = verification
 		ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
 
-		statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{ListAll: true})
+		statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
 		if err != nil {
 			log.Error("GetLatestCommitStatus: %v", err)
 		}
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index edd1fd1568..4236553927 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -79,7 +79,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
 	}
 	ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
 	state := toCommitStatus(job.Status)
-	if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true}); err == nil {
+	if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil {
 		for _, v := range statuses {
 			if v.Context == ctxname {
 				if v.State == state {
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index 653bfe6bcb..aa1ad7cd66 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -152,7 +152,7 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
 		return "", errors.Wrap(err, "LoadBaseRepo")
 	}
 
-	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
+	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
 	if err != nil {
 		return "", errors.Wrap(err, "GetLatestCommitStatus")
 	}
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 8a9c6db917..4289e2e6e1 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -883,7 +883,7 @@ func getAllCommitStatus(ctx context.Context, gitRepo *git.Repository, pr *issues
 		return nil, nil, shaErr
 	}
 
-	statuses, _, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
+	statuses, _, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
 	lastStatus = git_model.CalcCommitStatus(statuses)
 	return statuses, lastStatus, err
 }
diff --git a/services/pull/review.go b/services/pull/review.go
index 8900ae2ab1..de1021c5c0 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -52,9 +52,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
 	issueIDs := prs.GetIssueIDs()
 
 	codeComments, err := db.Find[issues_model.Comment](ctx, issues_model.FindCommentsOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions: db.ListOptionsAll,
 		Type:        issues_model.CommentTypeCode,
 		Invalidated: optional.Some(false),
 		IssueIDs:    issueIDs,
@@ -322,12 +320,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
 // DismissApprovalReviews dismiss all approval reviews because of new commits
 func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
 	reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
-		IssueID:   pull.IssueID,
-		Type:      issues_model.ReviewTypeApprove,
-		Dismissed: optional.Some(false),
+		ListOptions: db.ListOptionsAll,
+		IssueID:     pull.IssueID,
+		Type:        issues_model.ReviewTypeApprove,
+		Dismissed:   optional.Some(false),
 	})
 	if err != nil {
 		return err
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 0ac3c774b7..b337eac38a 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -144,10 +144,8 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
 	}
 
 	branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: repo.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		RepoID:          repo.ID,
+		ListOptions:     db.ListOptionsAll,
 		IsDeletedBranch: optional.Some(false),
 	})
 

From 0c55506b407731546c6bacd1442a785db68f55a7 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 22 Mar 2024 21:17:39 +0800
Subject: [PATCH 476/679] Add border radius for wiki pages (#29937)

Before

<img width="1312" alt="image"
src="https://github.com/go-gitea/gitea/assets/81045/26a6dec2-9fea-4c0c-b6fb-290eab12a55a">

After

<img width="1298" alt="image"
src="https://github.com/go-gitea/gitea/assets/81045/01f7a714-eae9-4729-918f-3b4795094d0b">
---
 web_src/css/repo/wiki.css | 1 +
 1 file changed, 1 insertion(+)

diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css
index bb6f364557..ba502d3216 100644
--- a/web_src/css/repo/wiki.css
+++ b/web_src/css/repo/wiki.css
@@ -21,6 +21,7 @@
 
 .repository.wiki .wiki-content-parts .markup {
   border: 1px solid var(--color-secondary);
+  border-radius: var(--border-radius);
   padding: 1em;
   margin-top: 1em;
   font-size: 1em;

From f88ad5424f381bf2a45fd863b551c5a72891bb68 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 22 Mar 2024 14:45:10 +0100
Subject: [PATCH 477/679] Replace 10 more gt- classes with tw- (#29945)

Likely the biggest change of the tailwind refactors. Only thing of note
is that `tw-flex-1` resolves to `flex: 1 1 0%` while our `gt-f1` was
`flex: 1 1 0`, I don't think it will make any difference. Commands I've
ran:

```sh
perl -p -i -e 's#gt-vm#tw-align-middle#g' web_src/js/**/* templates/**/* models/**/*
perl -p -i -e 's#gt-fw#tw-flex-wrap#g' web_src/js/**/* templates/**/* models/**/*
perl -p -i -e 's#gt-f1#tw-flex-1#g' web_src/js/**/* templates/**/* models/**/*
perl -p -i -e 's#gt-fc#tw-flex-col#g' web_src/js/**/* templates/**/* models/**/*
perl -p -i -e 's#gt-sb#tw-justify-between#g' web_src/js/**/* templates/**/* models/**/*
perl -p -i -e 's#gt-je#tw-justify-end#g' web_src/js/**/* templates/**/* models/**/*
perl -p -i -e 's#gt-jc#tw-justify-center#g' web_src/js/**/* templates/**/* models/**/*
perl -p -i -e 's#gt-ac#tw-content-center#g' web_src/js/**/* templates/**/* models/**/* tests/**/*
perl -p -i -e 's#gt-df#tw-flex#g' web_src/js/**/* templates/**/* models/**/* tests/**/*
perl -p -i -e 's#gt-dib#tw-inline-block#g' web_src/js/**/* templates/**/* models/**/* tests/**/*

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 .../contributing/guidelines-frontend.en-us.md |  2 +-
 .../contributing/guidelines-frontend.zh-cn.md |  2 +-
 models/avatars/avatar.go                      |  2 +-
 templates/admin/emails/list.tmpl              |  4 +-
 templates/admin/notice.tmpl                   |  2 +-
 templates/admin/org/list.tmpl                 |  4 +-
 templates/admin/queue_manage.tmpl             |  2 +-
 templates/admin/repo/unadopted.tmpl           |  4 +-
 templates/admin/stacktrace-row.tmpl           |  8 ++--
 templates/admin/stacktrace.tmpl               |  4 +-
 templates/admin/user/list.tmpl                |  2 +-
 templates/admin/user/view.tmpl                |  4 +-
 templates/base/head_navbar.tmpl               |  8 ++--
 templates/devtest/fomantic-modal.tmpl         |  2 +-
 templates/devtest/tmplerr.tmpl                |  2 +-
 templates/explore/search.tmpl                 |  4 +-
 templates/explore/user_list.tmpl              |  2 +-
 templates/org/header.tmpl                     |  6 +--
 templates/org/home.tmpl                       | 12 ++---
 templates/org/member/members.tmpl             |  2 +-
 templates/org/settings/labels.tmpl            |  4 +-
 templates/org/team/members.tmpl               |  6 +--
 templates/org/team/new.tmpl                   |  6 +--
 templates/org/team/repositories.tmpl          |  8 ++--
 templates/package/view.tmpl                   |  4 +-
 templates/projects/list.tmpl                  |  2 +-
 templates/projects/view.tmpl                  |  2 +-
 templates/repo/actions/list.tmpl              |  2 +-
 templates/repo/actions/runs_list.tmpl         |  2 +-
 templates/repo/actions/status.tmpl            |  2 +-
 templates/repo/blame.tmpl                     |  6 +--
 templates/repo/branch/list.tmpl               | 10 ++---
 templates/repo/branch_dropdown.tmpl           |  4 +-
 .../code/recently_pushed_new_branches.tmpl    |  4 +-
 .../repo/commit_load_branches_and_tags.tmpl   |  8 ++--
 templates/repo/commit_page.tmpl               | 16 +++----
 templates/repo/commit_statuses.tmpl           |  4 +-
 templates/repo/commits.tmpl                   |  2 +-
 templates/repo/commits_list_small.tmpl        |  2 +-
 templates/repo/commits_table.tmpl             |  4 +-
 templates/repo/diff/blob_excerpt.tmpl         |  4 +-
 templates/repo/diff/box.tmpl                  | 16 +++----
 templates/repo/diff/comments.tmpl             |  6 +--
 templates/repo/diff/conversation.tmpl         | 12 ++---
 templates/repo/diff/new_review.tmpl           | 10 ++---
 templates/repo/diff/section_split.tmpl        |  2 +-
 templates/repo/diff/section_unified.tmpl      |  2 +-
 templates/repo/find/files.tmpl                |  4 +-
 templates/repo/forks.tmpl                     |  2 +-
 templates/repo/graph/commits.tmpl             |  8 ++--
 templates/repo/header.tmpl                    |  2 +-
 templates/repo/home.tmpl                      | 14 +++---
 templates/repo/icon.tmpl                      |  2 +-
 templates/repo/issue/card.tmpl                | 16 +++----
 templates/repo/issue/filter_actions.tmpl      |  2 +-
 templates/repo/issue/filter_list.tmpl         |  6 +--
 templates/repo/issue/label_precolors.tmpl     |  4 +-
 templates/repo/issue/labels/label_list.tmpl   |  4 +-
 templates/repo/issue/milestone_issues.tmpl    | 10 ++---
 templates/repo/issue/milestones.tmpl          |  2 +-
 templates/repo/issue/new_form.tmpl            |  2 +-
 templates/repo/issue/view_content.tmpl        |  6 +--
 .../repo/issue/view_content/attachments.tmpl  |  6 +--
 .../repo/issue/view_content/comments.tmpl     | 14 +++---
 .../repo/issue/view_content/conversation.tmpl | 16 +++----
 templates/repo/issue/view_content/pull.tmpl   |  6 +--
 .../repo/issue/view_content/sidebar.tmpl      | 44 +++++++++----------
 .../view_content/update_branch_by_merge.tmpl  |  2 +-
 templates/repo/issue/view_title.tmpl          |  2 +-
 templates/repo/migrate/migrate.tmpl           |  2 +-
 templates/repo/projects/view.tmpl             |  2 +-
 templates/repo/pulls/fork.tmpl                |  2 +-
 templates/repo/pulls/tab_menu.tmpl            |  2 +-
 templates/repo/release/list.tmpl              |  4 +-
 templates/repo/release/new.tmpl               |  6 +--
 templates/repo/release_tag_header.tmpl        |  4 +-
 templates/repo/settings/branches.tmpl         |  6 +--
 templates/repo/settings/collaboration.tmpl    |  6 +--
 templates/repo/settings/githooks.tmpl         |  2 +-
 templates/repo/settings/options.tmpl          |  8 ++--
 templates/repo/settings/tags.tmpl             |  2 +-
 .../repo/settings/webhook/base_list.tmpl      |  2 +-
 templates/repo/settings/webhook/history.tmpl  |  2 +-
 templates/repo/tag/list.tmpl                  |  8 ++--
 templates/repo/view_file.tmpl                 |  6 +--
 templates/repo/wiki/new.tmpl                  |  2 +-
 templates/repo/wiki/pages.tmpl                |  2 +-
 templates/repo/wiki/view.tmpl                 |  2 +-
 templates/shared/actions/runner_edit.tmpl     |  8 ++--
 templates/shared/search/code/results.tmpl     |  8 ++--
 templates/shared/searchbottom.tmpl            |  4 +-
 templates/shared/secrets/add_list.tmpl        |  2 +-
 templates/shared/user/org_profile_avatar.tmpl |  2 +-
 templates/shared/user/profile_big_avatar.tmpl |  6 +--
 templates/shared/variables/variable_list.tmpl |  2 +-
 templates/status/500.tmpl                     |  4 +-
 templates/user/auth/signin_inner.tmpl         |  6 +--
 templates/user/auth/signup_inner.tmpl         |  6 +--
 templates/user/auth/webauthn.tmpl             |  2 +-
 templates/user/dashboard/feeds.tmpl           |  2 +-
 templates/user/dashboard/milestones.tmpl      |  2 +-
 .../user/notification/notification_div.tmpl   | 12 ++---
 .../notification_subscriptions.tmpl           |  6 +--
 templates/user/settings/account.tmpl          |  2 +-
 .../settings/applications_oauth2_list.tmpl    |  2 +-
 templates/user/settings/repos.tmpl            |  4 +-
 templates/user/settings/security/openid.tmpl  |  2 +-
 templates/webhook/new.tmpl                    |  2 +-
 tests/integration/release_test.go             |  2 +-
 web_src/css/actions.css                       |  4 --
 web_src/css/helpers.css                       | 10 -----
 web_src/js/components/ActionRunStatus.vue     |  2 +-
 web_src/js/components/DashboardRepoList.vue   | 20 ++++-----
 web_src/js/components/DiffCommitSelector.vue  |  2 +-
 web_src/js/components/DiffFileList.vue        |  4 +-
 web_src/js/components/DiffFileTreeItem.vue    |  2 +-
 .../js/components/PullRequestMergeForm.vue    |  2 +-
 .../js/components/RepoBranchTagSelector.vue   |  4 +-
 web_src/js/components/RepoCodeFrequency.vue   |  4 +-
 web_src/js/components/RepoContributors.vue    | 10 ++---
 web_src/js/components/RepoRecentCommits.vue   |  4 +-
 web_src/js/features/repo-issue-content.js     |  2 +-
 web_src/js/features/repo-issue-list.js        |  2 +-
 123 files changed, 308 insertions(+), 322 deletions(-)

diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index 2c0aaaed4a..eec4a88fd0 100644
--- a/docs/content/contributing/guidelines-frontend.en-us.md
+++ b/docs/content/contributing/guidelines-frontend.en-us.md
@@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
 11. Custom event names are recommended to use `ce-` prefix.
-12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
+12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-mono`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
 
 ### Accessibility / ARIA
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index b5fb8964b3..040dba3d76 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
 11. 推荐使用自定义事件名称前缀`ce-`。
-12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
+12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-mono`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
 13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
 
 ### 可访问性 / ARIA
diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go
index bbe16483bf..9c56e0f9a0 100644
--- a/models/avatars/avatar.go
+++ b/models/avatars/avatar.go
@@ -24,7 +24,7 @@ import (
 
 const (
 	// DefaultAvatarClass is the default class of a rendered avatar
-	DefaultAvatarClass = "ui avatar gt-vm"
+	DefaultAvatarClass = "ui avatar tw-align-middle"
 	// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
 	DefaultAvatarPixelSize = 28
 )
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 660df55999..0b5249fbd9 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -4,8 +4,8 @@
 			{{ctx.Locale.Tr "admin.emails.email_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
 		</h4>
 		<div class="ui attached segment">
-			<div class="ui secondary filter menu gt-ac gt-mx-0">
-				<form class="ui form ignore-dirty gt-f1">
+			<div class="ui secondary filter menu tw-content-center gt-mx-0">
+				<form class="ui form ignore-dirty tw-flex-1">
 					{{template "shared/search/combo" dict "Value" .Keyword}}
 				</form>
 				<!-- Sort -->
diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index f7d77eab1d..c788ebc602 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -17,7 +17,7 @@
 			<tbody>
 				{{range .Notices}}
 					<tr>
-						<td><div class="ui checkbox gt-df" data-id="{{.ID}}"><input type="checkbox"></div></td>
+						<td><div class="ui checkbox tw-flex" data-id="{{.ID}}"><input type="checkbox"></div></td>
 						<td>{{.ID}}</td>
 						<td>{{ctx.Locale.Tr .TrStr}}</td>
 						<td class="view-detail auto-ellipsis" style="width: 80%;"><span class="notice-description">{{.Description}}</span></td>
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index 4609d1b8b4..abd43d297e 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -7,8 +7,8 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			<div class="ui secondary filter menu gt-ac gt-mx-0">
-				<form class="ui form ignore-dirty gt-f1">
+			<div class="ui secondary filter menu tw-content-center gt-mx-0">
+				<form class="ui form ignore-dirty tw-flex-1">
 					{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}}
 				</form>
 				<!-- Sort -->
diff --git a/templates/admin/queue_manage.tmpl b/templates/admin/queue_manage.tmpl
index 80214d1021..dd1682a000 100644
--- a/templates/admin/queue_manage.tmpl
+++ b/templates/admin/queue_manage.tmpl
@@ -30,7 +30,7 @@
 								-
 							{{else}}
 								{{$sum}}
-								<form action="{{$.Link}}/remove-all-items" method="post" class="gt-dib gt-ml-4">
+								<form action="{{$.Link}}/remove-all-items" method="post" class="tw-inline-block gt-ml-4">
 									{{$.CsrfTokenHtml}}
 									<button class="ui tiny basic red button">{{ctx.Locale.Tr "admin.monitor.queue.settings.remove_all_items"}}</button>
 								</form>
diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl
index c65cfd9db4..eb8188de14 100644
--- a/templates/admin/repo/unadopted.tmpl
+++ b/templates/admin/repo/unadopted.tmpl
@@ -20,8 +20,8 @@
 				{{if .Dirs}}
 					<div class="ui aligned divided list">
 						{{range $dirI, $dir := .Dirs}}
-							<div class="item gt-df gt-ac">
-								<span class="gt-f1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
+							<div class="item tw-flex tw-content-center">
+								<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
 								<div>
 									<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
 									<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl
index ffb8bf812f..fdce81eda7 100644
--- a/templates/admin/stacktrace-row.tmpl
+++ b/templates/admin/stacktrace-row.tmpl
@@ -1,5 +1,5 @@
 <div class="item">
-	<div class="gt-df gt-ac">
+	<div class="tw-flex tw-content-center">
 		<div class="icon gt-ml-3 gt-mr-3">
 			{{if eq .Process.Type "request"}}
 				{{svg "octicon-globe" 16}}
@@ -11,7 +11,7 @@
 				{{svg "octicon-code" 16}}
 			{{end}}
 		</div>
-		<div class="content gt-f1">
+		<div class="content tw-flex-1">
 			<div class="header">{{.Process.Description}}</div>
 			<div class="description">{{if ne .Process.Type "none"}}{{TimeSince .Process.Start ctx.Locale}}{{end}}</div>
 		</div>
@@ -40,9 +40,9 @@
 						</summary>
 						<div class="list">
 							{{range .Entry}}
-								<div class="item gt-df gt-ac">
+								<div class="item tw-flex tw-content-center">
 									<span class="icon gt-mr-4">{{svg "octicon-dot-fill" 16}}</span>
-									<div class="content gt-f1">
+									<div class="content tw-flex-1">
 										<div class="header"><code>{{.Function}}</code></div>
 										<div class="description"><code>{{.File}}:{{.Line}}</code></div>
 									</div>
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index 950aa0ea86..3c13c1e9dd 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -1,8 +1,8 @@
 {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
 <div class="admin-setting-content">
 
-	<div class="gt-df gt-ac">
-		<div class="gt-f1">
+	<div class="tw-flex tw-content-center">
+		<div class="tw-flex-1">
 			<div class="ui compact small menu">
 				<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
 				<a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 091cbe7287..427eef7a78 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -103,7 +103,7 @@
 								<td><span>{{ctx.Locale.Tr "admin.users.never_login"}}</span></td>
 							{{end}}
 							<td>
-								<div class="gt-df gt-gap-3">
+								<div class="tw-flex gt-gap-3">
 									<a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.users.details"}}">{{svg "octicon-person"}}</a>
 									<a href="{{$.Link}}/{{.ID}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a>
 								</div>
diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl
index fd3017607c..21943a8382 100644
--- a/templates/admin/user/view.tmpl
+++ b/templates/admin/user/view.tmpl
@@ -2,7 +2,7 @@
 
 <div class="admin-setting-content">
 	<div class="admin-responsive-columns">
-		<div class="gt-f1">
+		<div class="tw-flex-1">
 			<h4 class="ui top attached header">
 				{{.Title}}
 				<div class="ui right">
@@ -13,7 +13,7 @@
 				{{template "admin/user/view_details" .}}
 			</div>
 		</div>
-		<div class="gt-f1">
+		<div class="tw-flex-1">
 			<h4 class="ui top attached header">
 				{{ctx.Locale.Tr "admin.emails"}}
 				<div class="ui right">
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index ffa2ef844f..1c2a7b2d9a 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -56,7 +56,7 @@
 	<div class="navbar-right ui secondary menu">
 		{{if and .IsSigned .MustChangePassword}}
 			<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
-				<span class="text gt-df gt-ac">
+				<span class="text tw-flex tw-content-center">
 					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
 					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
@@ -83,8 +83,8 @@
 				<span class="only-mobile gt-ml-3">{{ctx.Locale.Tr "active_stopwatch"}}</span>
 			</a>
 			<div class="active-stopwatch-popup item tippy-target gt-p-3">
-				<div class="gt-df gt-ac">
-					<a class="stopwatch-link gt-df gt-ac" href="{{.ActiveStopwatch.IssueLink}}">
+				<div class="tw-flex tw-content-center">
+					<a class="stopwatch-link tw-flex tw-content-center" href="{{.ActiveStopwatch.IssueLink}}">
 						{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
 						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
 						<span class="ui primary label stopwatch-time gt-my-0 gt-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
@@ -142,7 +142,7 @@
 			</div><!-- end dropdown menu create new -->
 
 			<div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
-				<span class="text gt-df gt-ac">
+				<span class="text tw-flex tw-content-center">
 					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
 					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/fomantic-modal.tmpl
index 0b4199a197..5cd36721a7 100644
--- a/templates/devtest/fomantic-modal.tmpl
+++ b/templates/devtest/fomantic-modal.tmpl
@@ -73,7 +73,7 @@
 		{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" "I know and must do  this is dangerous operation")}}
 	</div>
 
-	<div class="modal-buttons flex-text-block gt-fw"></div>
+	<div class="modal-buttons flex-text-block tw-flex-wrap"></div>
 	<script type="module">
 		for (const el of $('.ui.modal')) {
 			const $btn = $('<button>').text(`${el.id}`).on('click', () => {
diff --git a/templates/devtest/tmplerr.tmpl b/templates/devtest/tmplerr.tmpl
index 2fe3f1effd..09cf05fc1f 100644
--- a/templates/devtest/tmplerr.tmpl
+++ b/templates/devtest/tmplerr.tmpl
@@ -1,6 +1,6 @@
 {{template "base/head" .}}
 <div class="page-content devtest">
-	<div class="gt-df">
+	<div class="tw-flex">
 		<div style="width: 80%; ">
 			hello hello hello hello hello hello hello hello hello hello
 		</div>
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index c1d114125e..505fc64548 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -1,5 +1,5 @@
-<div class="ui small secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
+<div class="ui small secondary filter menu tw-content-center gt-mx-0">
+	<form class="ui form ignore-dirty tw-flex-1">
 		{{if .PageIsExploreUsers}}
 			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
 		{{else}}
diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl
index fb86fbbea2..e49ca1d069 100644
--- a/templates/explore/user_list.tmpl
+++ b/templates/explore/user_list.tmpl
@@ -1,6 +1,6 @@
 <div class="flex-list">
 	{{range .Users}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-content-center">
 			<div class="flex-item-leading">
 				{{ctx.AvatarUtils.Avatar . 48}}
 			</div>
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 1a55101c2e..c8a0ad3ab0 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -1,13 +1,13 @@
-<div class="ui container gt-df">
+<div class="ui container tw-flex">
 	{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}}
-	<div id="org-info" class="gt-df gt-fc">
+	<div id="org-info" class="tw-flex tw-flex-col">
 		<div class="ui header">
 			{{.Org.DisplayName}}
 			<span class="org-visibility">
 				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
 				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
 			</span>
-			<span class="gt-df gt-ac gt-gap-2 tw-ml-auto gt-font-16 tw-whitespace-nowrap">
+			<span class="tw-flex tw-content-center gt-gap-2 tw-ml-auto gt-font-16 tw-whitespace-nowrap">
 				{{if .EnableFeed}}
 					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
 						{{svg "octicon-rss" 24}}
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index ddd05b4738..04c6a65608 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -25,9 +25,9 @@
 					<div class="divider"></div>
 				{{end}}
 				{{if .NumMembers}}
-					<h4 class="ui top attached header gt-df">
-						<strong class="gt-f1">{{ctx.Locale.Tr "org.members"}}</strong>
-						<a class="text grey gt-df gt-ac" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
+					<h4 class="ui top attached header tw-flex">
+						<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
+						<a class="text grey tw-flex tw-content-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
 					</h4>
 					<div class="ui attached segment members">
 						{{$isMember := .IsOrganizationMember}}
@@ -39,9 +39,9 @@
 					</div>
 				{{end}}
 				{{if .IsOrganizationMember}}
-					<div class="ui top attached header gt-df">
-						<strong class="gt-f1">{{ctx.Locale.Tr "org.teams"}}</strong>
-						<a class="text grey gt-df gt-ac" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
+					<div class="ui top attached header tw-flex">
+						<strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong>
+						<a class="text grey tw-flex tw-content-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
 					</div>
 					<div class="ui attached table segment teams">
 						{{range .Teams}}
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl
index 54f84450eb..cb9e60da29 100644
--- a/templates/org/member/members.tmpl
+++ b/templates/org/member/members.tmpl
@@ -7,7 +7,7 @@
 		<div class="flex-list">
 			{{range .Members}}
 				{{$isPublic := index $.MembersIsPublicMember .ID}}
-				<div class="flex-item {{if $.PublicOnly}}gt-ac{{end}}">
+				<div class="flex-item {{if $.PublicOnly}}tw-content-center{{end}}">
 					<div class="flex-item-leading">
 						<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
 					</div>
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl
index 8eb7b4584e..19a7d5355e 100644
--- a/templates/org/settings/labels.tmpl
+++ b/templates/org/settings/labels.tmpl
@@ -1,7 +1,7 @@
 {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}}
 				<div class="org-setting-content">
-					<div class="gt-df gt-ac">
-						<div class="gt-f1">
+					<div class="tw-flex tw-content-center">
+						<div class="tw-flex-1">
 							{{ctx.Locale.Tr "org.settings.labels_desc"}}
 						</div>
 						<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index 02220a917a..d86aeb7ce4 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -9,7 +9,7 @@
 				{{template "org/team/navbar" .}}
 				{{if .IsOrganizationOwner}}
 					<div class="ui attached segment">
-						<form class="ui form ignore-dirty gt-df gt-fw gt-gap-3" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post">
+						<form class="ui form ignore-dirty tw-flex tw-flex-wrap gt-gap-3" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post">
 							{{.CsrfTokenHtml}}
 							<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
 							<div id="search-user-box" class="ui search gt-mr-3"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
@@ -24,7 +24,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Team.Members}}
-							<div class="flex-item gt-ac">
+							<div class="flex-item tw-content-center">
 								<div class="flex-item-leading">
 									<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
 								</div>
@@ -56,7 +56,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Invites}}
-							<div class="flex-item gt-ac">
+							<div class="flex-item tw-content-center">
 								<div class="flex-item-main">
 									{{.Email}}
 								</div>
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl
index 50ef53b91b..d1e0dbe382 100644
--- a/templates/org/team/new.tmpl
+++ b/templates/org/team/new.tmpl
@@ -78,11 +78,11 @@
 										<tr>
 											<th>{{ctx.Locale.Tr "units.unit"}}</th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.none_access"}}
-											<span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.read_access"}}
-											<span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.write_access"}}
-											<span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
 										</tr>
 									</thead>
 									<tbody>
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl
index bd38cda6d1..9efe8f9f09 100644
--- a/templates/org/team/repositories.tmpl
+++ b/templates/org/team/repositories.tmpl
@@ -9,8 +9,8 @@
 				{{template "org/team/navbar" .}}
 				{{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}}
 				{{if $canAddRemove}}
-					<div class="ui attached segment gt-df gt-fw gt-gap-3">
-						<form class="ui form ignore-dirty gt-f1 gt-df" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post">
+					<div class="ui attached segment tw-flex tw-flex-wrap gt-gap-3">
+						<form class="ui form ignore-dirty tw-flex-1 tw-flex" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post">
 							{{.CsrfTokenHtml}}
 							<div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search">
 								<div class="ui input">
@@ -19,7 +19,7 @@
 							</div>
 							<button class="ui primary button gt-ml-3">{{ctx.Locale.Tr "add"}}</button>
 						</form>
-						<div class="gt-dib">
+						<div class="tw-inline-block">
 							<button class="ui primary button link-action" data-modal-confirm="{{ctx.Locale.Tr "org.teams.add_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/addall">{{ctx.Locale.Tr "add_all"}}</button>
 							<button class="ui red button link-action" data-modal-confirm="{{ctx.Locale.Tr "org.teams.remove_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/removeall">{{ctx.Locale.Tr "remove_all"}}</button>
 						</div>
@@ -28,7 +28,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Team.Repos}}
-							<div class="flex-item gt-ac">
+							<div class="flex-item tw-content-center">
 								<div class="flex-item-leading">
 									{{template "repo/icon" .}}
 								</div>
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 54af71126f..e81a714895 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -90,8 +90,8 @@
 				<a class="tw-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a>
 				<div class="ui relaxed list">
 				{{range .LatestVersions}}
-					<div class="item gt-df">
-						<a class="gt-f1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a>
+					<div class="item tw-flex">
+						<a class="tw-flex-1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a>
 						<span class="text small">{{DateTime "short" .CreatedUnix}}</span>
 					</div>
 				{{end}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index d87e7e0663..f33f9180bb 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -1,5 +1,5 @@
 {{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
-	<div class="gt-df gt-sb gt-mb-4">
+	<div class="tw-flex tw-justify-between gt-mb-4">
 		<div class="small-menu-items ui compact tiny menu list-header-toggle">
 			<a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}">
 				{{svg "octicon-project-symlink" 16 "gt-mr-3"}}
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index a6e84024bc..d36ecdfc85 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -1,7 +1,7 @@
 {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
 
 <div class="ui container">
-	<div class="gt-df gt-sb gt-ac gt-mb-4">
+	<div class="tw-flex tw-justify-between tw-content-center gt-mb-4">
 		<h2 class="gt-mb-0">{{.Project.Title}}</h2>
 		{{if $canWriteProject}}
 			<div class="ui compact mini menu">
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index 55c0494566..f4215829ba 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -25,7 +25,7 @@
 				</div>
 			</div>
 			<div class="twelve wide column content">
-				<div class="ui secondary filter menu gt-je gt-df gt-ac">
+				<div class="ui secondary filter menu tw-justify-end tw-flex tw-content-center">
 					<!-- Actor -->
 					<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
 						<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl
index 580fb08a9e..b898837a26 100644
--- a/templates/repo/actions/runs_list.tmpl
+++ b/templates/repo/actions/runs_list.tmpl
@@ -6,7 +6,7 @@
 	</div>
 	{{end}}
 	{{range .Runs}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-content-center">
 			<div class="flex-item-leading">
 				{{template "repo/actions/status" (dict "status" .Status.String)}}
 			</div>
diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl
index 5016570142..e42eafe8f6 100644
--- a/templates/repo/actions/status.tmpl
+++ b/templates/repo/actions/status.tmpl
@@ -12,7 +12,7 @@
 {{- $className = .className -}}
 {{- end -}}
 
-<span class="gt-df gt-ac" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
+<span class="tw-flex tw-content-center" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
 {{if eq .status "success"}}
 	{{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}}
 {{else if eq .status "skipped"}}
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 4df7b18c44..5bc9a1375e 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -11,11 +11,11 @@
 	{{end}}
 {{end}}
 <div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
-	<h4 class="file-header ui top attached header gt-df gt-ac gt-sb gt-fw">
-		<div class="file-header-left gt-df gt-ac gt-py-3 gt-pr-4">
+	<h4 class="file-header ui top attached header tw-flex tw-content-center tw-justify-between tw-flex-wrap">
+		<div class="file-header-left tw-flex tw-content-center gt-py-3 gt-pr-4">
 			{{template "repo/file_info" .}}
 		</div>
-		<div class="file-header-right file-actions gt-df gt-ac gt-fw">
+		<div class="file-header-right file-actions tw-flex tw-content-center tw-flex-wrap">
 			<div class="ui buttons">
 				<a class="ui tiny button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
 				{{if not .IsViewCommit}}
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 48c14cf343..21121c4f09 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -25,7 +25,7 @@
 									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
 								</div>
-								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
+								<p class="info tw-flex tw-content-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
 							</td>
 							<td class="right aligned middle aligned overflow-visible">
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
@@ -67,8 +67,8 @@
 			</div>
 		{{end}}
 
-		<h4 class="ui top attached header gt-df gt-ac gt-sb">
-			<div class="gt-df gt-ac">
+		<h4 class="ui top attached header tw-flex tw-content-center tw-justify-between">
+			<div class="tw-flex tw-content-center">
 				{{ctx.Locale.Tr "repo.branches"}}
 			</div>
 		</h4>
@@ -98,7 +98,7 @@
 									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
 								</div>
-								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
+								<p class="info tw-flex tw-content-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
 							{{end}}
 							</td>
 							<td class="two wide ui">
@@ -134,7 +134,7 @@
 									</a>
 									{{end}}
 								{{else}}
-									<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
+									<a href="{{.LatestPullRequest.Issue.Link}}" class="tw-align-middle ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
 									{{if .LatestPullRequest.HasMerged}}
 										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
 									{{else if .LatestPullRequest.Issue.IsClosed}}
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index 367c6aab98..328429dd9e 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -70,8 +70,8 @@
 <div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}">
 	{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
 	<div class="ui dropdown custom">
-		<button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0">
-			<span class="text gt-df gt-ac gt-mr-2">
+		<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex gt-m-0">
+			<span class="text tw-flex tw-content-center gt-mr-2">
 				{{if .release}}
 					{{ctx.Locale.Tr "repo.release.compare"}}
 				{{else}}
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index 5e603eae81..4d11d3f603 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -1,6 +1,6 @@
 {{range .RecentlyPushedNewBranches}}
-	<div class="ui positive message gt-df gt-ac">
-		<div class="gt-f1">
+	<div class="ui positive message tw-flex tw-content-center">
+		<div class="tw-flex-1">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
 			{{$branchLink := HTMLFormat `<a href="%s/src/branch/%s">%s</a>` $.RepoLink (PathEscapeSegments .Name) .Name}}
 			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl
index 883230ac29..49f7323845 100644
--- a/templates/repo/commit_load_branches_and_tags.tmpl
+++ b/templates/repo/commit_load_branches_and_tags.tmpl
@@ -7,13 +7,13 @@
 	<div class="branch-and-tag-detail gt-hidden">
 		<div class="divider"></div>
 		<div>{{ctx.Locale.Tr "repo.commit.contained_in"}}</div>
-		<div class="gt-df gt-mt-3">
+		<div class="tw-flex gt-mt-3">
 			<div class="gt-p-2">{{svg "octicon-git-branch"}}</div>
-			<div class="branch-area flex-text-block gt-fw gt-f1"></div>
+			<div class="branch-area flex-text-block tw-flex-wrap tw-flex-1"></div>
 		</div>
-		<div class="gt-df gt-mt-3">
+		<div class="tw-flex gt-mt-3">
 			<div class="gt-p-2">{{svg "octicon-tag"}}</div>
-			<div class="tag-area flex-text-block gt-fw gt-f1"></div>
+			<div class="tag-area flex-text-block tw-flex-wrap tw-flex-1"></div>
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 80af73ce48..1670781e24 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -18,8 +18,8 @@
 			{{end}}
 		{{end}}
 		<div class="ui top attached header clearing segment tw-relative commit-header {{$class}}">
-			<div class="gt-df gt-mb-4 gt-fw">
-				<h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
+			<div class="tw-flex gt-mb-4 tw-flex-wrap">
+				<h3 class="gt-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
 				{{if not $.PageIsWiki}}
 					<div>
 						<a class="ui primary tiny button" href="{{.SourcePath}}">
@@ -139,8 +139,8 @@
 			{{end}}
 			{{template "repo/commit_load_branches_and_tags" .}}
 		</div>
-		<div class="ui attached segment gt-df gt-ac gt-sb gt-py-2 commit-header-row gt-fw {{$class}}">
-				<div class="gt-df gt-ac author">
+		<div class="ui attached segment tw-flex tw-content-center tw-justify-between gt-py-2 commit-header-row tw-flex-wrap {{$class}}">
+				<div class="tw-flex tw-content-center author">
 					{{if .Author}}
 						{{ctx.AvatarUtils.Avatar .Author 28 "gt-mr-3"}}
 						{{if .Author.FullName}}
@@ -164,7 +164,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="ui horizontal list gt-df gt-ac">
+				<div class="ui horizontal list tw-flex tw-content-center">
 					{{if .Parents}}
 						<div class="item">
 							<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
@@ -184,8 +184,8 @@
 				</div>
 		</div>
 		{{if .Commit.Signature}}
-			<div class="ui bottom attached message tw-text-left gt-df gt-ac gt-sb commit-header-row gt-fw gt-mb-0 {{$class}}">
-				<div class="gt-df gt-ac">
+			<div class="ui bottom attached message tw-text-left tw-flex tw-content-center tw-justify-between commit-header-row tw-flex-wrap gt-mb-0 {{$class}}">
+				<div class="tw-flex tw-content-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
 							{{svg "gitea-lock" 16 "gt-mr-3"}}
@@ -209,7 +209,7 @@
 						<span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span>
 					{{end}}
 				</div>
-				<div class="gt-df gt-ac">
+				<div class="tw-flex tw-content-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
 							{{svg "octicon-verified" 16 "gt-mr-3"}}
diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl
index b035e74c2f..f451ac06a1 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 {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
+		<a class="tw-align-middle {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
 			{{template "repo/commit_status" .Status}}
 		</a>
 	{{else}}
-		<span class="gt-vm {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
+		<span class="tw-align-middle {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
 			{{template "repo/commit_status" .Status}}
 		</span>
 	{{end}}
diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl
index 42004c2610..210f73d456 100644
--- a/templates/repo/commits.tmpl
+++ b/templates/repo/commits.tmpl
@@ -4,7 +4,7 @@
 	<div class="ui container">
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
-			<div class="gt-df gt-ac">
+			<div class="tw-flex tw-content-center">
 				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
 				<a href="{{.RepoLink}}/graph" class="ui basic small compact button">
 					{{svg "octicon-git-branch"}}
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index 86e6b7225e..53834f7acb 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -13,7 +13,7 @@
 
 		{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
 
-		<span class="shabox gt-df gt-ac tw-float-right">
+		<span class="shabox tw-flex tw-content-center tw-float-right">
 			{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
 			{{$class := "ui sha label"}}
 			{{if .Signature}}
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index 221ee8d99b..330130ac0d 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -1,5 +1,5 @@
-<h4 class="ui top attached header commits-table gt-df gt-ac gt-sb">
-	<div class="commits-table-left gt-df gt-ac">
+<h4 class="ui top attached header commits-table tw-flex tw-content-center tw-justify-between">
+	<div class="commits-table-left tw-flex tw-content-center">
 		{{if or .PageIsCommits (gt .CommitCount 0)}}
 			{{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}}
 		{{else if .IsNothingToCompare}}
diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl
index 353f6db705..201bff805a 100644
--- a/templates/repo/diff/blob_excerpt.tmpl
+++ b/templates/repo/diff/blob_excerpt.tmpl
@@ -3,7 +3,7 @@
 	<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}">
 		{{if eq .GetType 4}}
 			<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}">
-				<div class="gt-df">
+				<div class="tw-flex">
 				{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
 					<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 						{{svg "octicon-fold-down"}}
@@ -49,7 +49,7 @@
 	<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}">
 		{{if eq .GetType 4}}
 			<td colspan="2" class="lines-num">
-				<div class="gt-df">
+				<div class="tw-flex">
 					{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
 						<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 							{{svg "octicon-fold-down"}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index d2ee5db1b8..d71ad1b2ad 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -1,7 +1,7 @@
 {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
 <div>
 	<div class="diff-detail-box diff-box">
-		<div class="gt-df gt-ac gt-fw gt-gap-3 gt-ml-1">
+		<div class="tw-flex tw-content-center tw-flex-wrap gt-gap-3 gt-ml-1">
 			{{if $showFileTree}}
 				<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
 					{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
@@ -18,14 +18,14 @@
 				</script>
 			{{end}}
 			{{if not .DiffNotAvailable}}
-				<div class="diff-detail-stats gt-df gt-ac gt-fw">
+				<div class="diff-detail-stats tw-flex tw-content-center tw-flex-wrap">
 					{{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}}
 				</div>
 			{{end}}
 		</div>
 		<div class="diff-detail-actions">
 			{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}}
-				<div class="not-mobile gt-df gt-ac gt-fc tw-whitespace-nowrap gt-mr-2">
+				<div class="not-mobile tw-flex tw-content-center tw-flex-col tw-whitespace-nowrap gt-mr-2">
 					<label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{ctx.Locale.Tr "repo.pulls.viewed_files_label"}}">
 						{{ctx.Locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
 					</label>
@@ -110,8 +110,8 @@
 					{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
 					{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
 					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
-						<h4 class="diff-file-header sticky-2nd-row ui top attached normal header gt-df gt-ac gt-sb gt-fw">
-							<div class="diff-file-name gt-df gt-ac gt-gap-2 gt-fw">
+						<h4 class="diff-file-header sticky-2nd-row ui top attached normal header tw-flex tw-content-center tw-justify-between tw-flex-wrap">
+							<div class="diff-file-name tw-flex tw-content-center gt-gap-2 tw-flex-wrap">
 								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
 										{{svg "octicon-chevron-right" 18}}
@@ -119,7 +119,7 @@
 										{{svg "octicon-chevron-down" 18}}
 									{{end}}
 								</button>
-								<div class="gt-font-semibold gt-df gt-ac gt-mono">
+								<div class="gt-font-semibold tw-flex tw-content-center gt-mono">
 									{{if $file.IsBin}}
 										<span class="gt-ml-1 gt-mr-3">
 											{{ctx.Locale.Tr "repo.diff.bin"}}
@@ -144,7 +144,7 @@
 									<span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
 								{{end}}
 							</div>
-							<div class="diff-file-header-actions gt-df gt-ac gt-gap-2 gt-fw">
+							<div class="diff-file-header-actions tw-flex tw-content-center gt-gap-2 tw-flex-wrap">
 								{{if $showFileViewToggle}}
 									<div class="ui compact icon buttons">
 										<button class="ui tiny basic button file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code"}}</button>
@@ -218,7 +218,7 @@
 
 				{{if .Diff.IsIncomplete}}
 					<div class="diff-file-box diff-box file-content gt-mt-3" id="diff-incomplete">
-						<h4 class="ui top attached normal header gt-df gt-ac gt-sb">
+						<h4 class="ui top attached normal header tw-flex tw-content-center tw-justify-between">
 							{{ctx.Locale.Tr "repo.diff.too_many_files"}}
 							<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
 						</h4>
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 227dcc49d0..aed01ef5fb 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -8,8 +8,8 @@
 		{{template "shared/user/avatarlink" dict "user" .Poster}}
 	{{end}}
 	<div class="content comment-container">
-		<div class="ui top attached header comment-header gt-df gt-ac gt-sb">
-			<div class="comment-header-left gt-df gt-ac">
+		<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between">
+			<div class="comment-header-left tw-flex tw-content-center">
 				{{if .OriginalAuthor}}
 					<span class="text black gt-font-semibold gt-mr-2">
 						{{svg (MigrationIcon $.root.Repository.GetOriginalURLHostname)}}
@@ -30,7 +30,7 @@
 					</span>
 				{{end}}
 			</div>
-			<div class="comment-header-right actions gt-df gt-ac">
+			<div class="comment-header-right actions tw-flex tw-content-center">
 				{{if .Invalidated}}
 					{{$referenceUrl := printf "%s#%s" $.root.Issue.Link .HashTag}}
 					<a href="{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index 507f17fd94..1bc018e8e2 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -8,8 +8,8 @@
 	{{$referenceUrl := printf "%s#%s" $.Issue.Link $comment.HashTag}}
 	<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
 		{{if $resolved}}
-			<div class="ui attached header resolved-placeholder gt-df gt-ac gt-sb">
-				<div class="ui grey text gt-df gt-ac gt-fw gt-gap-2">
+			<div class="ui attached header resolved-placeholder tw-flex tw-content-center tw-justify-between">
+				<div class="ui grey text tw-flex tw-content-center tw-flex-wrap gt-gap-2">
 					{{svg "octicon-check" 16 "icon gt-mr-2"}}
 					<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
 					{{if $invalid}}
@@ -22,12 +22,12 @@
 						</a>
 					{{end}}
 				</div>
-				<div class="gt-df gt-ac gt-gap-3">
-					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated gt-df gt-ac">
+				<div class="tw-flex tw-content-center gt-gap-3">
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-content-center">
 						{{svg "octicon-unfold" 16 "gt-mr-3"}}
 						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
 					</button>
-					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated gt-df gt-ac gt-hidden">
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-content-center gt-hidden">
 						{{svg "octicon-fold" 16 "gt-mr-3"}}
 						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
 					</button>
@@ -40,7 +40,7 @@
 					{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
 				</ui>
 			</div>
-			<div class="gt-df gt-je gt-ac gt-fw gt-mt-3">
+			<div class="tw-flex tw-justify-end tw-content-center tw-flex-wrap gt-mt-3">
 				<div class="ui buttons gt-mr-2">
 					<button class="ui icon tiny basic button previous-conversation">
 						{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl
index ae7182c930..9d1eef712d 100644
--- a/templates/repo/diff/new_review.tmpl
+++ b/templates/repo/diff/new_review.tmpl
@@ -1,5 +1,5 @@
 <div id="review-box">
-	<button class="ui tiny primary button gt-pr-2 gt-df js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}>
+	<button class="ui tiny primary button gt-pr-2 tw-flex js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}>
 		{{ctx.Locale.Tr "repo.diff.review"}}
 		<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -10,8 +10,8 @@
 			<form class="ui form form-fetch-action" action="{{.Link}}/reviews/submit" method="post">
 				{{.CsrfTokenHtml}}
 				<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
-				<div class="field gt-df gt-ac">
-					<div class="gt-f1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
+				<div class="field tw-flex tw-content-center">
+					<div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
 					<a class="muted close">{{svg "octicon-x" 16}}</a>
 				</div>
 				<div class="field">
@@ -31,7 +31,7 @@
 				<div class="divider"></div>
 				{{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
 				{{if $showSelfTooltip}}
-					<span class="gt-dib" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
+					<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
 						<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
 					</span>
 				{{else}}
@@ -39,7 +39,7 @@
 				{{end}}
 				<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
 				{{if $showSelfTooltip}}
-					<span class="gt-dib" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
+					<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
 						<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
 					</span>
 				{{else}}
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index 0999d36f20..5af5da09b4 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -16,7 +16,7 @@
 			<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}" data-line-type="{{.GetHTMLDiffLineType}}">
 				{{if eq .GetType 4}}
 					<td class="lines-num lines-num-old">
-						<div class="gt-df">
+						<div class="tw-flex">
 						{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
 							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 								{{svg "octicon-fold-down"}}
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index 2fc116a991..eb51c46d88 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -12,7 +12,7 @@
 			{{if eq .GetType 4}}
 				{{if $.root.AfterCommitID}}
 					<td colspan="2" class="lines-num">
-						<div class="gt-df">
+						<div class="tw-flex">
 							{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
 								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 									{{svg "octicon-fold-down"}}
diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl
index eac6ec2011..de2c34a158 100644
--- a/templates/repo/find/files.tmpl
+++ b/templates/repo/find/files.tmpl
@@ -2,10 +2,10 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="gt-df gt-ac">
+		<div class="tw-flex tw-content-center">
 			<a href="{{$.RepoLink}}">{{.RepoName}}</a>
 			<span class="gt-mx-3">/</span>
-			<div class="ui input gt-f1">
+			<div class="ui input tw-flex-1">
 				<input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}">
 			</div>
 		</div>
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
index b27b55c131..0a4b369cdb 100644
--- a/templates/repo/forks.tmpl
+++ b/templates/repo/forks.tmpl
@@ -6,7 +6,7 @@
 			{{ctx.Locale.Tr "repo.forks"}}
 		</h2>
 		{{range .Forks}}
-			<div class="gt-df gt-ac gt-py-3">
+			<div class="tw-flex tw-content-center gt-py-3">
 				<span class="gt-mr-2">{{ctx.AvatarUtils.Avatar .Owner}}</span>
 				<a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a>
 			</div>
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index 61ef1fe10d..fc7cf925ab 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -28,10 +28,10 @@
 							{{- end -}}
 						</a>
 					</span>
-					<span class="message gt-dib gt-ellipsis gt-mr-3">
+					<span class="message tw-inline-block gt-ellipsis gt-mr-3">
 						<span>{{RenderCommitMessage $.Context $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
 					</span>
-					<span class="commit-refs gt-df gt-ac gt-mr-2">
+					<span class="commit-refs tw-flex tw-content-center gt-mr-2">
 						{{range $commit.Refs}}
 							{{$refGroup := .RefGroup}}
 							{{if eq $refGroup "pull"}}
@@ -58,7 +58,7 @@
 							{{end}}
 						{{end}}
 					</span>
-					<span class="author gt-df gt-ac gt-mr-3">
+					<span class="author tw-flex tw-content-center gt-mr-3">
 						{{$userName := $commit.Commit.Author.Name}}
 						{{if $commit.User}}
 							{{if $commit.User.FullName}}
@@ -71,7 +71,7 @@
 							{{$userName}}
 						{{end}}
 					</span>
-					<span class="time gt-df gt-ac">{{DateTime "full" $commit.Date}}</span>
+					<span class="time tw-flex tw-content-center">{{DateTime "full" $commit.Date}}</span>
 				{{end}}
 			</li>
 		{{end}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index e4d39839fc..b7ee38ae83 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -2,7 +2,7 @@
 {{with .Repository}}
 	<div class="ui container">
 		<div class="repo-header">
-			<div class="flex-item gt-ac">
+			<div class="flex-item tw-content-center">
 				<div class="flex-item-leading">
 					{{template "repo/icon" .}}
 				</div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 24bafb8d9d..7dfc6cfd73 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -27,19 +27,19 @@
 				</div>
 			{{end}}
 		</div>
-		<div class="gt-df gt-ac gt-fw gt-gap-2" id="repo-topics">
+		<div class="tw-flex tw-content-center tw-flex-wrap gt-gap-2" id="repo-topics">
 			{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
 			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg gt-font-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
 		</div>
 		{{end}}
 		{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-		<div class="ui form gt-hidden gt-df gt-fc gt-mt-4" id="topic_edit">
-			<div class="field gt-f1 gt-mb-2">
-				<div class="ui fluid multiple search selection dropdown gt-fw" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
+		<div class="ui form gt-hidden tw-flex tw-flex-col gt-mt-4" id="topic_edit">
+			<div class="field tw-flex-1 gt-mb-2">
+				<div class="ui fluid multiple search selection dropdown tw-flex-wrap" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
 					<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
 					{{range .Topics}}
 						{{/* keey the same layout as Fomantic UI generated labels */}}
-						<a class="ui label transition visible tw-cursor-default gt-dib" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
+						<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
 					{{end}}
 					<div class="text"></div>
 				</div>
@@ -61,7 +61,7 @@
 		{{end}}
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
-			<div class="gt-df gt-ac gt-fw gt-gap-y-3">
+			<div class="tw-flex tw-content-center tw-flex-wrap gt-gap-y-3">
 				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
 				{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 					{{$cmpBranch := ""}}
@@ -121,7 +121,7 @@
 					</span>
 				{{end}}
 			</div>
-			<div class="gt-df gt-ac">
+			<div class="tw-flex tw-content-center">
 				<!-- Only show clone panel in repository home page -->
 				{{if eq $n 0}}
 					<div class="clone-panel ui action tiny input">
diff --git a/templates/repo/icon.tmpl b/templates/repo/icon.tmpl
index a001f81891..e5e0bd68e7 100644
--- a/templates/repo/icon.tmpl
+++ b/templates/repo/icon.tmpl
@@ -1,6 +1,6 @@
 {{$avatarLink := (.RelAvatarLink ctx)}}
 {{if $avatarLink}}
-	<img class="ui avatar gt-vm" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}">
+	<img class="ui avatar tw-align-middle" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}">
 {{else if $.IsMirror}}
 	{{svg "octicon-mirror" 24}}
 {{else if $.IsFork}}
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index b461c5fc98..47c44af9b8 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -7,13 +7,13 @@
 		</div>
 	{{end}}
 	<div class="content gt-p-0 tw-w-full">
-		<div class="gt-df tw-items-start">
+		<div class="tw-flex tw-items-start">
 			<div class="issue-card-icon">
 				{{template "shared/issueicon" .}}
 			</div>
 			<a class="issue-card-title muted issue-title" href="{{.Link}}">{{.Title | RenderEmoji ctx | RenderCodeBlock}}</a>
 			{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
-				<a role="button" class="issue-card-unpin muted gt-df gt-ac" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
+				<a role="button" class="issue-card-unpin muted tw-flex tw-content-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
 					{{svg "octicon-x" 16}}
 				</a>
 			{{end}}
@@ -34,8 +34,8 @@
 		{{if .MilestoneID}}
 		<div class="meta gt-my-2">
 			<a class="milestone" href="{{.Repo.Link}}/milestone/{{.MilestoneID}}">
-				{{svg "octicon-milestone" 16 "gt-mr-2 gt-vm"}}
-				<span class="gt-vm">{{.Milestone.Name}}</span>
+				{{svg "octicon-milestone" 16 "gt-mr-2 tw-align-middle"}}
+				<span class="tw-align-middle">{{.Milestone.Name}}</span>
 			</a>
 		</div>
 		{{end}}
@@ -43,8 +43,8 @@
 		{{range index $.Page.LinkedPRs .ID}}
 		<div class="meta gt-my-2">
 			<a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}">
-				<span class="gt-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "gt-mr-2 gt-vm"}}</span>
-				<span class="gt-vm">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
+				<span class="gt-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "gt-mr-2 tw-align-middle"}}</span>
+				<span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
 			</a>
 		</div>
 		{{end}}
@@ -52,8 +52,8 @@
 		{{$tasks := .GetTasks}}
 		{{if gt $tasks 0}}
 			<div class="meta gt-my-2">
-				{{svg "octicon-checklist" 16 "gt-mr-2 gt-vm"}}
-				<span class="gt-vm">{{.GetTasksDone}} / {{$tasks}}</span>
+				{{svg "octicon-checklist" 16 "gt-mr-2 tw-align-middle"}}
+				<span class="tw-align-middle">{{.GetTasksDone}} / {{$tasks}}</span>
 			</div>
 		{{end}}
 	</div>
diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl
index f573b8e09e..9c259fe0e1 100644
--- a/templates/repo/issue/filter_actions.tmpl
+++ b/templates/repo/issue/filter_actions.tmpl
@@ -29,7 +29,7 @@
 						<div class="divider"></div>
 					{{end}}
 					{{$previousExclusiveScope = $exclusiveScope}}
-					<div class="item issue-action gt-df gt-sb" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels">
+					<div class="item issue-action tw-flex tw-justify-between" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels">
 						{{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context ctx.Locale .}}
 						{{template "repo/issue/labels/label_archived" .}}
 					</div>
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index 696b7db46b..b5c950b121 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -32,7 +32,7 @@
 				<div class="divider"></div>
 			{{end}}
 			{{$previousExclusiveScope = $exclusiveScope}}
-			<a class="item label-filter-item gt-df gt-ac" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
+			<a class="item label-filter-item tw-flex tw-content-center" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
 				{{if .IsExcluded}}
 					{{svg "octicon-circle-slash"}}
 				{{else if .IsSelected}}
@@ -107,7 +107,7 @@
 				{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
 			</div>
 			{{range .OpenProjects}}
-				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item gt-df" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 					{{svg .IconName 18 "gt-mr-3 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
 				</a>
 			{{end}}
@@ -160,7 +160,7 @@
 		<a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a>
 		<div class="divider"></div>
 		{{range .Assignees}}
-			<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item gt-df" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+			<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 				{{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}}
 			</a>
 		{{end}}
diff --git a/templates/repo/issue/label_precolors.tmpl b/templates/repo/issue/label_precolors.tmpl
index 146119b978..80007662c0 100644
--- a/templates/repo/issue/label_precolors.tmpl
+++ b/templates/repo/issue/label_precolors.tmpl
@@ -1,5 +1,5 @@
 <div class="precolors">
-	<div class="gt-df">
+	<div class="tw-flex">
 		<a class="color" style="background-color:#e11d21" data-color-hex="#e11d21"></a>
 		<a class="color" style="background-color:#eb6420" data-color-hex="#eb6420"></a>
 		<a class="color" style="background-color:#fbca04" data-color-hex="#fbca04"></a>
@@ -9,7 +9,7 @@
 		<a class="color" style="background-color:#0052cc" data-color-hex="#0052cc"></a>
 		<a class="color" style="background-color:#5319e7" data-color-hex="#5319e7"></a>
 	</div>
-	<div class="gt-df">
+	<div class="tw-flex">
 		<a class="color" style="background-color:#f6c6c7" data-color-hex="#f6c6c7"></a>
 		<a class="color" style="background-color:#fad8c7" data-color-hex="#fad8c7"></a>
 		<a class="color" style="background-color:#fef2c0" data-color-hex="#fef2c0"></a>
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index ca28e3af2d..86d08e5f75 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -42,9 +42,9 @@
 					<a class="open-issues" href="{{$.RepoLink}}/issues?labels={{.ID}}">{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.label_open_issues" .NumOpenIssues}}</a>
 				{{end}}
 			</div>
-			<div class="label-operation gt-df">
+			<div class="label-operation tw-flex">
 				{{template "repo/issue/labels/label_archived" .}}
-				<div class="gt-df tw-ml-auto">
+				<div class="tw-flex tw-ml-auto">
 					{{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}}
 						<a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a>
 						<a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a>
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index 507c3ce37a..e029bf6031 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -3,10 +3,10 @@
 	{{template "repo/header" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
-		<div class="gt-df">
+		<div class="tw-flex">
 			<h1 class="gt-mb-3">{{.Milestone.Name}}</h1>
 			{{if not .Repository.IsArchived}}
-				<div class="text right gt-f1">
+				<div class="text right tw-flex-1">
 					{{if or .CanWriteIssues .CanWritePulls}}
 						{{if .Milestone.IsClosed}}
 							<a class="ui primary basic button link-action" href data-url="{{$.RepoLink}}/milestones/{{.MilestoneID}}/open">{{ctx.Locale.Tr "repo.milestones.open"}}
@@ -26,10 +26,10 @@
 				{{.Milestone.RenderedContent}}
 		</div>
 		{{end}}
-		<div class="gt-df gt-fc gt-gap-3">
+		<div class="tw-flex tw-flex-col gt-gap-3">
 			<progress class="milestone-progress-big" value="{{.Milestone.Completeness}}" max="100"></progress>
-			<div class="gt-df gt-gap-4">
-				<div classs="gt-df gt-ac">
+			<div class="tw-flex gt-gap-4">
+				<div classs="tw-flex tw-content-center">
 					{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}}
 					{{if .IsClosed}}
 						{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 363ba7e3a2..af7dd70193 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -23,7 +23,7 @@
 							{{svg "octicon-milestone" 16}}
 							<a class="muted" href="{{$.RepoLink}}/milestone/{{.ID}}">{{.Name}}</a>
 						</h3>
-						<div class="gt-df gt-ac">
+						<div class="tw-flex tw-content-center">
 							<span class="gt-mr-3">{{.Completeness}}%</span>
 							<progress value="{{.Completeness}}" max="100"></progress>
 						</div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index b2b9e308f5..ba1e19bf07 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -171,7 +171,7 @@
 				<div class="selected">
 				{{range .Assignees}}
 					<a class="item gt-p-2 muted gt-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
-						{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3 gt-vm"}}{{.GetDisplayName}}
+						{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3 tw-align-middle"}}{{.GetDisplayName}}
 					</a>
 				{{end}}
 				</div>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 89520ebe65..ff06d8c5bd 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -20,8 +20,8 @@
 				</a>
 				{{end}}
 				<div class="content comment-container">
-					<div class="ui top attached header comment-header gt-df gt-ac gt-sb" role="heading" aria-level="3">
-						<div class="comment-header-left gt-df gt-ac">
+					<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between" role="heading" aria-level="3">
+						<div class="comment-header-left tw-flex tw-content-center">
 							{{if .Issue.OriginalAuthor}}
 								<span class="text black gt-font-semibold">
 									{{svg (MigrationIcon .Repository.GetOriginalURLHostname)}}
@@ -43,7 +43,7 @@
 								</span>
 							{{end}}
 						</div>
-						<div class="comment-header-right actions gt-df gt-ac">
+						<div class="comment-header-right actions tw-flex tw-content-center">
 							{{template "repo/issue/view_content/show_role" dict "ShowRole" .Issue.ShowRole "IgnorePoster" true}}
 							{{if not $.Repository.IsArchived}}
 								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
index 2c3a47d670..151131366f 100644
--- a/templates/repo/issue/view_content/attachments.tmpl
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -4,8 +4,8 @@
 	{{end}}
 	{{$hasThumbnails := false}}
 	{{- range .Attachments -}}
-		<div class="gt-df">
-			<div class="gt-f1 gt-p-3">
+		<div class="tw-flex">
+			<div class="tw-flex-1 gt-p-3">
 				<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					{{if FilenameIsImage .Name}}
 						{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
@@ -18,7 +18,7 @@
 					<span><strong>{{.Name}}</strong></span>
 				</a>
 			</div>
-			<div class="gt-p-3 gt-df gt-ac">
+			<div class="gt-p-3 tw-flex tw-content-center">
 				<span class="ui text grey">{{.Size | FileSize}}</span>
 			</div>
 		</div>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 6654224320..2e2ce0fc28 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -25,8 +25,8 @@
 				</a>
 			{{end}}
 				<div class="content comment-container">
-					<div class="ui top attached header comment-header gt-df gt-ac gt-sb" role="heading" aria-level="3">
-						<div class="comment-header-left gt-df gt-ac">
+					<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between" role="heading" aria-level="3">
+						<div class="comment-header-left tw-flex tw-content-center">
 							{{if .OriginalAuthor}}
 								<span class="text black gt-font-semibold gt-mr-2">
 									{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
@@ -50,7 +50,7 @@
 								</span>
 							{{end}}
 						</div>
-						<div class="comment-header-right actions gt-df gt-ac">
+						<div class="comment-header-right actions tw-flex tw-content-center">
 							{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 							{{if not $.Repository.IsArchived}}
 								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -402,8 +402,8 @@
 				{{if or .Content .Attachments}}
 				<div class="timeline-item comment">
 					<div class="content comment-container">
-						<div class="ui top attached header comment-header gt-df gt-ac gt-sb">
-							<div class="comment-header-left gt-df gt-ac">
+						<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between">
+							<div class="comment-header-left tw-flex tw-content-center">
 								{{if gt .Poster.ID 0}}
 									<a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}">
 										{{ctx.AvatarUtils.Avatar .Poster 24}}
@@ -424,7 +424,7 @@
 									{{ctx.Locale.Tr "repo.issues.review.left_comment"}}
 								</span>
 							</div>
-							<div class="comment-header-right actions gt-df gt-ac">
+							<div class="comment-header-right actions tw-flex tw-content-center">
 								{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 								{{if not $.Repository.IsArchived}}
 									{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -623,7 +623,7 @@
 				{{if .Content}}
 					<div class="timeline-item comment">
 						<div class="content">
-							<div class="ui top attached header comment-header-left gt-df gt-ac arrow-top">
+							<div class="ui top attached header comment-header-left tw-flex tw-content-center arrow-top">
 								{{if gt .Poster.ID 0}}
 									<a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}">
 										{{ctx.AvatarUtils.Avatar .Poster 24}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index d93589539c..83c8fc6ca3 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -6,8 +6,8 @@
 	{{$hasReview := and $comment.Review}}
 	{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
 	<div class="ui segments conversation-holder">
-		<div class="ui segment collapsible-comment-box gt-py-3 gt-df gt-ac gt-sb">
-			<div class="gt-df gt-ac">
+		<div class="ui segment collapsible-comment-box gt-py-3 tw-flex tw-content-center tw-justify-between">
+			<div class="tw-flex tw-content-center">
 				<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment gt-ml-3 gt-word-break">{{$comment.TreePath}}</a>
 				{{if $invalid}}
 					<span class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
@@ -17,7 +17,7 @@
 			</div>
 			<div>
 				{{if or $invalid $resolved}}
-					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated gt-df gt-ac">
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-content-center">
 						{{svg "octicon-unfold" 16 "gt-mr-3"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
@@ -25,7 +25,7 @@
 							{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
 						{{end}}
 					</button>
-					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated gt-df gt-ac">
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-content-center">
 						{{svg "octicon-fold" 16 "gt-mr-3"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
@@ -58,7 +58,7 @@
 					<div class="comment code-comment gt-pb-4" id="{{.HashTag}}">
 						<div class="content">
 							<div class="header comment-header">
-								<div class="comment-header-left gt-df gt-ac">
+								<div class="comment-header-left tw-flex tw-content-center">
 									{{if not .OriginalAuthor}}
 										<a class="avatar">
 											{{ctx.AvatarUtils.Avatar .Poster 20}}
@@ -79,7 +79,7 @@
 										{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdSubStr}}
 									</span>
 								</div>
-								<div class="comment-header-right actions gt-df gt-ac">
+								<div class="comment-header-right actions tw-flex tw-content-center">
 									{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 									{{if not $.Repository.IsArchived}}
 										{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -109,8 +109,8 @@
 					</div>
 				{{end}}
 			</div>
-			<div class="code-comment-buttons gt-df gt-ac gt-fw gt-mt-3 gt-mb-2 gt-mx-3">
-				<div class="gt-f1">
+			<div class="code-comment-buttons tw-flex tw-content-center tw-flex-wrap gt-mt-3 gt-mb-2 gt-mx-3">
+				<div class="tw-flex-1">
 					{{if $resolved}}
 						<div class="ui grey text">
 							{{svg "octicon-check" 16 "gt-mr-2"}}
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index aac30180df..9fafeb5ee3 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -33,7 +33,7 @@
 		<div class="ui attached merge-section segment {{if not $.LatestCommitStatus}}no-header{{end}} flex-items-block">
 			{{if .Issue.PullRequest.HasMerged}}
 				{{if .IsPullBranchDeletable}}
-					<div class="item item-section text gt-f1">
+					<div class="item item-section text tw-flex-1">
 						<div class="item-section-left">
 							<h3 class="gt-mb-3">
 								{{ctx.Locale.Tr "repo.pulls.merged_success"}}
@@ -48,7 +48,7 @@
 					</div>
 				{{end}}
 			{{else if .Issue.IsClosed}}
-				<div class="item item-section text gt-f1">
+				<div class="item item-section text tw-flex-1">
 					<div class="item-section-left">
 						<h3 class="gt-mb-3">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3>
 						<div class="merge-section-info">
@@ -82,7 +82,7 @@
 				</div>
 			{{else if .IsPullWorkInProgress}}
 				<div class="item toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title">
-					<div class="item-section-left flex-text-inline gt-f1">
+					<div class="item-section-left flex-text-inline tw-flex-1">
 						{{svg "octicon-x"}}
 						{{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}}
 					</div>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 329a39dd69..b8867c11e7 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -3,7 +3,7 @@
 	{{if .Issue.IsPull}}
 		<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
 		<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
-			<a class="text gt-df gt-ac muted">
+			<a class="text tw-flex tw-content-center muted">
 				<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
 				{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
 					{{svg "octicon-gear" 16 "gt-ml-2"}}
@@ -50,17 +50,17 @@
 			<span class="no-select item {{if or .OriginalReviews .PullReviewers}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
 			<div class="selected">
 				{{range .PullReviewers}}
-					<div class="item gt-df gt-ac gt-py-3">
-						<div class="gt-df gt-ac gt-f1">
+					<div class="item tw-flex tw-content-center gt-py-3">
+						<div class="tw-flex tw-content-center tw-flex-1">
 							{{if .User}}
 								<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "gt-mr-3"}}{{.User.GetDisplayName}}</a>
 							{{else if .Team}}
 								<span class="text">{{svg "octicon-people" 20 "gt-mr-3"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
 							{{end}}
 						</div>
-						<div class="gt-df gt-ac gt-gap-3">
+						<div class="tw-flex tw-content-center gt-gap-3">
 							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
-								<a href="#" class="ui muted icon gt-df gt-ac show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
+								<a href="#" class="ui muted icon tw-flex tw-content-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
 									{{svg "octicon-x" 20}}
 								</a>
 								<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
@@ -99,14 +99,14 @@
 					</div>
 				{{end}}
 				{{range .OriginalReviews}}
-					<div class="item gt-df gt-ac gt-py-3">
-						<div class="gt-df gt-ac gt-f1">
+					<div class="item tw-flex tw-content-center gt-py-3">
+						<div class="tw-flex tw-content-center tw-flex-1">
 							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
 								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "gt-mr-3"}}
 								{{.OriginalAuthor}}
 							</a>
 						</div>
-						<div class="gt-df gt-ac gt-gap-3">
+						<div class="tw-flex tw-content-center gt-gap-3">
 							{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
 						</div>
 					</div>
@@ -257,7 +257,7 @@
 
 	{{if .Participants}}
 		<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}}</strong></span>
-		<div class="ui list gt-df gt-fw">
+		<div class="ui list tw-flex tw-flex-wrap">
 			{{range .Participants}}
 				<a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}">
 					{{ctx.AvatarUtils.Avatar . 28 "gt-my-1 gt-mr-2"}}
@@ -361,7 +361,7 @@
 		</div>
 		{{if ne .Issue.DeadlineUnix 0}}
 			<p>
-				<div class="gt-df gt-sb gt-ac">
+				<div class="tw-flex tw-justify-between tw-content-center">
 					<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
 						{{svg "octicon-calendar" 16 "gt-mr-3"}}
 						{{DateTime "long" .Issue.DeadlineUnix.FormatDate}}
@@ -417,8 +417,8 @@
 				</span>
 				<div class="ui relaxed divided list">
 					{{range .BlockingDependencies}}
-						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
-							<div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis">
+						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-content-center tw-justify-between">
+							<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 								<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 									#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
 								</a>
@@ -426,7 +426,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right gt-df gt-ac gt-m-2">
+							<div class="item-right tw-flex tw-content-center gt-m-2">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -436,7 +436,7 @@
 						</div>
 					{{end}}
 					{{if .BlockingDependenciesNotPermitted}}
-						<div class="item gt-df gt-ac gt-sb gt-ellipsis">
+						<div class="item tw-flex tw-content-center tw-justify-between gt-ellipsis">
 							<span>{{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span>
 						</div>
 					{{end}}
@@ -449,8 +449,8 @@
 				</span>
 				<div class="ui relaxed divided list">
 					{{range .BlockedByDependencies}}
-						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
-							<div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis">
+						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-content-center tw-justify-between">
+							<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 								<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 									#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
 								</a>
@@ -458,7 +458,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right gt-df gt-ac gt-m-2">
+							<div class="item-right tw-flex tw-content-center gt-m-2">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -469,8 +469,8 @@
 					{{end}}
 					{{if $.CanCreateIssueDependencies}}
 						{{range .BlockedByDependenciesNotPermitted}}
-							<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
-								<div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis">
+							<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-content-center tw-justify-between">
+								<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 									<div class="gt-ellipsis">
 										<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
 										<span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
@@ -481,7 +481,7 @@
 										{{.Repository.OwnerName}}/{{.Repository.Name}}
 									</div>
 								</div>
-								<div class="item-right gt-df gt-ac gt-m-2">
+								<div class="item-right tw-flex tw-content-center gt-m-2">
 									{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 										<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 											{{svg "octicon-trash" 16}}
@@ -491,7 +491,7 @@
 							</div>
 						{{end}}
 					{{else if .BlockedByDependenciesNotPermitted}}
-						<div class="item gt-df gt-ac gt-sb gt-ellipsis">
+						<div class="item tw-flex tw-content-center tw-justify-between gt-ellipsis">
 							<span>{{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span>
 						</div>
 					{{end}}
@@ -548,7 +548,7 @@
 	<div class="divider"></div>
 	<div class="ui equal width compact grid">
 		{{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
-		<div class="row gt-ac" data-tooltip-content="{{$issueReferenceLink}}">
+		<div class="row tw-content-center" data-tooltip-content="{{$issueReferenceLink}}">
 			<span class="text column truncate">{{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}</span>
 			<button class="ui two wide button column gt-p-3" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
 		</div>
diff --git a/templates/repo/issue/view_content/update_branch_by_merge.tmpl b/templates/repo/issue/view_content/update_branch_by_merge.tmpl
index 4dbefefe00..adce052dee 100644
--- a/templates/repo/issue/view_content/update_branch_by_merge.tmpl
+++ b/templates/repo/issue/view_content/update_branch_by_merge.tmpl
@@ -7,7 +7,7 @@
 		</div>
 		<div class="item-section-right">
 			{{if and $.UpdateAllowed $.UpdateByRebaseAllowed}}
-				<div class="gt-dib">
+				<div class="tw-inline-block">
 					<div class="ui buttons update-button">
 						<button class="ui button" data-do="{{$.Link}}/update" data-redirect="{{$.Link}}">
 							<span class="button-text">
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 370826e0fd..4b5bf2ec0a 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -8,7 +8,7 @@
 		<h1 class="gt-word-break">
 			<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
 </span>
-			<div id="edit-title-input" class="ui input gt-f1 gt-hidden">
+			<div id="edit-title-input" class="ui input tw-flex-1 gt-hidden">
 				<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
 			</div>
 		</h1>
diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl
index 8ba567ee6b..c0336b9b97 100644
--- a/templates/repo/migrate/migrate.tmpl
+++ b/templates/repo/migrate/migrate.tmpl
@@ -5,7 +5,7 @@
 			{{template "repo/migrate/helper" .}}
 			<div class="ui cards migrate-entries">
 				{{range .Services}}
-					<a class="ui card migrate-entry gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
+					<a class="ui card migrate-entry tw-flex tw-content-center" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
 						{{if eq .Name "github"}}
 							{{svg "octicon-mark-github" 184 "gt-p-4"}}
 						{{else if eq .Name "gitlab"}}
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl
index 377a7ff79f..b227ce4439 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 	{{template "repo/header" .}}
 	<div class="ui container padded">
-		<div class="gt-df gt-sb gt-ac gt-mb-4">
+		<div class="tw-flex tw-justify-between tw-content-center gt-mb-4">
 			{{template "repo/issue/navbar" .}}
 			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 		</div>
diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl
index f0907f409b..2cf0a85fc8 100644
--- a/templates/repo/pulls/fork.tmpl
+++ b/templates/repo/pulls/fork.tmpl
@@ -37,7 +37,7 @@
 
 					<div class="inline field">
 						<label>{{ctx.Locale.Tr "repo.fork_from"}}</label>
-						<a href="{{.ForkRepo.Link}}" class="gt-dib">{{.ForkRepo.FullName}}</a>
+						<a href="{{.ForkRepo.Link}}" class="tw-inline-block">{{.ForkRepo.FullName}}</a>
 					</div>
 					<div class="inline required field {{if .Err_RepoName}}error{{end}}">
 						<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index 340b1bb397..fb00acde32 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -15,7 +15,7 @@
 			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
-		<span class="item tw-ml-auto gt-pr-0 gt-font-bold gt-df gt-ac gt-gap-3">
+		<span class="item tw-ml-auto gt-pr-0 gt-font-bold tw-flex tw-content-center gt-gap-3">
 			<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
 			<span class="diff-stats-bar">
 				<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 873cccab79..29059ea4a4 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -16,10 +16,10 @@
 						{{end}}
 					</div>
 					<div class="ui twelve wide column detail">
-						<div class="gt-df gt-ac gt-sb gt-fw gt-mb-3">
+						<div class="tw-flex tw-content-center tw-justify-between tw-flex-wrap gt-mb-3">
 							<h4 class="release-list-title gt-word-break">
 								{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>{{end}}
-								{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "gt-df"}}
+								{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}}
 								{{if $release.IsDraft}}
 									<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
 								{{else if $release.IsPrerelease}}
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index 30e783167c..fd6338a701 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -21,7 +21,7 @@
 					{{else}}
 						<input id="tag-name" name="tag_name" value="{{.tag_name}}" aria-label="{{ctx.Locale.Tr "repo.release.tag_name"}}" placeholder="{{ctx.Locale.Tr "repo.release.tag_name"}}" autofocus required maxlength="255">
 						<input id="tag-name-editor" type="hidden" data-existing-tags="{{JsonUtils.EncodeToString .Tags}}" data-tag-helper="{{ctx.Locale.Tr "repo.release.tag_helper"}}" data-tag-helper-new="{{ctx.Locale.Tr "repo.release.tag_helper_new"}}" data-tag-helper-existing="{{ctx.Locale.Tr "repo.release.tag_helper_existing"}}">
-						<div id="tag-target-selector" class="gt-dib">
+						<div id="tag-target-selector" class="tw-inline-block">
 							<span class="at">@</span>
 							<div class="ui selection dropdown">
 								<input type="hidden" name="tag_target" value="{{.tag_target}}">
@@ -61,7 +61,7 @@
 				</div>
 				{{range .attachments}}
 					<div class="field flex-text-block" id="attachment-{{.ID}}">
-						<div class="flex-text-inline gt-f1">
+						<div class="flex-text-inline tw-flex-1">
 							<input name="attachment-edit-{{.UUID}}"  class="attachment_edit" required value="{{.Name}}">
 							<input name="attachment-del-{{.UUID}}" type="hidden" value="false">
 							<span class="ui text grey tw-whitespace-nowrap">{{.Size | FileSize}}</span>
@@ -101,7 +101,7 @@
 					</div>
 					<span class="help">{{ctx.Locale.Tr "repo.release.prerelease_helper"}}</span>
 					<div class="divider gt-mt-0"></div>
-					<div class="gt-df gt-je">
+					<div class="tw-flex tw-justify-end">
 						{{if .PageIsEditRelease}}
 							<a class="ui small button" href="{{.RepoLink}}/releases">
 								{{ctx.Locale.Tr "repo.release.cancel"}}
diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl
index 8e6088790d..18a3c8c6b8 100644
--- a/templates/repo/release_tag_header.tmpl
+++ b/templates/repo/release_tag_header.tmpl
@@ -2,8 +2,8 @@
 {{$canReadCode := $.Permission.CanRead $.UnitTypeCode}}
 
 {{if $canReadReleases}}
-	<div class="gt-df">
-		<div class="gt-f1 gt-df gt-ac">
+	<div class="tw-flex">
+		<div class="tw-flex-1 tw-flex tw-content-center">
 			<h2 class="ui compact small menu header small-menu-items">
 				<a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
 				{{if $canReadCode}}
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index 73aff887f3..2610ae02fe 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -12,11 +12,11 @@
 				<p>
 					{{ctx.Locale.Tr "repo.settings.default_branch_desc"}}
 				</p>
-				<form class="gt-df" action="{{.Link}}" method="post">
+				<form class="tw-flex" action="{{.Link}}" method="post">
 					{{.CsrfTokenHtml}}
 					<input type="hidden" name="action" value="default_branch">
 					{{if not .Repository.IsEmpty}}
-						<div class="ui dropdown selection search gt-f1 gt-mr-3 tw-max-w-96">
+						<div class="ui dropdown selection search tw-flex-1 gt-mr-3 tw-max-w-96">
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 							<input type="hidden" name="branch" value="{{.Repository.DefaultBranch}}">
 							<div class="default text">{{.Repository.DefaultBranch}}</div>
@@ -41,7 +41,7 @@
 			<div class="ui attached segment">
 				<div class="flex-list">
 					{{range .ProtectedBranches}}
-						<div class="flex-item gt-ac">
+						<div class="flex-item tw-content-center">
 							<div class="flex-item-main">
 								<div class="flex-item-title">
 									<div class="ui basic primary label">{{.RuleName}}</div>
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index d7b5c96bab..8783de2544 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -7,7 +7,7 @@
 		<div class="ui attached segment">
 			<div class="flex-list">
 				{{range .Collaborators}}
-					<div class="flex-item gt-ac">
+					<div class="flex-item tw-content-center">
 						<div class="flex-item-leading">
 							<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
 						</div>
@@ -41,7 +41,7 @@
 		<div class="ui bottom attached segment">
 			<form class="ui form" id="repo-collab-form" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
-				<div id="search-user-box" class="ui search input gt-vm">
+				<div id="search-user-box" class="ui search input tw-align-middle">
 					<input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
 				</div>
 				<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_collaborator"}}</button>
@@ -89,7 +89,7 @@
 			{{if $allowedToChangeTeams}}
 				<form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post">
 					{{.CsrfTokenHtml}}
-					<div id="search-team-box" class="ui search input gt-vm" data-org-name="{{.OrgName}}">
+					<div id="search-team-box" class="ui search input tw-align-middle" data-org-name="{{.OrgName}}">
 						<input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
 					</div>
 					<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_team"}}</button>
diff --git a/templates/repo/settings/githooks.tmpl b/templates/repo/settings/githooks.tmpl
index 3fce29d545..3d15d097cc 100644
--- a/templates/repo/settings/githooks.tmpl
+++ b/templates/repo/settings/githooks.tmpl
@@ -11,7 +11,7 @@
 				{{range .Hooks}}
 					<div class="item truncated-item-container">
 						<span class="text {{if .IsActive}}green{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span>
-						<span class="text truncate gt-f1 gt-mr-3">{{.Name}}</span>
+						<span class="text truncate tw-flex-1 gt-mr-3">{{.Name}}</span>
 						<a class="muted tw-float-right gt-p-3" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}">
 							{{svg "octicon-pencil"}}
 						</a>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 604e5d4152..aa40a1cd1b 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -132,7 +132,7 @@
 								<td>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}</td>
 								<td>{{DateTime "full" .PullMirror.UpdatedUnix}}</td>
 								<td class="right aligned">
-									<form method="post" class="gt-dib">
+									<form method="post" class="tw-inline-block">
 										{{.CsrfTokenHtml}}
 										<input type="hidden" name="action" value="mirror-sync">
 										<button class="ui primary tiny button inline text-thin">{{ctx.Locale.Tr "repo.settings.sync_mirror"}}</button>
@@ -230,13 +230,13 @@
 									>
 										{{svg "octicon-pencil" 14}}
 									</button>
-									<form method="post" class="gt-dib">
+									<form method="post" class="tw-inline-block">
 										{{$.CsrfTokenHtml}}
 										<input type="hidden" name="action" value="push-mirror-sync">
 										<input type="hidden" name="push_mirror_id" value="{{.ID}}">
 										<button class="ui primary tiny button" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.sync_mirror"}}">{{svg "octicon-sync" 14}}</button>
 									</form>
-									<form method="post" class="gt-dib">
+									<form method="post" class="tw-inline-block">
 										{{$.CsrfTokenHtml}}
 										<input type="hidden" name="action" value="push-mirror-remove">
 										<input type="hidden" name="push_mirror_id" value="{{.ID}}">
@@ -837,7 +837,7 @@
 					</div>
 				</div>
 				{{if not .Repository.IsMirror}}
-					<div class="flex-item gt-ac">
+					<div class="flex-item tw-content-center">
 						<div class="flex-item-main">
 							{{if .Repository.IsArchived}}
 								<div class="flex-item-title">{{ctx.Locale.Tr "repo.settings.unarchive.header"}}</div>
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl
index 4c196f0f99..c9efb7b67e 100644
--- a/templates/repo/settings/tags.tmpl
+++ b/templates/repo/settings/tags.tmpl
@@ -106,7 +106,7 @@
 										</td>
 										<td class="right aligned">
 											<a class="ui tiny primary button" href="{{$.RepoLink}}/settings/tags/{{.ID}}">{{ctx.Locale.Tr "edit"}}</a>
-											<form class="gt-dib" action="{{$.RepoLink}}/settings/tags/delete" method="post">
+											<form class="tw-inline-block" action="{{$.RepoLink}}/settings/tags/delete" method="post">
 												{{$.CsrfTokenHtml}}
 												<input type="hidden" name="id" value="{{.ID}}">
 												<button class="ui tiny red button">{{ctx.Locale.Tr "remove"}}</button>
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index e56929b70f..9abc03e40e 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -15,7 +15,7 @@
 		{{range .Webhooks}}
 			<div class="item truncated-item-container">
 				<span class="text {{if eq .LastStatus 1}}green{{else if eq .LastStatus 2}}red{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span>
-				<div class="text truncate gt-f1 gt-mr-3">
+				<div class="text truncate tw-flex-1 gt-mr-3">
 					<a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a>
 				</div>
 				<a class="muted gt-p-3" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl
index 9f7a7816ea..e2aee13941 100644
--- a/templates/repo/settings/webhook/history.tmpl
+++ b/templates/repo/settings/webhook/history.tmpl
@@ -17,7 +17,7 @@
 		<div class="ui list">
 			{{range .History}}
 				<div class="item">
-					<div class="flex-text-block gt-sb">
+					<div class="flex-text-block tw-justify-between">
 						<div class="flex-text-inline">
 							{{if .IsSucceed}}
 								<span class="text green">{{svg "octicon-check"}}</span>
diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl
index 9f0676e395..0348334623 100644
--- a/templates/repo/tag/list.tmpl
+++ b/templates/repo/tag/list.tmpl
@@ -5,7 +5,7 @@
 		{{template "base/alert" .}}
 		{{template "repo/release_tag_header" .}}
 		<h4 class="ui top attached header">
-			<div class="five wide column gt-df gt-ac">
+			<div class="five wide column tw-flex tw-content-center">
 				{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.tags"}}
 			</div>
 		</h4>
@@ -18,12 +18,12 @@
 							<td class="tag">
 								<h3 class="release-tag-name gt-mb-3">
 									{{if $canReadReleases}}
-										<a class="gt-df gt-ac" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
+										<a class="tw-flex tw-content-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
 									{{else}}
-										<a class="gt-df gt-ac" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
+										<a class="tw-flex tw-content-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
 									{{end}}
 								</h3>
-								<div class="download gt-df gt-ac">
+								<div class="download tw-flex tw-content-center">
 									{{if $.Permission.CanRead $.UnitTypeCode}}
 										{{if .CreatedUnix}}
 											<span class="gt-mr-3">{{svg "octicon-clock" 16 "gt-mr-2"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 8c1e7982eb..d8ef710400 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -25,8 +25,8 @@
 		</div>
 	{{end}}
 
-	<h4 class="file-header ui top attached header gt-df gt-ac gt-sb gt-fw">
-		<div class="file-header-left gt-df gt-ac gt-py-3 gt-pr-4">
+	<h4 class="file-header ui top attached header tw-flex tw-content-center tw-justify-between tw-flex-wrap">
+		<div class="file-header-left tw-flex tw-content-center gt-py-3 gt-pr-4">
 			{{if .ReadmeInList}}
 				{{svg "octicon-book" 16 "gt-mr-3"}}
 				<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong>
@@ -34,7 +34,7 @@
 				{{template "repo/file_info" .}}
 			{{end}}
 		</div>
-		<div class="file-header-right file-actions gt-df gt-ac gt-fw">
+		<div class="file-header-right file-actions tw-flex tw-content-center tw-flex-wrap">
 			{{if .HasSourceRenderedToggle}}
 				<div class="ui compact icon buttons">
 					<a href="?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl
index 411c7fc869..d1abd27342 100644
--- a/templates/repo/wiki/new.tmpl
+++ b/templates/repo/wiki/new.tmpl
@@ -3,7 +3,7 @@
 	{{template "repo/header" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
-		<div class="ui header flex-text-block gt-sb">
+		<div class="ui header flex-text-block tw-justify-between">
 			{{ctx.Locale.Tr "repo.wiki.new_page"}}
 			{{if .PageIsWikiEdit}}
 				<a class="ui tiny primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl
index cace0d48e0..fa7352e510 100644
--- a/templates/repo/wiki/pages.tmpl
+++ b/templates/repo/wiki/pages.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository wiki pages">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<h2 class="ui header gt-df gt-ac gt-sb">
+		<h2 class="ui header tw-flex tw-content-center tw-justify-between">
 			<span>{{ctx.Locale.Tr "repo.wiki.pages"}}</span>
 			<span>
 				{{if and .CanWriteWiki (not .Repository.IsMirror)}}
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index fefa9c589e..aa05a97fb0 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -4,7 +4,7 @@
 	{{$title := .title}}
 	<div class="ui container">
 		<div class="repo-button-row">
-			<div class="gt-df gt-ac">
+			<div class="tw-flex tw-content-center">
 				<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 					<div class="ui basic small button">
 						<span class="text">
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index fbc730b288..f8bbf23b62 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -7,15 +7,15 @@
 			{{template "base/disable_form_autofill"}}
 			{{.CsrfTokenHtml}}
 			<div class="runner-basic-info">
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block gt-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.status"}}</label>
 					<span class="ui {{if .Runner.IsOnline}}green{{else}}basic{{end}} label">{{.Runner.StatusLocaleName ctx.Locale}}</span>
 				</div>
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block gt-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.last_online"}}</label>
 					<span>{{if .Runner.LastOnline}}{{TimeSinceUnix .Runner.LastOnline ctx.Locale}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</span>
 				</div>
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block gt-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.labels"}}</label>
 					<span>
 						{{range .Runner.AgentLabels}}
@@ -23,7 +23,7 @@
 						{{end}}
 					</span>
 				</div>
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block gt-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.owner_type"}}</label>
 					<span data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</span>
 				</div>
diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl
index 68b183e9bf..3bae959006 100644
--- a/templates/shared/search/code/results.tmpl
+++ b/templates/shared/search/code/results.tmpl
@@ -1,4 +1,4 @@
-<div class="flex-text-block gt-fw">
+<div class="flex-text-block tw-flex-wrap">
 	{{range $term := .SearchResultLanguages}}
 	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0"
 		href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&fuzzy={{$.IsFuzzy}}">
@@ -12,9 +12,9 @@
 	{{range $result := .SearchResults}}
 		{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
 		<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
-			<h4 class="ui top attached normal header gt-df gt-fw">
+			<h4 class="ui top attached normal header tw-flex tw-flex-wrap">
 				{{if not $.Repo}}
-					<span class="file gt-f1">
+					<span class="file tw-flex-1">
 						<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>
 						{{if $repo.IsArchived}}
 							<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
@@ -22,7 +22,7 @@
 						- {{.Filename}}
 					</span>
 				{{else}}
-					<span class="file gt-f1">{{.Filename}}</span>
+					<span class="file tw-flex-1">{{.Filename}}</span>
 				{{end}}
 				<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
 			</h4>
diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl
index b123b497c7..b920e10bb2 100644
--- a/templates/shared/searchbottom.tmpl
+++ b/templates/shared/searchbottom.tmpl
@@ -1,5 +1,5 @@
-<div class="ui bottom attached table segment gt-df gt-ac gt-sb">
-		<div class="gt-df gt-ac gt-ml-4">
+<div class="ui bottom attached table segment tw-flex tw-content-center tw-justify-between">
+		<div class="tw-flex tw-content-center gt-ml-4">
 			{{if .result.Language}}
 					<i class="color-icon gt-mr-3" style="background-color: {{.result.Color}}"></i>{{.result.Language}}
 			{{end}}
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index 4fbd8ddcfd..ea36d0cec2 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -14,7 +14,7 @@
 	{{if .Secrets}}
 	<div class="flex-list">
 		{{range .Secrets}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-content-center">
 			<div class="flex-item-leading">
 				{{svg "octicon-key" 32}}
 			</div>
diff --git a/templates/shared/user/org_profile_avatar.tmpl b/templates/shared/user/org_profile_avatar.tmpl
index a8846b0abd..07e7b8aed5 100644
--- a/templates/shared/user/org_profile_avatar.tmpl
+++ b/templates/shared/user/org_profile_avatar.tmpl
@@ -2,7 +2,7 @@
 	<div class="ui container">
 		<div class="ui vertically grid head">
 			<div class="column">
-				<div class="ui header gt-df gt-ac gt-word-break">
+				<div class="ui header tw-flex tw-content-center gt-word-break">
 					{{ctx.AvatarUtils.Avatar . 100}}
 					<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
 					<span class="org-visibility">
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index a168e6903e..3e1cacd9ba 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -1,5 +1,5 @@
 <div id="profile-avatar-card" class="ui card">
-	<div id="profile-avatar" class="content gt-df">
+	<div id="profile-avatar" class="content tw-flex">
 	{{if eq .SignedUserID .ContextUser.ID}}
 		<a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{ctx.Locale.Tr "user.change_avatar"}}">
 			{{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}}
@@ -36,7 +36,7 @@
 			{{if .ContextUser.Location}}
 				<li>
 					{{svg "octicon-location"}}
-					<span class="gt-f1">{{.ContextUser.Location}}</span>
+					<span class="tw-flex-1">{{.ContextUser.Location}}</span>
 					{{if .ContextUserLocationMapURL}}
 						<a href="{{.ContextUserLocationMapURL}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
 							{{svg "octicon-link-external"}}
@@ -47,7 +47,7 @@
 			{{if (eq .SignedUserID .ContextUser.ID)}}
 				<li>
 					{{svg "octicon-mail"}}
-					<a class="gt-f1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
+					<a class="tw-flex-1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
 					<a href="{{AppSubUrl}}/user/settings#privacy-user-settings">
 						{{if .ShowUserEmail}}
 							<i data-tooltip-content="{{ctx.Locale.Tr "user.email_visibility.limited"}}">
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index 8e262d016c..dc8c7d7a80 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -16,7 +16,7 @@
 	{{if .Variables}}
 	<div class="flex-list">
 		{{range .Variables}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-content-center">
 			<div class="flex-item-leading">
 				{{svg "octicon-pencil" 32}}
 			</div>
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index 29de861a25..106529ca72 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -17,8 +17,8 @@
 <body>
 	<div class="full height">
 		<nav class="ui secondary menu">
-			<div class="ui container gt-df">
-				<div class="item gt-f1">
+			<div class="ui container tw-flex">
+				<div class="item tw-flex-1">
 					<a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home"}}">
 						<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
 					</a>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index d7d3649a4d..f8eb81423c 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -54,10 +54,10 @@
 		{{ctx.Locale.Tr "sign_in_or"}}
 	</div>
 	<div id="oauth2-login-navigator" class="gt-py-2">
-		<div class="gt-df gt-fc gt-jc">
-			<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
+		<div class="tw-flex tw-flex-col tw-justify-center">
+			<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-content-center gt-gap-3">
 				{{range $provider := .OAuth2Providers}}
-					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+					<a class="{{$provider.Name}} ui button tw-flex tw-content-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
 						{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 					</a>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index cfd826a0ce..a911537996 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -59,10 +59,10 @@
 				{{ctx.Locale.Tr "sign_in_or"}}
 			</div>
 			<div id="oauth2-login-navigator" class="gt-py-2">
-				<div class="gt-df gt-fc gt-jc">
-					<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
+				<div class="tw-flex tw-flex-col tw-justify-center">
+					<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-content-center gt-gap-3">
 						{{range $provider := .OAuth2Providers}}
-							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+							<a class="{{$provider.Name}} ui button tw-flex tw-content-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
 								{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 							</a>
diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl
index 722da02f54..375ebba9ae 100644
--- a/templates/user/auth/webauthn.tmpl
+++ b/templates/user/auth/webauthn.tmpl
@@ -10,7 +10,7 @@
 				{{template "base/alert" .}}
 				<p>{{ctx.Locale.Tr "webauthn_sign_in"}}</p>
 			</div>
-			<div class="ui attached segment gt-df gt-ac gt-jc gt-gap-2 gt-py-3">
+			<div class="ui attached segment tw-flex tw-content-center tw-justify-center gt-gap-2 gt-py-3">
 				<div class="is-loading" style="width: 40px; height: 40px"></div>
 				{{ctx.Locale.Tr "webauthn_press_button"}}
 			</div>
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index 0e7371ad83..382b0d4542 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -84,7 +84,7 @@
 					{{$push := ActionContent2Commits .}}
 					{{$repoLink := (.GetRepoLink ctx)}}
 					{{$repo := .Repo}}
-					<div class="gt-df gt-fc gt-gap-2">
+					<div class="tw-flex tw-flex-col gt-gap-2">
 						{{range $push.Commits}}
 							{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
 							<div class="flex-text-block">
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 214081d423..05f2b30efb 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -79,7 +79,7 @@
 									{{svg "octicon-milestone" 16}}
 									<a class="muted" href="{{.Repo.Link}}/milestone/{{.ID}}">{{.Name}}</a>
 								</h3>
-								<div class="gt-df gt-ac">
+								<div class="tw-flex tw-content-center">
 									<span class="gt-mr-3">{{.Completeness}}%</span>
 									<progress value="{{.Completeness}}" max="100"></progress>
 								</div>
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index 431aca0975..9da9e16d93 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -1,7 +1,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
 	<div class="ui container">
 		{{$notificationUnreadCount := call .NotificationUnreadCount}}
-		<div class="gt-df gt-ac gt-sb gt-mb-4">
+		<div class="tw-flex tw-content-center tw-justify-between gt-mb-4">
 			<div class="small-menu-items ui compact tiny menu">
 				<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
 					{{ctx.Locale.Tr "notification.unread"}}
@@ -25,7 +25,7 @@
 		<div class="gt-p-0">
 			<div id="notification_table">
 				{{if not .Notifications}}
-					<div class="gt-df gt-ac gt-fc gt-p-4">
+					<div class="tw-flex tw-content-center tw-flex-col gt-p-4">
 						{{svg "octicon-inbox" 56 "gt-mb-4"}}
 						{{if eq .Status 1}}
 							{{ctx.Locale.Tr "notification.no_unread"}}
@@ -35,7 +35,7 @@
 					</div>
 				{{else}}
 					{{range $notification := .Notifications}}
-						<div class="notifications-item gt-df gt-ac gt-fw gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
+						<div class="notifications-item tw-flex tw-content-center tw-flex-wrap gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
 							<div class="notifications-icon gt-ml-3 gt-mr-2 tw-self-start gt-mt-2">
 								{{if .Issue}}
 									{{template "shared/issueicon" .Issue}}
@@ -43,7 +43,7 @@
 									{{svg "octicon-repo" 16 "text grey"}}
 								{{end}}
 							</div>
-							<a class="notifications-link gt-df gt-f1 gt-fc silenced" href="{{.Link ctx}}">
+							<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}">
 								<div class="notifications-top-row gt-font-13">
 									{{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}}
 									{{if eq .Status 3}}
@@ -60,14 +60,14 @@
 									</span>
 								</div>
 							</a>
-							<div class="notifications-updated gt-ac gt-mr-3">
+							<div class="notifications-updated tw-content-center gt-mr-3">
 								{{if .Issue}}
 									{{TimeSinceUnix .Issue.UpdatedUnix ctx.Locale}}
 								{{else}}
 									{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
 								{{end}}
 							</div>
-							<div class="notifications-buttons gt-ac gt-je gt-gap-2 gt-px-2">
+							<div class="notifications-buttons tw-content-center tw-justify-end gt-gap-2 gt-px-2">
 								{{if ne .Status 3}}
 									<form action="{{AppSubUrl}}/notifications/status" method="post">
 										{{$.CsrfTokenHtml}}
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
index d39e628263..eb71621d92 100644
--- a/templates/user/notification/notification_subscriptions.tmpl
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -11,8 +11,8 @@
 		</div>
 		<div class="ui bottom attached active tab segment">
 			{{if eq .Status 1}}
-				<div class="gt-df gt-sb">
-					<div class="gt-df">
+				<div class="tw-flex tw-justify-between">
+					<div class="tw-flex">
 						<div class="small-menu-items ui compact tiny menu">
 							<a class="{{if eq .State "all"}}active {{end}}item" href="?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}">
 								{{ctx.Locale.Tr "all"}}
@@ -27,7 +27,7 @@
 							</a>
 						</div>
 					</div>
-					<div class="gt-df gt-sb">
+					<div class="tw-flex tw-justify-between">
 						<div class="ui right aligned secondary filter menu labels">
 							<!-- Type -->
 								<div class="ui dropdown type jump item">
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index 515e79d739..efc09156e6 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -46,7 +46,7 @@
 					<form action="{{AppSubUrl}}/user/settings/account/email" class="ui form" method="post">
 						{{$.CsrfTokenHtml}}
 						<input name="_method" type="hidden" value="NOTIFICATION">
-						<div class="gt-df gt-fw gt-gap-3">
+						<div class="tw-flex tw-flex-wrap gt-gap-3">
 							<div class="ui selection dropdown">
 								<input name="preference" type="hidden" value="{{.EmailNotificationsPreference}}">
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl
index bf6b28ec5f..1125a66d47 100644
--- a/templates/user/settings/applications_oauth2_list.tmpl
+++ b/templates/user/settings/applications_oauth2_list.tmpl
@@ -4,7 +4,7 @@
 			{{ctx.Locale.Tr "settings.oauth2_application_create_description"}}
 		</div>
 		{{range .Applications}}
-			<div class="flex-item gt-ac">
+			<div class="flex-item tw-content-center">
 				<div class="flex-item-leading">
 					{{svg "octicon-apps" 32}}
 				</div>
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index eeb2b6cbdd..41cdae2968 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -30,8 +30,8 @@
 											<span><a href="{{$repo.BaseRepo.Link}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span>
 										{{end}}
 									{{else}}
-										<span class="icon gt-dib gt-pt-3">{{svg "octicon-file-directory-fill"}}</span>
-										<span class="name gt-dib gt-pt-3">{{$.ContextUser.Name}}/{{$dir}}</span>
+										<span class="icon tw-inline-block gt-pt-3">{{svg "octicon-file-directory-fill"}}</span>
+										<span class="name tw-inline-block gt-pt-3">{{$.ContextUser.Name}}/{{$dir}}</span>
 										<div class="tw-float-right">
 											{{if $.allowAdopt}}
 												<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
diff --git a/templates/user/settings/security/openid.tmpl b/templates/user/settings/security/openid.tmpl
index 0e9b4adcbe..63bc56ba9b 100644
--- a/templates/user/settings/security/openid.tmpl
+++ b/templates/user/settings/security/openid.tmpl
@@ -7,7 +7,7 @@
 			{{ctx.Locale.Tr "settings.openid_desc"}}
 		</div>
 		{{range .OpenIDs}}
-			<div class="flex-item gt-ac">
+			<div class="flex-item tw-content-center">
 				<div class="flex-item-leading">
 					{{svg "fontawesome-openid" 20}}
 				</div>
diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl
index 305133c03a..e0d04c1767 100644
--- a/templates/webhook/new.tmpl
+++ b/templates/webhook/new.tmpl
@@ -1,7 +1,7 @@
 <h4 class="ui top attached header">
 	{{.CustomHeaderTitle}}
 	<div class="ui right type dropdown">
-		<div class="text gt-df gt-ac">
+		<div class="text tw-flex tw-content-center">
 			{{template "shared/webhook/icon" (dict "Size" 20 "HookType" .ctxData.HookType)}}
 			{{ctx.Locale.Tr (print "repo.settings.web_hook_name_" .ctxData.HookType)}}
 		</div>
diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go
index 04de0c123f..b5af43e492 100644
--- a/tests/integration/release_test.go
+++ b/tests/integration/release_test.go
@@ -234,7 +234,7 @@ func TestViewTagsList(t *testing.T) {
 
 	tagNames := make([]string, 0, 5)
 	tags.Each(func(i int, s *goquery.Selection) {
-		tagNames = append(tagNames, s.Find(".tag a.gt-df.gt-ac").Text())
+		tagNames = append(tagNames, s.Find(".tag a.tw-flex.tw-content-center").Text())
 	})
 
 	assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
diff --git a/web_src/css/actions.css b/web_src/css/actions.css
index e353a013a7..e7b9a3855a 100644
--- a/web_src/css/actions.css
+++ b/web_src/css/actions.css
@@ -14,10 +14,6 @@
   color: var(--color-red-light);
 }
 
-.runner-container .runner-basic-info .gt-dib {
-  margin-right: 1em;
-}
-
 .runner-container .runner-new-text {
   color: var(--color-white);
 }
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index a3817be223..c7097e631b 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -2,16 +2,6 @@
 Gitea's tailwind-style CSS helper classes have `gt-` prefix.
 Gitea's private styles use `g-` prefix.
 */
-.gt-df { display: flex !important; }
-.gt-dib { display: inline-block !important; }
-.gt-ac { align-items: center !important; }
-.gt-jc { justify-content: center !important; }
-.gt-je { justify-content: flex-end !important; }
-.gt-sb { justify-content: space-between !important; }
-.gt-fc { flex-direction: column !important; }
-.gt-f1 { flex: 1 !important; }
-.gt-fw { flex-wrap: wrap !important; }
-.gt-vm { vertical-align: middle !important; }
 
 .gt-mono {
   font-family: var(--fonts-monospace) !important;
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue
index 51a7745431..08a47eded7 100644
--- a/web_src/js/components/ActionRunStatus.vue
+++ b/web_src/js/components/ActionRunStatus.vue
@@ -28,7 +28,7 @@ export default {
 };
 </script>
 <template>
-  <span class="gt-df gt-ac" :data-tooltip-content="localeStatus" v-if="status">
+  <span class="tw-flex tw-content-center" :data-tooltip-content="localeStatus" v-if="status">
     <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
     <SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
     <SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 177ae04855..df46516449 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -345,12 +345,12 @@ export default sfc; // activate the IDE's Vue plugin
       <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textOrganization }}</a>
     </div>
     <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
-      <h4 class="ui top attached header gt-df gt-ac">
-        <div class="gt-f1 gt-df gt-ac">
+      <h4 class="ui top attached header tw-flex tw-content-center">
+        <div class="tw-flex-1 tw-flex tw-content-center">
           {{ textMyRepos }}
           <span class="ui grey label gt-ml-3">{{ reposTotalCount }}</span>
         </div>
-        <a class="gt-df gt-ac muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
+        <a class="tw-flex tw-content-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
           <svg-icon name="octicon-plus"/>
         </a>
       </h4>
@@ -411,7 +411,7 @@ export default sfc; // activate the IDE's Vue plugin
       </div>
       <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="gt-df gt-ac gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
+          <li class="tw-flex tw-content-center gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
             <a class="repo-list-link muted" :href="repo.link">
               <svg-icon :name="repoIcon(repo)" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ repo.full_name }}</div>
@@ -419,7 +419,7 @@ export default sfc; // activate the IDE's Vue plugin
                 <svg-icon name="octicon-archive" :size="16"/>
               </div>
             </a>
-            <a class="gt-df gt-ac" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
+            <a class="tw-flex tw-content-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
               <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
               <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'gt-ml-3 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
             </a>
@@ -458,18 +458,18 @@ export default sfc; // activate the IDE's Vue plugin
       </div>
     </div>
     <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs">
-      <h4 class="ui top attached header gt-df gt-ac">
-        <div class="gt-f1 gt-df gt-ac">
+      <h4 class="ui top attached header tw-flex tw-content-center">
+        <div class="tw-flex-1 tw-flex tw-content-center">
           {{ textMyOrgs }}
           <span class="ui grey label gt-ml-3">{{ organizationsTotalCount }}</span>
         </div>
-        <a class="gt-df gt-ac muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
+        <a class="tw-flex tw-content-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
           <svg-icon name="octicon-plus"/>
         </a>
       </h4>
       <div v-if="organizations.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="gt-df gt-ac gt-py-3" v-for="org in organizations" :key="org.name">
+          <li class="tw-flex tw-content-center gt-py-3" v-for="org in organizations" :key="org.name">
             <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
               <svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ org.name }}</div>
@@ -479,7 +479,7 @@ export default sfc; // activate the IDE's Vue plugin
                 </span>
               </div>
             </a>
-            <div class="text light grey gt-df gt-ac gt-ml-3">
+            <div class="text light grey tw-flex tw-content-center gt-ml-3">
               {{ org.num_repos }}
               <svg-icon name="octicon-repo" :size="16" class-name="gt-ml-2 gt-mt-1"/>
             </div>
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index b465127aca..e8ceffa3e8 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -240,7 +240,7 @@ export default {
           @click.meta.exact="commitClicked(commit.id, true)"
           @click.shift.exact.stop.prevent="commitClickedShift(commit)"
         >
-          <div class="gt-f1 gt-df gt-fc gt-gap-2">
+          <div class="tw-flex-1 tw-flex tw-flex-col gt-gap-2">
             <div class="gt-ellipsis commit-list-summary">
               {{ commit.summary }}
             </div>
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 8bde61804f..2499d998a8 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -38,7 +38,7 @@ export default {
 <template>
   <ol class="diff-stats gt-m-0" ref="root" v-if="store.fileListIsVisible">
     <li v-for="file in store.files" :key="file.NameHash">
-      <div class="gt-font-semibold gt-df gt-ac pull-right">
+      <div class="gt-font-semibold tw-flex tw-content-center pull-right">
         <span v-if="file.IsBin" class="gt-ml-1 gt-mr-3">{{ store.binaryFileMessage }}</span>
         {{ file.IsBin ? '' : file.Addition + file.Deletion }}
         <span v-if="!file.IsBin" class="diff-stats-bar gt-mx-3" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
@@ -50,7 +50,7 @@ export default {
       <a class="file gt-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
     </li>
     <li v-if="store.isIncomplete" class="gt-pt-2">
-      <span class="file gt-df gt-ac gt-sb">{{ store.tooManyFilesMessage }}
+      <span class="file tw-flex tw-content-center tw-justify-between">{{ store.tooManyFilesMessage }}
         <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
       </span>
     </li>
diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue
index 9d7ab4afc5..a5d78f07f1 100644
--- a/web_src/js/components/DiffFileTreeItem.vue
+++ b/web_src/js/components/DiffFileTreeItem.vue
@@ -37,7 +37,7 @@ export default {
   >
     <!-- file -->
     <SvgIcon name="octicon-file"/>
-    <span class="gt-ellipsis gt-f1">{{ item.name }}</span>
+    <span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
     <SvgIcon :name="getIconForDiffType(item.file.Type).name" :class="getIconForDiffType(item.file.Type).classes"/>
   </a>
   <div v-else class="item-directory" :title="item.name" @click.stop="collapsed = !collapsed">
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index 170d0d85c6..5f2e19f2e5 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -136,7 +136,7 @@ export default {
       </div>
     </form>
 
-    <div v-if="!showActionForm" class="gt-df">
+    <div v-if="!showActionForm" class="tw-flex">
       <!-- the merge button -->
       <div class="ui buttons merge-button" :class="[mergeForm.emptyCommit ? 'grey' : mergeForm.allOverridableChecksOk ? 'primary' : 'red']" @click="toggleActionForm(true)">
         <button class="ui button">
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index 83289c8852..a5ac689e5a 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -245,8 +245,8 @@ export default sfc; // activate IDE's Vue plugin
 </script>
 <template>
   <div class="ui dropdown custom">
-    <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
-      <span class="text gt-df gt-ac gt-mr-2">
+    <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex gt-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
+      <span class="text tw-flex tw-content-center gt-mr-2">
         <template v-if="release">{{ textReleaseCompare }}</template>
         <template v-else>
           <svg-icon v-if="isViewTag" name="octicon-tag"/>
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
index c55bbff9cd..305732afc1 100644
--- a/web_src/js/components/RepoCodeFrequency.vue
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -144,10 +144,10 @@ export default {
 </script>
 <template>
   <div>
-    <div class="ui header gt-df gt-ac gt-sb">
+    <div class="ui header tw-flex tw-content-center tw-justify-between">
       {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
     </div>
-    <div class="gt-df ui segment main-graph">
+    <div class="tw-flex ui segment main-graph">
       <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
           <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 6093c762cb..ca51ca8aba 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -303,7 +303,7 @@ export default {
 </script>
 <template>
   <div>
-    <div class="ui header gt-df gt-ac gt-sb">
+    <div class="ui header tw-flex tw-content-center tw-justify-between">
       <div>
         <relative-time
           v-if="xAxisMin > 0"
@@ -352,7 +352,7 @@ export default {
         </div>
       </div>
     </div>
-    <div class="gt-df ui segment main-graph">
+    <div class="tw-flex ui segment main-graph">
       <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
           <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
@@ -374,17 +374,17 @@ export default {
         :key="index"
         v-memo="[sortedContributors, type]"
       >
-        <div class="ui top attached header gt-df gt-f1">
+        <div class="ui top attached header tw-flex tw-flex-1">
           <b class="ui right">#{{ index + 1 }}</b>
           <a :href="contributor.home_link">
-            <img class="ui avatar gt-vm" height="40" width="40" :src="contributor.avatar_link">
+            <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link">
           </a>
           <div class="gt-ml-3">
             <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
             <h4 v-else class="contributor-name">
               {{ contributor.name }}
             </h4>
-            <p class="gt-font-12 gt-df gt-gap-2">
+            <p class="gt-font-12 tw-flex gt-gap-2">
               <strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong>
               <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
               <strong v-if="contributor.total_deletions" class="text red">
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
index c1fd40f506..23738b8060 100644
--- a/web_src/js/components/RepoRecentCommits.vue
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -121,10 +121,10 @@ export default {
 </script>
 <template>
   <div>
-    <div class="ui header gt-df gt-ac gt-sb">
+    <div class="ui header tw-flex tw-content-center tw-justify-between">
       {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
     </div>
-    <div class="gt-df ui segment main-graph">
+    <div class="tw-flex ui segment main-graph">
       <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
           <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index 9e2b773730..f67a22ea6f 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -16,7 +16,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
   $dialog = $(`
 <div class="ui modal content-history-detail-dialog">
   ${svg('octicon-x', 16, 'close icon inside')}
-  <div class="header gt-df gt-ac gt-sb">
+  <div class="header tw-flex tw-content-center tw-justify-between">
     <div>${itemTitleHtml}</div>
     <div class="ui dropdown dialog-header-options gt-mr-5 gt-hidden">
       ${i18nTextOptions}
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 48658fd723..4bdd5e5a8e 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -108,7 +108,7 @@ function initRepoIssueListAuthorDropdown() {
         // the content is provided by backend IssuePosters handler
         const processedResults = []; // to be used by dropdown to generate menu items
         for (const item of resp.results) {
-          let html = `<img class="ui avatar gt-vm" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
+          let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
           if (item.full_name) html += `<span class="search-fullname gt-ml-3">${htmlEscape(item.full_name)}</span>`;
           processedResults.push({value: item.user_id, name: html});
         }

From 3d751b6ec18e57698ce86b79866031d2c80c2071 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 22 Mar 2024 15:06:53 +0100
Subject: [PATCH 478/679] Enforce trailing comma in JS on multiline (#30002)

To keep blame info accurate and to avoid [changes like
this](https://github.com/go-gitea/gitea/pull/29977/files#diff-c3422631a14edbe1e508c4b22f0c718db318be08a6e889427802f9b6165d88d6R359),
it's good to always have a trailing comma, so let's enforce it in JS.

This rule is completely automatically fixable with `make lint-js-fix`
and that's what I did here.
---
 .eslintrc.yaml                                |  2 +-
 playwright.config.js                          |  2 +-
 tools/generate-images.js                      |  2 +-
 tools/generate-svg.js                         |  4 +--
 web_src/js/components/ActionRunStatus.vue     | 10 +++----
 web_src/js/components/ActivityHeatmap.vue     |  4 +--
 web_src/js/components/ContextPopup.vue        |  6 ++--
 web_src/js/components/DashboardRepoList.vue   |  4 +--
 web_src/js/components/DiffCommitSelector.vue  |  6 ++--
 web_src/js/components/DiffFileList.vue        |  2 +-
 web_src/js/components/DiffFileTree.vue        |  6 ++--
 web_src/js/components/DiffFileTreeItem.vue    |  2 +-
 .../js/components/PullRequestMergeForm.vue    |  2 +-
 web_src/js/components/RepoActionView.vue      |  6 ++--
 .../js/components/RepoActivityTopAuthors.vue  |  2 +-
 .../js/components/RepoBranchTagSelector.vue   |  6 ++--
 web_src/js/components/RepoCodeFrequency.vue   |  6 ++--
 web_src/js/components/RepoContributors.vue    |  8 ++---
 web_src/js/components/RepoRecentCommits.vue   |  6 ++--
 .../components/ScopedAccessTokenSelector.vue  |  4 +--
 web_src/js/features/captcha.js                |  4 +--
 web_src/js/features/code-frequency.js         |  2 +-
 web_src/js/features/codeeditor.js             |  4 +--
 web_src/js/features/common-global.js          |  2 +-
 .../js/features/comp/EasyMDEToolbarActions.js |  2 +-
 web_src/js/features/comp/SearchUserBox.js     |  8 ++---
 web_src/js/features/contextpopup.js           |  2 +-
 web_src/js/features/contributors.js           |  2 +-
 .../js/features/eventsource.sharedworker.js   |  2 +-
 web_src/js/features/imagediff.js              | 30 +++++++++----------
 web_src/js/features/install.js                |  2 +-
 web_src/js/features/org-team.js               |  6 ++--
 web_src/js/features/recent-commits.js         |  2 +-
 web_src/js/features/repo-code.js              |  2 +-
 web_src/js/features/repo-home.js              | 12 ++++----
 web_src/js/features/repo-issue-content.js     |  2 +-
 web_src/js/features/repo-issue.js             | 14 ++++-----
 web_src/js/features/repo-settings.js          |  8 ++---
 web_src/js/features/repo-template.js          |  6 ++--
 web_src/js/features/repo-wiki.js              |  2 +-
 web_src/js/features/tribute.js                |  4 +--
 web_src/js/features/user-auth-webauthn.js     |  6 ++--
 web_src/js/modules/tippy.js                   |  2 +-
 web_src/js/standalone/swagger.js              |  6 ++--
 web_src/js/svg.js                             |  2 +-
 web_src/js/utils/dom.js                       |  2 +-
 web_src/js/webcomponents/polyfills.js         |  2 +-
 webpack.config.js                             |  6 ++--
 48 files changed, 117 insertions(+), 117 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 72039a6013..eeb3e20cb8 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -119,7 +119,7 @@ rules:
   "@stylistic/js/arrow-spacing": [2, {before: true, after: true}]
   "@stylistic/js/block-spacing": [0]
   "@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}]
-  "@stylistic/js/comma-dangle": [2, only-multiline]
+  "@stylistic/js/comma-dangle": [2, always-multiline]
   "@stylistic/js/comma-spacing": [2, {before: false, after: true}]
   "@stylistic/js/comma-style": [2, last]
   "@stylistic/js/computed-property-spacing": [2, never]
diff --git a/playwright.config.js b/playwright.config.js
index b7badf1cc0..bdd303ae25 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -20,7 +20,7 @@ export default {
      * Maximum time expect() should wait for the condition to be met.
      * For example in `await expect(locator).toHaveText();`
      */
-    timeout: 2000
+    timeout: 2000,
   },
 
   /* Fail the build on CI if you accidentally left test.only in the source code. */
diff --git a/tools/generate-images.js b/tools/generate-images.js
index cc2855c18e..0bd3af29e4 100755
--- a/tools/generate-images.js
+++ b/tools/generate-images.js
@@ -20,7 +20,7 @@ async function generate(svg, path, {size, bg}) {
         'removeDimensions',
         {
           name: 'addAttributesToSVGElement',
-          params: {attributes: [{width: size}, {height: size}]}
+          params: {attributes: [{width: size}, {height: size}]},
         },
       ],
     });
diff --git a/tools/generate-svg.js b/tools/generate-svg.js
index f26b60d960..f744162099 100755
--- a/tools/generate-svg.js
+++ b/tools/generate-svg.js
@@ -39,8 +39,8 @@ async function processFile(file, {prefix, fullName} = {}) {
           attributes: [
             {'xmlns': 'http://www.w3.org/2000/svg'},
             {'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'},
-          ]
-        }
+          ],
+        },
       },
     ],
   });
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue
index 08a47eded7..4eccddffdf 100644
--- a/web_src/js/components/ActionRunStatus.vue
+++ b/web_src/js/components/ActionRunStatus.vue
@@ -10,20 +10,20 @@ export default {
   props: {
     status: {
       type: String,
-      required: true
+      required: true,
     },
     size: {
       type: Number,
-      default: 16
+      default: 16,
     },
     className: {
       type: String,
-      default: ''
+      default: '',
     },
     localeStatus: {
       type: String,
-      default: ''
-    }
+      default: '',
+    },
   },
 };
 </script>
diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue
index 96a6e68012..9592a0df3c 100644
--- a/web_src/js/components/ActivityHeatmap.vue
+++ b/web_src/js/components/ActivityHeatmap.vue
@@ -11,7 +11,7 @@ export default {
     locale: {
       type: Object,
       default: () => {},
-    }
+    },
   },
   data: () => ({
     colorRange: [
@@ -49,7 +49,7 @@ export default {
 
       const newSearch = params.toString();
       window.location.search = newSearch.length ? `?${newSearch}` : '';
-    }
+    },
   },
 };
 </script>
diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue
index 3a1b828cca..149cabd41e 100644
--- a/web_src/js/components/ContextPopup.vue
+++ b/web_src/js/components/ContextPopup.vue
@@ -69,7 +69,7 @@ export default {
         }
         return {name: label.name, color: `#${label.color}`, textColor};
       });
-    }
+    },
   },
   mounted() {
     this.$refs.root.addEventListener('ce-load-context-popup', (e) => {
@@ -97,8 +97,8 @@ export default {
       } finally {
         this.loading = false;
       }
-    }
-  }
+    },
+  },
 };
 </script>
 <template>
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index df46516449..f4edce955a 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -253,7 +253,7 @@ const sfc = {
             ...webSearchRepo.repository,
             latest_commit_status_state: webSearchRepo.latest_commit_status.State,
             locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
-            latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL
+            latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL,
           };
         });
         const count = response.headers.get('X-Total-Count');
@@ -325,7 +325,7 @@ const sfc = {
       if (this.activeIndex === -1 || this.activeIndex > this.repos.length - 1) {
         this.activeIndex = 0;
       }
-    }
+    },
   },
 };
 
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index e8ceffa3e8..df712a2cb4 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -14,7 +14,7 @@ export default {
       },
       commits: [],
       hoverActivated: false,
-      lastReviewCommitSha: null
+      lastReviewCommitSha: null,
     };
   },
   computed: {
@@ -29,7 +29,7 @@ export default {
     },
     issueLink() {
       return this.$el.parentNode.getAttribute('data-issuelink');
-    }
+    },
   },
   mounted() {
     document.body.addEventListener('click', this.onBodyClick);
@@ -185,7 +185,7 @@ export default {
         }
       }
     },
-  }
+  },
 };
 </script>
 <template>
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 2499d998a8..64493b348a 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -31,7 +31,7 @@ export default {
     },
     loadMoreData() {
       loadMoreFiles(this.store.linkLoadMore);
-    }
+    },
   },
 };
 </script>
diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue
index 3686629df8..83d57b00d1 100644
--- a/web_src/js/components/DiffFileTree.vue
+++ b/web_src/js/components/DiffFileTree.vue
@@ -30,7 +30,7 @@ export default {
           let newParent = {
             name: split,
             children: [],
-            isFile
+            isFile,
           };
 
           if (isFile === true) {
@@ -40,7 +40,7 @@ export default {
           if (parent) {
             // check if the folder already exists
             const existingFolder = parent.children.find(
-              (x) => x.name === split
+              (x) => x.name === split,
             );
             if (existingFolder) {
               newParent = existingFolder;
@@ -74,7 +74,7 @@ export default {
       // reduce the depth of our tree.
       mergeChildIfOnlyOneDir(result);
       return result;
-    }
+    },
   },
   mounted() {
     // Default to true if unset
diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue
index a5d78f07f1..0f6e54363f 100644
--- a/web_src/js/components/DiffFileTreeItem.vue
+++ b/web_src/js/components/DiffFileTreeItem.vue
@@ -7,7 +7,7 @@ export default {
   props: {
     item: {
       type: Object,
-      required: true
+      required: true,
     },
   },
   data: () => ({
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index 5f2e19f2e5..35acbdf74f 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -43,7 +43,7 @@ export default {
       for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
         toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
       }
-    }
+    },
   },
   created() {
     this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index c1e2c2b2d5..2a4a6d77ff 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -66,7 +66,7 @@ const sfc = {
             name: '',
             link: '',
           },
-        }
+        },
       },
       currentJob: {
         title: '',
@@ -311,7 +311,7 @@ const sfc = {
       const logLine = this.$refs.steps.querySelector(selectedLogStep);
       if (!logLine) return;
       logLine.querySelector('.line-num').click();
-    }
+    },
   },
 };
 
@@ -352,7 +352,7 @@ export function initRepositoryActionView() {
         skipped: el.getAttribute('data-locale-status-skipped'),
         blocked: el.getAttribute('data-locale-status-blocked'),
       },
-    }
+    },
   });
   view.mount(el);
 }
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index fe41218d88..a41fb61d78 100644
--- a/web_src/js/components/RepoActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -47,7 +47,7 @@ const sfc = {
     this.colors.barColor = refStyle.backgroundColor;
     this.colors.textColor = refStyle.color;
     this.colors.textAltColor = refAltStyle.color;
-  }
+  },
 };
 
 export function initRepoActivityTopAuthorsChart() {
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index a5ac689e5a..e3e0c13fb9 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -36,7 +36,7 @@ const sfc = {
     },
     shouldCreateTag() {
       return this.mode === 'tags';
-    }
+    },
   },
 
   watch: {
@@ -45,7 +45,7 @@ const sfc = {
         this.focusSearchField();
         this.fetchBranchesOrTags();
       }
-    }
+    },
   },
 
   beforeMount() {
@@ -209,7 +209,7 @@ const sfc = {
         this.isLoading = false;
       }
     },
-  }
+  },
 };
 
 export function initRepoBranchTagSelector(selector) {
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
index 305732afc1..4e11d51a4a 100644
--- a/web_src/js/components/RepoCodeFrequency.vue
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -39,7 +39,7 @@ export default {
   props: {
     locale: {
       type: Object,
-      required: true
+      required: true,
     },
   },
   data: () => ({
@@ -128,12 +128,12 @@ export default {
             },
             ticks: {
               maxRotation: 0,
-              maxTicksLimit: 12
+              maxTicksLimit: 12,
             },
           },
           y: {
             ticks: {
-              maxTicksLimit: 6
+              maxTicksLimit: 6,
             },
           },
         },
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index ca51ca8aba..731b21bea3 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -34,7 +34,7 @@ const customEventListener = {
       chart.resetZoom();
       opts.instance.updateOtherCharts(args.event, true);
     }
-  }
+  },
 };
 
 Chart.defaults.color = chartJsColors.text;
@@ -82,7 +82,7 @@ export default {
         this.xAxisMax = this.xAxisEnd;
         this.type = val;
         this.sortContributors();
-      }
+      },
     });
   },
   methods: {
@@ -175,7 +175,7 @@ export default {
       // Normally, chartjs handles this automatically, but it will resize the graph when you
       // zoom, pan etc. I think resizing the graph makes it harder to compare things visually.
       const maxValue = Math.max(
-        ...this.totalStats.weeks.map((o) => o[this.type])
+        ...this.totalStats.weeks.map((o) => o[this.type]),
       );
       const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
       if (coefficient % 1 === 0) return maxValue;
@@ -187,7 +187,7 @@ export default {
       // for contributors' graph. If I let chartjs do this for me, it will choose different
       // maxY value for each contributors' graph which again makes it harder to compare.
       const maxValue = Math.max(
-        ...this.sortedContributors.map((c) => c.max_contribution_type)
+        ...this.sortedContributors.map((c) => c.max_contribution_type),
       );
       const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
       if (coefficient % 1 === 0) return maxValue;
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
index 23738b8060..1818d57943 100644
--- a/web_src/js/components/RepoRecentCommits.vue
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -35,7 +35,7 @@ export default {
   props: {
     locale: {
       type: Object,
-      required: true
+      required: true,
     },
   },
   data: () => ({
@@ -105,12 +105,12 @@ export default {
             },
             ticks: {
               maxRotation: 0,
-              maxTicksLimit: 52
+              maxTicksLimit: 52,
             },
           },
           y: {
             ticks: {
-              maxTicksLimit: 6
+              maxTicksLimit: 6,
             },
           },
         },
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
index f6af7e447f..ae4e8299f2 100644
--- a/web_src/js/components/ScopedAccessTokenSelector.vue
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -39,7 +39,7 @@ const sfc = {
         'repository',
         'user');
       return categories;
-    }
+    },
   },
 
   mounted() {
@@ -68,7 +68,7 @@ const sfc = {
       }
       // no scopes selected, show validation error
       showElem(warningEl);
-    }
+    },
   },
 };
 
diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js
index 3da5dbda41..c803a5006b 100644
--- a/web_src/js/features/captcha.js
+++ b/web_src/js/features/captcha.js
@@ -9,7 +9,7 @@ export async function initCaptcha() {
 
   const params = {
     sitekey: siteKey,
-    theme: isDark ? 'dark' : 'light'
+    theme: isDark ? 'dark' : 'light',
   };
 
   switch (captchaEl.getAttribute('data-captcha-type')) {
@@ -42,7 +42,7 @@ export async function initCaptcha() {
         siteKey: {
           instanceUrl: new URL(instanceURL),
           key: siteKey,
-        }
+        },
       });
       break;
     }
diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js
index 103d82f6e3..47e1539ddc 100644
--- a/web_src/js/features/code-frequency.js
+++ b/web_src/js/features/code-frequency.js
@@ -11,7 +11,7 @@ export async function initRepoCodeFrequency() {
         loadingTitle: el.getAttribute('data-locale-loading-title'),
         loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
         loadingInfo: el.getAttribute('data-locale-loading-info'),
-      }
+      },
     });
     View.mount(el);
   } catch (err) {
diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js
index fceb2f7620..4fb8bb9e63 100644
--- a/web_src/js/features/codeeditor.js
+++ b/web_src/js/features/codeeditor.js
@@ -80,7 +80,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
     rules: [
       {
         background: getColor('--color-code-bg'),
-      }
+      },
     ],
     colors: {
       'editor.background': getColor('--color-code-bg'),
@@ -98,7 +98,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
       'input.foreground': getColor('--color-input-text'),
       'scrollbar.shadow': getColor('--color-shadow'),
       'progressBar.background': getColor('--color-primary'),
-    }
+    },
   });
 
   // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index d99f606c8a..2469361c6e 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -335,7 +335,7 @@ export function initGlobalLinkActions() {
           const data = await response.json();
           window.location.href = data.redirect;
         }
-      }
+      },
     }).modal('show');
   }
 
diff --git a/web_src/js/features/comp/EasyMDEToolbarActions.js b/web_src/js/features/comp/EasyMDEToolbarActions.js
index 8286d5d871..c97d683704 100644
--- a/web_src/js/features/comp/EasyMDEToolbarActions.js
+++ b/web_src/js/features/comp/EasyMDEToolbarActions.js
@@ -139,7 +139,7 @@ export function easyMDEToolbarActions(EasyMDE, editor) {
       },
       icon: svg('octicon-chevron-right'),
       title: 'Add Inline Code',
-    }
+    },
   };
 
   for (const [key, value] of Object.entries(actions)) {
diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js
index 541052c174..83d7044f11 100644
--- a/web_src/js/features/comp/SearchUserBox.js
+++ b/web_src/js/features/comp/SearchUserBox.js
@@ -22,7 +22,7 @@ export function initCompSearchUserBox() {
         $.each(response.data, (_i, item) => {
           const resultItem = {
             title: item.login,
-            image: item.avatar_url
+            image: item.avatar_url,
           };
           if (item.full_name) {
             resultItem.description = htmlEscape(item.full_name);
@@ -37,15 +37,15 @@ export function initCompSearchUserBox() {
         if (allowEmailInput && items.length === 0 && looksLikeEmailAddressCheck.test(searchQuery)) {
           const resultItem = {
             title: searchQuery,
-            description: allowEmailDescription
+            description: allowEmailDescription,
           };
           items.push(resultItem);
         }
 
         return {results: items};
-      }
+      },
     },
     searchFields: ['login', 'full_name'],
-    showNoResults: false
+    showNoResults: false,
   });
 }
diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js
index 51363b810a..ce90f3e505 100644
--- a/web_src/js/features/contextpopup.js
+++ b/web_src/js/features/contextpopup.js
@@ -37,7 +37,7 @@ export function attachRefIssueContextPopup(refIssues) {
       interactiveBorder: 5,
       onShow: () => {
         el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
-      }
+      },
     });
   }
 }
diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js
index 66185ac315..1d9cba5b9b 100644
--- a/web_src/js/features/contributors.js
+++ b/web_src/js/features/contributors.js
@@ -18,7 +18,7 @@ export async function initRepoContributors() {
         loadingTitle: el.getAttribute('data-locale-loading-title'),
         loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
         loadingInfo: el.getAttribute('data-locale-loading-info'),
-      }
+      },
     });
     View.mount(el);
   } catch (err) {
diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js
index 2ac7d93cc1..62581cf687 100644
--- a/web_src/js/features/eventsource.sharedworker.js
+++ b/web_src/js/features/eventsource.sharedworker.js
@@ -48,7 +48,7 @@ class Source {
     this.eventSource.addEventListener(eventType, (event) => {
       this.notifyClients({
         type: eventType,
-        data: event.data
+        data: event.data,
       });
     });
   }
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index 2bac13b0bf..7b77b30ccc 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -20,19 +20,19 @@ function getDefaultSvgBoundsIfUndefined(text, src) {
     if (img.width > 1 && img.width < MaxSize && img.height > 1 && img.height < MaxSize) {
       return {
         width: img.width,
-        height: img.height
+        height: img.height,
       };
     }
     if (svg.hasAttribute('viewBox')) {
       const viewBox = svg.viewBox.baseVal;
       return {
         width: DefaultSize,
-        height: DefaultSize * viewBox.width / viewBox.height
+        height: DefaultSize * viewBox.width / viewBox.height,
       };
     }
     return {
       width: DefaultSize,
-      height: DefaultSize
+      height: DefaultSize,
     };
   }
   return null;
@@ -42,15 +42,15 @@ export function initImageDiff() {
   function createContext(image1, image2) {
     const size1 = {
       width: image1 && image1.width || 0,
-      height: image1 && image1.height || 0
+      height: image1 && image1.height || 0,
     };
     const size2 = {
       width: image2 && image2.width || 0,
-      height: image2 && image2.height || 0
+      height: image2 && image2.height || 0,
     };
     const max = {
       width: Math.max(size2.width, size1.width),
-      height: Math.max(size2.height, size1.height)
+      height: Math.max(size2.height, size1.height),
     };
 
     return {
@@ -63,8 +63,8 @@ export function initImageDiff() {
         Math.floor(max.width - size1.width) / 2,
         Math.floor(max.height - size1.height) / 2,
         Math.floor(max.width - size2.width) / 2,
-        Math.floor(max.height - size2.height) / 2
-      ]
+        Math.floor(max.height - size2.height) / 2,
+      ],
     };
   }
 
@@ -79,12 +79,12 @@ export function initImageDiff() {
       path: this.getAttribute('data-path-after'),
       mime: this.getAttribute('data-mime-after'),
       $images: $container.find('img.image-after'), // matches 3 <img>
-      $boundsInfo: $container.find('.bounds-info-after')
+      $boundsInfo: $container.find('.bounds-info-after'),
     }, {
       path: this.getAttribute('data-path-before'),
       mime: this.getAttribute('data-mime-before'),
       $images: $container.find('img.image-before'), // matches 3 <img>
-      $boundsInfo: $container.find('.bounds-info-before')
+      $boundsInfo: $container.find('.bounds-info-before'),
     }];
 
     await Promise.all(imageInfos.map(async (info) => {
@@ -222,21 +222,21 @@ export function initImageDiff() {
 
       sizes.image1.css({
         width: sizes.size1.width * factor,
-        height: sizes.size1.height * factor
+        height: sizes.size1.height * factor,
       });
       sizes.image2.css({
         width: sizes.size2.width * factor,
-        height: sizes.size2.height * factor
+        height: sizes.size2.height * factor,
       });
       sizes.image1.parent().css({
         margin: `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`,
         width: sizes.size1.width * factor + 2,
-        height: sizes.size1.height * factor + 2
+        height: sizes.size1.height * factor + 2,
       });
       sizes.image2.parent().css({
         margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
         width: sizes.size2.width * factor + 2,
-        height: sizes.size2.height * factor + 2
+        height: sizes.size2.height * factor + 2,
       });
 
       // some inner elements are `position: absolute`, so the container's height must be large enough
@@ -248,7 +248,7 @@ export function initImageDiff() {
 
       const $range = $container.find("input[type='range']");
       const onInput = () => sizes.image1.parent().css({
-        opacity: $range.val() / 100
+        opacity: $range.val() / 100,
       });
       $range.on('input', onInput);
       onInput();
diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js
index 2d6d345c1d..54ba3778f8 100644
--- a/web_src/js/features/install.js
+++ b/web_src/js/features/install.js
@@ -19,7 +19,7 @@ function initPreInstall() {
   const defaultDbHosts = {
     mysql: '127.0.0.1:3306',
     postgres: '127.0.0.1:5432',
-    mssql: '127.0.0.1:1433'
+    mssql: '127.0.0.1:1433',
   };
 
   const dbHost = document.getElementById('db_host');
diff --git a/web_src/js/features/org-team.js b/web_src/js/features/org-team.js
index 6ae3a90f4d..2236bc58bc 100644
--- a/web_src/js/features/org-team.js
+++ b/web_src/js/features/org-team.js
@@ -26,14 +26,14 @@ export function initOrgTeamSearchRepoBox() {
         $.each(response.data, (_i, item) => {
           items.push({
             title: item.repository.full_name.split('/')[1],
-            description: item.repository.full_name
+            description: item.repository.full_name,
           });
         });
 
         return {results: items};
-      }
+      },
     },
     searchFields: ['full_name'],
-    showNoResults: false
+    showNoResults: false,
   });
 }
diff --git a/web_src/js/features/recent-commits.js b/web_src/js/features/recent-commits.js
index ded10d39be..030c251a05 100644
--- a/web_src/js/features/recent-commits.js
+++ b/web_src/js/features/recent-commits.js
@@ -11,7 +11,7 @@ export async function initRepoRecentCommits() {
         loadingTitle: el.getAttribute('data-locale-loading-title'),
         loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
         loadingInfo: el.getAttribute('data-locale-loading-info'),
-      }
+      },
     });
     View.mount(el);
   } catch (err) {
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index c4a81ea165..08fae763b8 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -116,7 +116,7 @@ function showLineButton() {
       tippy.popper.addEventListener('click', () => {
         tippy.hide();
       }, {once: true});
-    }
+    },
   });
 }
 
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index 50f324d788..6ac7b96b9e 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -146,7 +146,7 @@ export function initRepoTopicBar() {
       addedValue = addedValue.toLowerCase().trim();
       $($addedChoice).attr('data-value', addedValue);
       $($addedChoice).attr('data-text', addedValue);
-    }
+    },
   });
 
   $.fn.form.settings.rules.validateTopic = function (_values, regExp) {
@@ -168,14 +168,14 @@ export function initRepoTopicBar() {
           {
             type: 'validateTopic',
             value: /^\s*[a-z0-9][-.a-z0-9]{0,35}\s*$/,
-            prompt: topicPrompts.formatPrompt
+            prompt: topicPrompts.formatPrompt,
           },
           {
             type: 'maxCount[25]',
-            prompt: topicPrompts.countPrompt
-          }
-        ]
+            prompt: topicPrompts.countPrompt,
+          },
+        ],
       },
-    }
+    },
   });
 }
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index f67a22ea6f..33ea55f027 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -60,7 +60,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
     },
     onHide() {
       $(this).dropdown('clear', true);
-    }
+    },
   });
   $dialog.modal({
     async onShow() {
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index c91dd06ac9..bf4ec15372 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -59,7 +59,7 @@ async function updateDeadline(deadlineString) {
 
   try {
     const response = await POST($('#update-issue-deadline-form').attr('action'), {
-      data: {due_date: realDeadline}
+      data: {due_date: realDeadline},
     });
 
     if (response.ok) {
@@ -268,7 +268,7 @@ export function initRepoPullRequestUpdate() {
         $pullUpdateButton.find('.button-text').text($choice.text());
         $pullUpdateButton.data('do', $url);
       }
-    }
+    },
   });
 }
 
@@ -316,7 +316,7 @@ export function initRepoIssueReferenceRepositorySearch() {
           $.each(response.data, (_r, repo) => {
             filteredResponse.results.push({
               name: htmlEscape(repo.repository.full_name),
-              value: repo.repository.full_name
+              value: repo.repository.full_name,
             });
           });
           return filteredResponse;
@@ -327,7 +327,7 @@ export function initRepoIssueReferenceRepositorySearch() {
         const $form = $choice.closest('form');
         $form.attr('action', `${appSubUrl}/${_text}/issues/new`);
       },
-      fullTextSearch: true
+      fullTextSearch: true,
     });
 }
 
@@ -443,7 +443,7 @@ export function initRepoPullRequestReview() {
         }
         window.scrollTo({
           top: $commentDiv.offset().top - offset,
-          behavior: 'instant'
+          behavior: 'instant',
         });
       }
     }
@@ -661,7 +661,7 @@ export function initRepoIssueBranchSelect() {
     // Replace branch name to keep translation from HTML template
     $selectionTextField.html($selectionTextField.html().replace(
       `${baseName}:${branchNameOld}`,
-      `${baseName}:${branchNameNew}`
+      `${baseName}:${branchNameNew}`,
     ));
     $selectionTextField.data('branch', branchNameNew); // update branch name in setting
   };
@@ -695,7 +695,7 @@ export function initIssueTemplateCommentEditors($commentForm) {
     const editor = await initComboMarkdownEditor($markdownEditor, {
       onContentChanged: (editor) => {
         $formField.val(editor.value());
-      }
+      },
     });
 
     $formField.on('focus', async () => {
diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 0418f3a14a..58b714fbb7 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -39,7 +39,7 @@ export function initRepoSettingsCollaboration() {
             $text.text('(none)'); // prevent from misleading users when the access mode is undefined
           }
         }, 0);
-      }
+      },
     });
   });
 }
@@ -56,15 +56,15 @@ export function initRepoSettingSearchTeamBox() {
         $.each(response.data, (_i, item) => {
           items.push({
             title: item.name,
-            description: `${item.permission} access` // TODO: translate this string
+            description: `${item.permission} access`, // TODO: translate this string
           });
         });
 
         return {results: items};
-      }
+      },
     },
     searchFields: ['name', 'description'],
-    showNoResults: false
+    showNoResults: false,
   });
 }
 
diff --git a/web_src/js/features/repo-template.js b/web_src/js/features/repo-template.js
index 1e83e74780..5f63e8b3ba 100644
--- a/web_src/js/features/repo-template.js
+++ b/web_src/js/features/repo-template.js
@@ -29,13 +29,13 @@ export function initRepoTemplateSearch() {
             const filteredResponse = {success: true, results: []};
             filteredResponse.results.push({
               name: '',
-              value: ''
+              value: '',
             });
             // Parse the response from the api to work with our dropdown
             $.each(response.data, (_r, repo) => {
               filteredResponse.results.push({
                 name: htmlEscape(repo.repository.full_name),
-                value: repo.repository.id
+                value: repo.repository.id,
               });
             });
             return filteredResponse;
@@ -43,7 +43,7 @@ export function initRepoTemplateSearch() {
           cache: false,
         },
 
-        fullTextSearch: true
+        fullTextSearch: true,
       });
   };
   $('#uid').on('change', changeOwner);
diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js
index d51bf35c81..03a2c68c5a 100644
--- a/web_src/js/features/repo-wiki.js
+++ b/web_src/js/features/repo-wiki.js
@@ -60,7 +60,7 @@ async function initRepoWikiFormEditor() {
         'gitea-code-inline', 'code', 'quote', '|', 'gitea-checkbox-empty', 'gitea-checkbox-checked', '|',
         'unordered-list', 'ordered-list', '|',
         'link', 'image', 'table', 'horizontal-rule', '|',
-        'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea'
+        'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea',
       ],
     },
   });
diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js
index 055777be79..70a5de6913 100644
--- a/web_src/js/features/tribute.js
+++ b/web_src/js/features/tribute.js
@@ -25,7 +25,7 @@ function makeCollections({mentions, emoji}) {
       },
       menuItemTemplate: (item) => {
         return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`;
-      }
+      },
     });
   }
 
@@ -41,7 +41,7 @@ function makeCollections({mentions, emoji}) {
             ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''}
           </div>
         `;
-      }
+      },
     });
   }
 
diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js
index 363e039760..6dfbb4d765 100644
--- a/web_src/js/features/user-auth-webauthn.js
+++ b/web_src/js/features/user-auth-webauthn.js
@@ -26,7 +26,7 @@ export async function initUserAuthWebAuthn() {
   }
   try {
     const credential = await navigator.credentials.get({
-      publicKey: options.publicKey
+      publicKey: options.publicKey,
     });
     await verifyAssertion(credential);
   } catch (err) {
@@ -37,7 +37,7 @@ export async function initUserAuthWebAuthn() {
     delete options.publicKey.extensions.appid;
     try {
       const credential = await navigator.credentials.get({
-        publicKey: options.publicKey
+        publicKey: options.publicKey,
       });
       await verifyAssertion(credential);
     } catch (err) {
@@ -185,7 +185,7 @@ async function webAuthnRegisterRequest() {
 
   try {
     const credential = await navigator.credentials.create({
-      publicKey: options.publicKey
+      publicKey: options.publicKey,
     });
     await webauthnRegistered(credential);
   } catch (err) {
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index e7eb39f457..220c9e5512 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -148,7 +148,7 @@ export function initGlobalTooltips() {
   const observerConnect = (observer) => observer.observe(document, {
     subtree: true,
     childList: true,
-    attributeFilter: ['data-tooltip-content', 'title']
+    attributeFilter: ['data-tooltip-content', 'title'],
   });
   const observer = new MutationObserver((mutationList, observer) => {
     const pending = observer.takeRecords();
diff --git a/web_src/js/standalone/swagger.js b/web_src/js/standalone/swagger.js
index cb91089daf..00854ef5d7 100644
--- a/web_src/js/standalone/swagger.js
+++ b/web_src/js/standalone/swagger.js
@@ -21,11 +21,11 @@ window.addEventListener('load', async () => {
     docExpansion: 'none',
     defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
     presets: [
-      SwaggerUI.presets.apis
+      SwaggerUI.presets.apis,
     ],
     plugins: [
-      SwaggerUI.plugins.DownloadUrl
-    ]
+      SwaggerUI.plugins.DownloadUrl,
+    ],
   });
 
   window.ui = ui;
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 6ad06f599d..3544b47c3d 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -189,7 +189,7 @@ export const SvgIcon = {
     name: {type: String, required: true},
     size: {type: Number, default: 16},
     className: {type: String, default: ''},
-    symbolId: {type: String}
+    symbolId: {type: String},
   },
   render() {
     let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index aa7c2604aa..4a6adf478e 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -191,7 +191,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
       textarea.removeEventListener('mousemove', onUserResize);
       textarea.removeEventListener('input', resizeToFit);
       textarea.form?.removeEventListener('reset', onFormReset);
-    }
+    },
   };
 }
 
diff --git a/web_src/js/webcomponents/polyfills.js b/web_src/js/webcomponents/polyfills.js
index 88c7276881..38f50fa02f 100644
--- a/web_src/js/webcomponents/polyfills.js
+++ b/web_src/js/webcomponents/polyfills.js
@@ -9,7 +9,7 @@ try {
       return {
         format(value) {
           return ` ${value} ${options.unit}`;
-        }
+        },
       };
     }
     return intlNumberFormat(locales, options);
diff --git a/webpack.config.js b/webpack.config.js
index 321ae561a4..00952f90b4 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -178,7 +178,7 @@ export default {
                 ],
               },
             },
-          }
+          },
         ],
       },
       {
@@ -191,14 +191,14 @@ export default {
         type: 'asset/resource',
         generator: {
           filename: 'fonts/[name].[contenthash:8][ext]',
-        }
+        },
       },
       {
         test: /\.png$/i,
         type: 'asset/resource',
         generator: {
           filename: 'img/webpack/[name].[contenthash:8][ext]',
-        }
+        },
       },
     ],
   },

From 04f9ad056882fc3f21b247b16f84437adf0f36d8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 22 Mar 2024 20:51:29 +0100
Subject: [PATCH 479/679] Fix incorrect tailwind migration (#30007)

Fixes https://github.com/go-gitea/gitea/issues/30005. Regression from
https://github.com/go-gitea/gitea/pull/29945.

There was only once instance of `tw-content-center` before that PR, so I
just ran below command and reverted that one instance.

```sh
perl -p -i -e 's#tw-content-center#tw-items-center#g' web_src/js/**/* templates/**/* models/**/* tests/**/*
```
---
 templates/admin/emails/list.tmpl              |  2 +-
 templates/admin/org/list.tmpl                 |  2 +-
 templates/admin/repo/unadopted.tmpl           |  2 +-
 templates/admin/stacktrace-row.tmpl           |  4 +--
 templates/admin/stacktrace.tmpl               |  2 +-
 templates/base/head_navbar.tmpl               |  8 ++---
 templates/explore/search.tmpl                 |  2 +-
 templates/explore/user_list.tmpl              |  2 +-
 templates/org/header.tmpl                     |  2 +-
 templates/org/home.tmpl                       |  4 +--
 templates/org/member/members.tmpl             |  2 +-
 templates/org/settings/labels.tmpl            |  2 +-
 templates/org/team/members.tmpl               |  4 +--
 templates/org/team/repositories.tmpl          |  2 +-
 templates/projects/view.tmpl                  |  2 +-
 templates/repo/actions/list.tmpl              |  2 +-
 templates/repo/actions/runs_list.tmpl         |  2 +-
 templates/repo/actions/status.tmpl            |  2 +-
 templates/repo/blame.tmpl                     |  6 ++--
 templates/repo/branch/list.tmpl               |  8 ++---
 templates/repo/branch_dropdown.tmpl           |  2 +-
 .../code/recently_pushed_new_branches.tmpl    |  2 +-
 templates/repo/commit_page.tmpl               | 12 +++----
 templates/repo/commits.tmpl                   |  2 +-
 templates/repo/commits_list_small.tmpl        |  2 +-
 templates/repo/commits_table.tmpl             |  4 +--
 templates/repo/diff/box.tmpl                  | 16 ++++-----
 templates/repo/diff/comments.tmpl             |  6 ++--
 templates/repo/diff/conversation.tmpl         | 12 +++----
 templates/repo/diff/new_review.tmpl           |  2 +-
 templates/repo/find/files.tmpl                |  2 +-
 templates/repo/forks.tmpl                     |  2 +-
 templates/repo/graph/commits.tmpl             |  6 ++--
 templates/repo/header.tmpl                    |  2 +-
 templates/repo/home.tmpl                      |  6 ++--
 templates/repo/issue/card.tmpl                |  2 +-
 templates/repo/issue/filter_list.tmpl         |  2 +-
 templates/repo/issue/milestone_issues.tmpl    |  2 +-
 templates/repo/issue/milestones.tmpl          |  2 +-
 templates/repo/issue/view_content.tmpl        |  6 ++--
 .../repo/issue/view_content/attachments.tmpl  |  2 +-
 .../repo/issue/view_content/comments.tmpl     | 14 ++++----
 .../repo/issue/view_content/conversation.tmpl | 14 ++++----
 .../repo/issue/view_content/sidebar.tmpl      | 36 +++++++++----------
 templates/repo/migrate/migrate.tmpl           |  2 +-
 templates/repo/projects/view.tmpl             |  2 +-
 templates/repo/pulls/tab_menu.tmpl            |  2 +-
 templates/repo/release/list.tmpl              |  2 +-
 templates/repo/release_tag_header.tmpl        |  2 +-
 templates/repo/settings/branches.tmpl         |  2 +-
 templates/repo/settings/collaboration.tmpl    |  2 +-
 templates/repo/settings/options.tmpl          |  2 +-
 templates/repo/tag/list.tmpl                  |  8 ++---
 templates/repo/view_file.tmpl                 |  6 ++--
 templates/repo/wiki/pages.tmpl                |  2 +-
 templates/repo/wiki/view.tmpl                 |  2 +-
 templates/shared/searchbottom.tmpl            |  4 +--
 templates/shared/secrets/add_list.tmpl        |  2 +-
 templates/shared/user/org_profile_avatar.tmpl |  2 +-
 templates/shared/variables/variable_list.tmpl |  2 +-
 templates/user/auth/signin_inner.tmpl         |  4 +--
 templates/user/auth/signup_inner.tmpl         |  4 +--
 templates/user/auth/webauthn.tmpl             |  2 +-
 templates/user/dashboard/milestones.tmpl      |  2 +-
 .../user/notification/notification_div.tmpl   | 10 +++---
 .../settings/applications_oauth2_list.tmpl    |  2 +-
 templates/user/settings/security/openid.tmpl  |  2 +-
 templates/webhook/new.tmpl                    |  2 +-
 tests/integration/release_test.go             |  2 +-
 web_src/js/components/ActionRunStatus.vue     |  2 +-
 web_src/js/components/DashboardRepoList.vue   | 20 +++++------
 web_src/js/components/DiffFileList.vue        |  4 +--
 .../js/components/RepoBranchTagSelector.vue   |  2 +-
 web_src/js/components/RepoCodeFrequency.vue   |  2 +-
 web_src/js/components/RepoContributors.vue    |  2 +-
 web_src/js/components/RepoRecentCommits.vue   |  2 +-
 web_src/js/features/repo-issue-content.js     |  2 +-
 77 files changed, 165 insertions(+), 165 deletions(-)

diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 0b5249fbd9..b72aef8f35 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -4,7 +4,7 @@
 			{{ctx.Locale.Tr "admin.emails.email_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
 		</h4>
 		<div class="ui attached segment">
-			<div class="ui secondary filter menu tw-content-center gt-mx-0">
+			<div class="ui secondary filter menu tw-items-center gt-mx-0">
 				<form class="ui form ignore-dirty tw-flex-1">
 					{{template "shared/search/combo" dict "Value" .Keyword}}
 				</form>
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index abd43d297e..ca0ee30092 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -7,7 +7,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			<div class="ui secondary filter menu tw-content-center gt-mx-0">
+			<div class="ui secondary filter menu tw-items-center gt-mx-0">
 				<form class="ui form ignore-dirty tw-flex-1">
 					{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}}
 				</form>
diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl
index eb8188de14..9166a844a7 100644
--- a/templates/admin/repo/unadopted.tmpl
+++ b/templates/admin/repo/unadopted.tmpl
@@ -20,7 +20,7 @@
 				{{if .Dirs}}
 					<div class="ui aligned divided list">
 						{{range $dirI, $dir := .Dirs}}
-							<div class="item tw-flex tw-content-center">
+							<div class="item tw-flex tw-items-center">
 								<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
 								<div>
 									<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl
index fdce81eda7..3f639ba161 100644
--- a/templates/admin/stacktrace-row.tmpl
+++ b/templates/admin/stacktrace-row.tmpl
@@ -1,5 +1,5 @@
 <div class="item">
-	<div class="tw-flex tw-content-center">
+	<div class="tw-flex tw-items-center">
 		<div class="icon gt-ml-3 gt-mr-3">
 			{{if eq .Process.Type "request"}}
 				{{svg "octicon-globe" 16}}
@@ -40,7 +40,7 @@
 						</summary>
 						<div class="list">
 							{{range .Entry}}
-								<div class="item tw-flex tw-content-center">
+								<div class="item tw-flex tw-items-center">
 									<span class="icon gt-mr-4">{{svg "octicon-dot-fill" 16}}</span>
 									<div class="content tw-flex-1">
 										<div class="header"><code>{{.Function}}</code></div>
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index 3c13c1e9dd..e324570c96 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -1,7 +1,7 @@
 {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
 <div class="admin-setting-content">
 
-	<div class="tw-flex tw-content-center">
+	<div class="tw-flex tw-items-center">
 		<div class="tw-flex-1">
 			<div class="ui compact small menu">
 				<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 1c2a7b2d9a..50ca744457 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -56,7 +56,7 @@
 	<div class="navbar-right ui secondary menu">
 		{{if and .IsSigned .MustChangePassword}}
 			<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
-				<span class="text tw-flex tw-content-center">
+				<span class="text tw-flex tw-items-center">
 					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
 					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
@@ -83,8 +83,8 @@
 				<span class="only-mobile gt-ml-3">{{ctx.Locale.Tr "active_stopwatch"}}</span>
 			</a>
 			<div class="active-stopwatch-popup item tippy-target gt-p-3">
-				<div class="tw-flex tw-content-center">
-					<a class="stopwatch-link tw-flex tw-content-center" href="{{.ActiveStopwatch.IssueLink}}">
+				<div class="tw-flex tw-items-center">
+					<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
 						{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
 						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
 						<span class="ui primary label stopwatch-time gt-my-0 gt-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
@@ -142,7 +142,7 @@
 			</div><!-- end dropdown menu create new -->
 
 			<div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
-				<span class="text tw-flex tw-content-center">
+				<span class="text tw-flex tw-items-center">
 					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
 					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index 505fc64548..c12ff325f9 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -1,4 +1,4 @@
-<div class="ui small secondary filter menu tw-content-center gt-mx-0">
+<div class="ui small secondary filter menu tw-items-center gt-mx-0">
 	<form class="ui form ignore-dirty tw-flex-1">
 		{{if .PageIsExploreUsers}}
 			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl
index e49ca1d069..f2cf939ffb 100644
--- a/templates/explore/user_list.tmpl
+++ b/templates/explore/user_list.tmpl
@@ -1,6 +1,6 @@
 <div class="flex-list">
 	{{range .Users}}
-		<div class="flex-item tw-content-center">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{ctx.AvatarUtils.Avatar . 48}}
 			</div>
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index c8a0ad3ab0..204ba7e3c1 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -7,7 +7,7 @@
 				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
 				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
 			</span>
-			<span class="tw-flex tw-content-center gt-gap-2 tw-ml-auto gt-font-16 tw-whitespace-nowrap">
+			<span class="tw-flex tw-items-center gt-gap-2 tw-ml-auto gt-font-16 tw-whitespace-nowrap">
 				{{if .EnableFeed}}
 					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
 						{{svg "octicon-rss" 24}}
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index 04c6a65608..1277665804 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -27,7 +27,7 @@
 				{{if .NumMembers}}
 					<h4 class="ui top attached header tw-flex">
 						<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
-						<a class="text grey tw-flex tw-content-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
+						<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
 					</h4>
 					<div class="ui attached segment members">
 						{{$isMember := .IsOrganizationMember}}
@@ -41,7 +41,7 @@
 				{{if .IsOrganizationMember}}
 					<div class="ui top attached header tw-flex">
 						<strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong>
-						<a class="text grey tw-flex tw-content-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
+						<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
 					</div>
 					<div class="ui attached table segment teams">
 						{{range .Teams}}
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl
index cb9e60da29..4388dc9520 100644
--- a/templates/org/member/members.tmpl
+++ b/templates/org/member/members.tmpl
@@ -7,7 +7,7 @@
 		<div class="flex-list">
 			{{range .Members}}
 				{{$isPublic := index $.MembersIsPublicMember .ID}}
-				<div class="flex-item {{if $.PublicOnly}}tw-content-center{{end}}">
+				<div class="flex-item {{if $.PublicOnly}}tw-items-center{{end}}">
 					<div class="flex-item-leading">
 						<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
 					</div>
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl
index 19a7d5355e..25a562c975 100644
--- a/templates/org/settings/labels.tmpl
+++ b/templates/org/settings/labels.tmpl
@@ -1,6 +1,6 @@
 {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}}
 				<div class="org-setting-content">
-					<div class="tw-flex tw-content-center">
+					<div class="tw-flex tw-items-center">
 						<div class="tw-flex-1">
 							{{ctx.Locale.Tr "org.settings.labels_desc"}}
 						</div>
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index d86aeb7ce4..65430cbda3 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -24,7 +24,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Team.Members}}
-							<div class="flex-item tw-content-center">
+							<div class="flex-item tw-items-center">
 								<div class="flex-item-leading">
 									<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
 								</div>
@@ -56,7 +56,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Invites}}
-							<div class="flex-item tw-content-center">
+							<div class="flex-item tw-items-center">
 								<div class="flex-item-main">
 									{{.Email}}
 								</div>
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl
index 9efe8f9f09..0c59eafbe1 100644
--- a/templates/org/team/repositories.tmpl
+++ b/templates/org/team/repositories.tmpl
@@ -28,7 +28,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Team.Repos}}
-							<div class="flex-item tw-content-center">
+							<div class="flex-item tw-items-center">
 								<div class="flex-item-leading">
 									{{template "repo/icon" .}}
 								</div>
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index d36ecdfc85..93c2cdbb57 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -1,7 +1,7 @@
 {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
 
 <div class="ui container">
-	<div class="tw-flex tw-justify-between tw-content-center gt-mb-4">
+	<div class="tw-flex tw-justify-between tw-items-center gt-mb-4">
 		<h2 class="gt-mb-0">{{.Project.Title}}</h2>
 		{{if $canWriteProject}}
 			<div class="ui compact mini menu">
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index f4215829ba..916949d4f9 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -25,7 +25,7 @@
 				</div>
 			</div>
 			<div class="twelve wide column content">
-				<div class="ui secondary filter menu tw-justify-end tw-flex tw-content-center">
+				<div class="ui secondary filter menu tw-justify-end tw-flex tw-items-center">
 					<!-- Actor -->
 					<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
 						<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl
index b898837a26..d393df6539 100644
--- a/templates/repo/actions/runs_list.tmpl
+++ b/templates/repo/actions/runs_list.tmpl
@@ -6,7 +6,7 @@
 	</div>
 	{{end}}
 	{{range .Runs}}
-		<div class="flex-item tw-content-center">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{template "repo/actions/status" (dict "status" .Status.String)}}
 			</div>
diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl
index e42eafe8f6..a0e02cf8d7 100644
--- a/templates/repo/actions/status.tmpl
+++ b/templates/repo/actions/status.tmpl
@@ -12,7 +12,7 @@
 {{- $className = .className -}}
 {{- end -}}
 
-<span class="tw-flex tw-content-center" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
+<span class="tw-flex tw-items-center" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
 {{if eq .status "success"}}
 	{{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}}
 {{else if eq .status "skipped"}}
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 5bc9a1375e..05cdf53b44 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -11,11 +11,11 @@
 	{{end}}
 {{end}}
 <div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
-	<h4 class="file-header ui top attached header tw-flex tw-content-center tw-justify-between tw-flex-wrap">
-		<div class="file-header-left tw-flex tw-content-center gt-py-3 gt-pr-4">
+	<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
+		<div class="file-header-left tw-flex tw-items-center gt-py-3 gt-pr-4">
 			{{template "repo/file_info" .}}
 		</div>
-		<div class="file-header-right file-actions tw-flex tw-content-center tw-flex-wrap">
+		<div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap">
 			<div class="ui buttons">
 				<a class="ui tiny button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
 				{{if not .IsViewCommit}}
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 21121c4f09..7e061696e4 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -25,7 +25,7 @@
 									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
 								</div>
-								<p class="info tw-flex tw-content-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
+								<p class="info tw-flex tw-items-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
 							</td>
 							<td class="right aligned middle aligned overflow-visible">
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
@@ -67,8 +67,8 @@
 			</div>
 		{{end}}
 
-		<h4 class="ui top attached header tw-flex tw-content-center tw-justify-between">
-			<div class="tw-flex tw-content-center">
+		<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
+			<div class="tw-flex tw-items-center">
 				{{ctx.Locale.Tr "repo.branches"}}
 			</div>
 		</h4>
@@ -98,7 +98,7 @@
 									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
 								</div>
-								<p class="info tw-flex tw-content-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
+								<p class="info tw-flex tw-items-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
 							{{end}}
 							</td>
 							<td class="two wide ui">
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index 328429dd9e..418006a0f6 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -71,7 +71,7 @@
 	{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
 	<div class="ui dropdown custom">
 		<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex gt-m-0">
-			<span class="text tw-flex tw-content-center gt-mr-2">
+			<span class="text tw-flex tw-items-center gt-mr-2">
 				{{if .release}}
 					{{ctx.Locale.Tr "repo.release.compare"}}
 				{{else}}
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index 4d11d3f603..840a0e32ec 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -1,5 +1,5 @@
 {{range .RecentlyPushedNewBranches}}
-	<div class="ui positive message tw-flex tw-content-center">
+	<div class="ui positive message tw-flex tw-items-center">
 		<div class="tw-flex-1">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
 			{{$branchLink := HTMLFormat `<a href="%s/src/branch/%s">%s</a>` $.RepoLink (PathEscapeSegments .Name) .Name}}
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 1670781e24..345c28f475 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -139,8 +139,8 @@
 			{{end}}
 			{{template "repo/commit_load_branches_and_tags" .}}
 		</div>
-		<div class="ui attached segment tw-flex tw-content-center tw-justify-between gt-py-2 commit-header-row tw-flex-wrap {{$class}}">
-				<div class="tw-flex tw-content-center author">
+		<div class="ui attached segment tw-flex tw-items-center tw-justify-between gt-py-2 commit-header-row tw-flex-wrap {{$class}}">
+				<div class="tw-flex tw-items-center author">
 					{{if .Author}}
 						{{ctx.AvatarUtils.Avatar .Author 28 "gt-mr-3"}}
 						{{if .Author.FullName}}
@@ -164,7 +164,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="ui horizontal list tw-flex tw-content-center">
+				<div class="ui horizontal list tw-flex tw-items-center">
 					{{if .Parents}}
 						<div class="item">
 							<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
@@ -184,8 +184,8 @@
 				</div>
 		</div>
 		{{if .Commit.Signature}}
-			<div class="ui bottom attached message tw-text-left tw-flex tw-content-center tw-justify-between commit-header-row tw-flex-wrap gt-mb-0 {{$class}}">
-				<div class="tw-flex tw-content-center">
+			<div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap gt-mb-0 {{$class}}">
+				<div class="tw-flex tw-items-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
 							{{svg "gitea-lock" 16 "gt-mr-3"}}
@@ -209,7 +209,7 @@
 						<span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span>
 					{{end}}
 				</div>
-				<div class="tw-flex tw-content-center">
+				<div class="tw-flex tw-items-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
 							{{svg "octicon-verified" 16 "gt-mr-3"}}
diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl
index 210f73d456..ea173da7a5 100644
--- a/templates/repo/commits.tmpl
+++ b/templates/repo/commits.tmpl
@@ -4,7 +4,7 @@
 	<div class="ui container">
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
-			<div class="tw-flex tw-content-center">
+			<div class="tw-flex tw-items-center">
 				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
 				<a href="{{.RepoLink}}/graph" class="ui basic small compact button">
 					{{svg "octicon-git-branch"}}
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index 53834f7acb..b195f06483 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -13,7 +13,7 @@
 
 		{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
 
-		<span class="shabox tw-flex tw-content-center tw-float-right">
+		<span class="shabox tw-flex tw-items-center tw-float-right">
 			{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
 			{{$class := "ui sha label"}}
 			{{if .Signature}}
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index 330130ac0d..48e9368c65 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -1,5 +1,5 @@
-<h4 class="ui top attached header commits-table tw-flex tw-content-center tw-justify-between">
-	<div class="commits-table-left tw-flex tw-content-center">
+<h4 class="ui top attached header commits-table tw-flex tw-items-center tw-justify-between">
+	<div class="commits-table-left tw-flex tw-items-center">
 		{{if or .PageIsCommits (gt .CommitCount 0)}}
 			{{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}}
 		{{else if .IsNothingToCompare}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index d71ad1b2ad..42eaf9d30e 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -1,7 +1,7 @@
 {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
 <div>
 	<div class="diff-detail-box diff-box">
-		<div class="tw-flex tw-content-center tw-flex-wrap gt-gap-3 gt-ml-1">
+		<div class="tw-flex tw-items-center tw-flex-wrap gt-gap-3 gt-ml-1">
 			{{if $showFileTree}}
 				<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
 					{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
@@ -18,14 +18,14 @@
 				</script>
 			{{end}}
 			{{if not .DiffNotAvailable}}
-				<div class="diff-detail-stats tw-flex tw-content-center tw-flex-wrap">
+				<div class="diff-detail-stats tw-flex tw-items-center tw-flex-wrap">
 					{{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}}
 				</div>
 			{{end}}
 		</div>
 		<div class="diff-detail-actions">
 			{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}}
-				<div class="not-mobile tw-flex tw-content-center tw-flex-col tw-whitespace-nowrap gt-mr-2">
+				<div class="not-mobile tw-flex tw-items-center tw-flex-col tw-whitespace-nowrap gt-mr-2">
 					<label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{ctx.Locale.Tr "repo.pulls.viewed_files_label"}}">
 						{{ctx.Locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
 					</label>
@@ -110,8 +110,8 @@
 					{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
 					{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
 					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
-						<h4 class="diff-file-header sticky-2nd-row ui top attached normal header tw-flex tw-content-center tw-justify-between tw-flex-wrap">
-							<div class="diff-file-name tw-flex tw-content-center gt-gap-2 tw-flex-wrap">
+						<h4 class="diff-file-header sticky-2nd-row ui top attached normal header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
+							<div class="diff-file-name tw-flex tw-items-center gt-gap-2 tw-flex-wrap">
 								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
 										{{svg "octicon-chevron-right" 18}}
@@ -119,7 +119,7 @@
 										{{svg "octicon-chevron-down" 18}}
 									{{end}}
 								</button>
-								<div class="gt-font-semibold tw-flex tw-content-center gt-mono">
+								<div class="gt-font-semibold tw-flex tw-items-center gt-mono">
 									{{if $file.IsBin}}
 										<span class="gt-ml-1 gt-mr-3">
 											{{ctx.Locale.Tr "repo.diff.bin"}}
@@ -144,7 +144,7 @@
 									<span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
 								{{end}}
 							</div>
-							<div class="diff-file-header-actions tw-flex tw-content-center gt-gap-2 tw-flex-wrap">
+							<div class="diff-file-header-actions tw-flex tw-items-center gt-gap-2 tw-flex-wrap">
 								{{if $showFileViewToggle}}
 									<div class="ui compact icon buttons">
 										<button class="ui tiny basic button file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code"}}</button>
@@ -218,7 +218,7 @@
 
 				{{if .Diff.IsIncomplete}}
 					<div class="diff-file-box diff-box file-content gt-mt-3" id="diff-incomplete">
-						<h4 class="ui top attached normal header tw-flex tw-content-center tw-justify-between">
+						<h4 class="ui top attached normal header tw-flex tw-items-center tw-justify-between">
 							{{ctx.Locale.Tr "repo.diff.too_many_files"}}
 							<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
 						</h4>
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index aed01ef5fb..4b626ba2b0 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -8,8 +8,8 @@
 		{{template "shared/user/avatarlink" dict "user" .Poster}}
 	{{end}}
 	<div class="content comment-container">
-		<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between">
-			<div class="comment-header-left tw-flex tw-content-center">
+		<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between">
+			<div class="comment-header-left tw-flex tw-items-center">
 				{{if .OriginalAuthor}}
 					<span class="text black gt-font-semibold gt-mr-2">
 						{{svg (MigrationIcon $.root.Repository.GetOriginalURLHostname)}}
@@ -30,7 +30,7 @@
 					</span>
 				{{end}}
 			</div>
-			<div class="comment-header-right actions tw-flex tw-content-center">
+			<div class="comment-header-right actions tw-flex tw-items-center">
 				{{if .Invalidated}}
 					{{$referenceUrl := printf "%s#%s" $.root.Issue.Link .HashTag}}
 					<a href="{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index 1bc018e8e2..85bfe4923a 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -8,8 +8,8 @@
 	{{$referenceUrl := printf "%s#%s" $.Issue.Link $comment.HashTag}}
 	<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
 		{{if $resolved}}
-			<div class="ui attached header resolved-placeholder tw-flex tw-content-center tw-justify-between">
-				<div class="ui grey text tw-flex tw-content-center tw-flex-wrap gt-gap-2">
+			<div class="ui attached header resolved-placeholder tw-flex tw-items-center tw-justify-between">
+				<div class="ui grey text tw-flex tw-items-center tw-flex-wrap gt-gap-2">
 					{{svg "octicon-check" 16 "icon gt-mr-2"}}
 					<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
 					{{if $invalid}}
@@ -22,12 +22,12 @@
 						</a>
 					{{end}}
 				</div>
-				<div class="tw-flex tw-content-center gt-gap-3">
-					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-content-center">
+				<div class="tw-flex tw-items-center gt-gap-3">
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-items-center">
 						{{svg "octicon-unfold" 16 "gt-mr-3"}}
 						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
 					</button>
-					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-content-center gt-hidden">
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-items-center gt-hidden">
 						{{svg "octicon-fold" 16 "gt-mr-3"}}
 						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
 					</button>
@@ -40,7 +40,7 @@
 					{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
 				</ui>
 			</div>
-			<div class="tw-flex tw-justify-end tw-content-center tw-flex-wrap gt-mt-3">
+			<div class="tw-flex tw-justify-end tw-items-center tw-flex-wrap gt-mt-3">
 				<div class="ui buttons gt-mr-2">
 					<button class="ui icon tiny basic button previous-conversation">
 						{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl
index 9d1eef712d..9c824db0ad 100644
--- a/templates/repo/diff/new_review.tmpl
+++ b/templates/repo/diff/new_review.tmpl
@@ -10,7 +10,7 @@
 			<form class="ui form form-fetch-action" action="{{.Link}}/reviews/submit" method="post">
 				{{.CsrfTokenHtml}}
 				<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
-				<div class="field tw-flex tw-content-center">
+				<div class="field tw-flex tw-items-center">
 					<div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
 					<a class="muted close">{{svg "octicon-x" 16}}</a>
 				</div>
diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl
index de2c34a158..eebdcb2b1b 100644
--- a/templates/repo/find/files.tmpl
+++ b/templates/repo/find/files.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="tw-flex tw-content-center">
+		<div class="tw-flex tw-items-center">
 			<a href="{{$.RepoLink}}">{{.RepoName}}</a>
 			<span class="gt-mx-3">/</span>
 			<div class="ui input tw-flex-1">
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
index 0a4b369cdb..6acb89f367 100644
--- a/templates/repo/forks.tmpl
+++ b/templates/repo/forks.tmpl
@@ -6,7 +6,7 @@
 			{{ctx.Locale.Tr "repo.forks"}}
 		</h2>
 		{{range .Forks}}
-			<div class="tw-flex tw-content-center gt-py-3">
+			<div class="tw-flex tw-items-center gt-py-3">
 				<span class="gt-mr-2">{{ctx.AvatarUtils.Avatar .Owner}}</span>
 				<a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a>
 			</div>
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index fc7cf925ab..b22527c8ef 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -31,7 +31,7 @@
 					<span class="message tw-inline-block gt-ellipsis gt-mr-3">
 						<span>{{RenderCommitMessage $.Context $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
 					</span>
-					<span class="commit-refs tw-flex tw-content-center gt-mr-2">
+					<span class="commit-refs tw-flex tw-items-center gt-mr-2">
 						{{range $commit.Refs}}
 							{{$refGroup := .RefGroup}}
 							{{if eq $refGroup "pull"}}
@@ -58,7 +58,7 @@
 							{{end}}
 						{{end}}
 					</span>
-					<span class="author tw-flex tw-content-center gt-mr-3">
+					<span class="author tw-flex tw-items-center gt-mr-3">
 						{{$userName := $commit.Commit.Author.Name}}
 						{{if $commit.User}}
 							{{if $commit.User.FullName}}
@@ -71,7 +71,7 @@
 							{{$userName}}
 						{{end}}
 					</span>
-					<span class="time tw-flex tw-content-center">{{DateTime "full" $commit.Date}}</span>
+					<span class="time tw-flex tw-items-center">{{DateTime "full" $commit.Date}}</span>
 				{{end}}
 			</li>
 		{{end}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index b7ee38ae83..6edfee6c7c 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -2,7 +2,7 @@
 {{with .Repository}}
 	<div class="ui container">
 		<div class="repo-header">
-			<div class="flex-item tw-content-center">
+			<div class="flex-item tw-items-center">
 				<div class="flex-item-leading">
 					{{template "repo/icon" .}}
 				</div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 7dfc6cfd73..5a9e02ca60 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -27,7 +27,7 @@
 				</div>
 			{{end}}
 		</div>
-		<div class="tw-flex tw-content-center tw-flex-wrap gt-gap-2" id="repo-topics">
+		<div class="tw-flex tw-items-center tw-flex-wrap gt-gap-2" id="repo-topics">
 			{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
 			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg gt-font-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
 		</div>
@@ -61,7 +61,7 @@
 		{{end}}
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
-			<div class="tw-flex tw-content-center tw-flex-wrap gt-gap-y-3">
+			<div class="tw-flex tw-items-center tw-flex-wrap gt-gap-y-3">
 				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
 				{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 					{{$cmpBranch := ""}}
@@ -121,7 +121,7 @@
 					</span>
 				{{end}}
 			</div>
-			<div class="tw-flex tw-content-center">
+			<div class="tw-flex tw-items-center">
 				<!-- Only show clone panel in repository home page -->
 				{{if eq $n 0}}
 					<div class="clone-panel ui action tiny input">
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 47c44af9b8..d25a36c456 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -13,7 +13,7 @@
 			</div>
 			<a class="issue-card-title muted issue-title" href="{{.Link}}">{{.Title | RenderEmoji ctx | RenderCodeBlock}}</a>
 			{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
-				<a role="button" class="issue-card-unpin muted tw-flex tw-content-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
+				<a role="button" class="issue-card-unpin muted tw-flex tw-items-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
 					{{svg "octicon-x" 16}}
 				</a>
 			{{end}}
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index b5c950b121..f1fc61bccb 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -32,7 +32,7 @@
 				<div class="divider"></div>
 			{{end}}
 			{{$previousExclusiveScope = $exclusiveScope}}
-			<a class="item label-filter-item tw-flex tw-content-center" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
+			<a class="item label-filter-item tw-flex tw-items-center" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
 				{{if .IsExcluded}}
 					{{svg "octicon-circle-slash"}}
 				{{else if .IsSelected}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index e029bf6031..2028375c03 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -29,7 +29,7 @@
 		<div class="tw-flex tw-flex-col gt-gap-3">
 			<progress class="milestone-progress-big" value="{{.Milestone.Completeness}}" max="100"></progress>
 			<div class="tw-flex gt-gap-4">
-				<div classs="tw-flex tw-content-center">
+				<div classs="tw-flex tw-items-center">
 					{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}}
 					{{if .IsClosed}}
 						{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index af7dd70193..57b697d8fd 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -23,7 +23,7 @@
 							{{svg "octicon-milestone" 16}}
 							<a class="muted" href="{{$.RepoLink}}/milestone/{{.ID}}">{{.Name}}</a>
 						</h3>
-						<div class="tw-flex tw-content-center">
+						<div class="tw-flex tw-items-center">
 							<span class="gt-mr-3">{{.Completeness}}%</span>
 							<progress value="{{.Completeness}}" max="100"></progress>
 						</div>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index ff06d8c5bd..7966fc7e1c 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -20,8 +20,8 @@
 				</a>
 				{{end}}
 				<div class="content comment-container">
-					<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between" role="heading" aria-level="3">
-						<div class="comment-header-left tw-flex tw-content-center">
+					<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3">
+						<div class="comment-header-left tw-flex tw-items-center">
 							{{if .Issue.OriginalAuthor}}
 								<span class="text black gt-font-semibold">
 									{{svg (MigrationIcon .Repository.GetOriginalURLHostname)}}
@@ -43,7 +43,7 @@
 								</span>
 							{{end}}
 						</div>
-						<div class="comment-header-right actions tw-flex tw-content-center">
+						<div class="comment-header-right actions tw-flex tw-items-center">
 							{{template "repo/issue/view_content/show_role" dict "ShowRole" .Issue.ShowRole "IgnorePoster" true}}
 							{{if not $.Repository.IsArchived}}
 								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
index 151131366f..0635a201be 100644
--- a/templates/repo/issue/view_content/attachments.tmpl
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -18,7 +18,7 @@
 					<span><strong>{{.Name}}</strong></span>
 				</a>
 			</div>
-			<div class="gt-p-3 tw-flex tw-content-center">
+			<div class="gt-p-3 tw-flex tw-items-center">
 				<span class="ui text grey">{{.Size | FileSize}}</span>
 			</div>
 		</div>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 2e2ce0fc28..c0c4df925b 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -25,8 +25,8 @@
 				</a>
 			{{end}}
 				<div class="content comment-container">
-					<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between" role="heading" aria-level="3">
-						<div class="comment-header-left tw-flex tw-content-center">
+					<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3">
+						<div class="comment-header-left tw-flex tw-items-center">
 							{{if .OriginalAuthor}}
 								<span class="text black gt-font-semibold gt-mr-2">
 									{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
@@ -50,7 +50,7 @@
 								</span>
 							{{end}}
 						</div>
-						<div class="comment-header-right actions tw-flex tw-content-center">
+						<div class="comment-header-right actions tw-flex tw-items-center">
 							{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 							{{if not $.Repository.IsArchived}}
 								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -402,8 +402,8 @@
 				{{if or .Content .Attachments}}
 				<div class="timeline-item comment">
 					<div class="content comment-container">
-						<div class="ui top attached header comment-header tw-flex tw-content-center tw-justify-between">
-							<div class="comment-header-left tw-flex tw-content-center">
+						<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between">
+							<div class="comment-header-left tw-flex tw-items-center">
 								{{if gt .Poster.ID 0}}
 									<a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}">
 										{{ctx.AvatarUtils.Avatar .Poster 24}}
@@ -424,7 +424,7 @@
 									{{ctx.Locale.Tr "repo.issues.review.left_comment"}}
 								</span>
 							</div>
-							<div class="comment-header-right actions tw-flex tw-content-center">
+							<div class="comment-header-right actions tw-flex tw-items-center">
 								{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 								{{if not $.Repository.IsArchived}}
 									{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -623,7 +623,7 @@
 				{{if .Content}}
 					<div class="timeline-item comment">
 						<div class="content">
-							<div class="ui top attached header comment-header-left tw-flex tw-content-center arrow-top">
+							<div class="ui top attached header comment-header-left tw-flex tw-items-center arrow-top">
 								{{if gt .Poster.ID 0}}
 									<a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}">
 										{{ctx.AvatarUtils.Avatar .Poster 24}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 83c8fc6ca3..c0ffec36c3 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -6,8 +6,8 @@
 	{{$hasReview := and $comment.Review}}
 	{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
 	<div class="ui segments conversation-holder">
-		<div class="ui segment collapsible-comment-box gt-py-3 tw-flex tw-content-center tw-justify-between">
-			<div class="tw-flex tw-content-center">
+		<div class="ui segment collapsible-comment-box gt-py-3 tw-flex tw-items-center tw-justify-between">
+			<div class="tw-flex tw-items-center">
 				<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment gt-ml-3 gt-word-break">{{$comment.TreePath}}</a>
 				{{if $invalid}}
 					<span class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
@@ -17,7 +17,7 @@
 			</div>
 			<div>
 				{{if or $invalid $resolved}}
-					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-content-center">
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-items-center">
 						{{svg "octicon-unfold" 16 "gt-mr-3"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
@@ -25,7 +25,7 @@
 							{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
 						{{end}}
 					</button>
-					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-content-center">
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-items-center">
 						{{svg "octicon-fold" 16 "gt-mr-3"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
@@ -58,7 +58,7 @@
 					<div class="comment code-comment gt-pb-4" id="{{.HashTag}}">
 						<div class="content">
 							<div class="header comment-header">
-								<div class="comment-header-left tw-flex tw-content-center">
+								<div class="comment-header-left tw-flex tw-items-center">
 									{{if not .OriginalAuthor}}
 										<a class="avatar">
 											{{ctx.AvatarUtils.Avatar .Poster 20}}
@@ -79,7 +79,7 @@
 										{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdSubStr}}
 									</span>
 								</div>
-								<div class="comment-header-right actions tw-flex tw-content-center">
+								<div class="comment-header-right actions tw-flex tw-items-center">
 									{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 									{{if not $.Repository.IsArchived}}
 										{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -109,7 +109,7 @@
 					</div>
 				{{end}}
 			</div>
-			<div class="code-comment-buttons tw-flex tw-content-center tw-flex-wrap gt-mt-3 gt-mb-2 gt-mx-3">
+			<div class="code-comment-buttons tw-flex tw-items-center tw-flex-wrap gt-mt-3 gt-mb-2 gt-mx-3">
 				<div class="tw-flex-1">
 					{{if $resolved}}
 						<div class="ui grey text">
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index b8867c11e7..833b5aa92f 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -3,7 +3,7 @@
 	{{if .Issue.IsPull}}
 		<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
 		<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
-			<a class="text tw-flex tw-content-center muted">
+			<a class="text tw-flex tw-items-center muted">
 				<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
 				{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
 					{{svg "octicon-gear" 16 "gt-ml-2"}}
@@ -50,17 +50,17 @@
 			<span class="no-select item {{if or .OriginalReviews .PullReviewers}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
 			<div class="selected">
 				{{range .PullReviewers}}
-					<div class="item tw-flex tw-content-center gt-py-3">
-						<div class="tw-flex tw-content-center tw-flex-1">
+					<div class="item tw-flex tw-items-center gt-py-3">
+						<div class="tw-flex tw-items-center tw-flex-1">
 							{{if .User}}
 								<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "gt-mr-3"}}{{.User.GetDisplayName}}</a>
 							{{else if .Team}}
 								<span class="text">{{svg "octicon-people" 20 "gt-mr-3"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
 							{{end}}
 						</div>
-						<div class="tw-flex tw-content-center gt-gap-3">
+						<div class="tw-flex tw-items-center gt-gap-3">
 							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
-								<a href="#" class="ui muted icon tw-flex tw-content-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
+								<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
 									{{svg "octicon-x" 20}}
 								</a>
 								<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
@@ -99,14 +99,14 @@
 					</div>
 				{{end}}
 				{{range .OriginalReviews}}
-					<div class="item tw-flex tw-content-center gt-py-3">
-						<div class="tw-flex tw-content-center tw-flex-1">
+					<div class="item tw-flex tw-items-center gt-py-3">
+						<div class="tw-flex tw-items-center tw-flex-1">
 							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
 								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "gt-mr-3"}}
 								{{.OriginalAuthor}}
 							</a>
 						</div>
-						<div class="tw-flex tw-content-center gt-gap-3">
+						<div class="tw-flex tw-items-center gt-gap-3">
 							{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
 						</div>
 					</div>
@@ -361,7 +361,7 @@
 		</div>
 		{{if ne .Issue.DeadlineUnix 0}}
 			<p>
-				<div class="tw-flex tw-justify-between tw-content-center">
+				<div class="tw-flex tw-justify-between tw-items-center">
 					<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
 						{{svg "octicon-calendar" 16 "gt-mr-3"}}
 						{{DateTime "long" .Issue.DeadlineUnix.FormatDate}}
@@ -417,7 +417,7 @@
 				</span>
 				<div class="ui relaxed divided list">
 					{{range .BlockingDependencies}}
-						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-content-center tw-justify-between">
+						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 							<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 								<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 									#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
@@ -426,7 +426,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right tw-flex tw-content-center gt-m-2">
+							<div class="item-right tw-flex tw-items-center gt-m-2">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -436,7 +436,7 @@
 						</div>
 					{{end}}
 					{{if .BlockingDependenciesNotPermitted}}
-						<div class="item tw-flex tw-content-center tw-justify-between gt-ellipsis">
+						<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
 							<span>{{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span>
 						</div>
 					{{end}}
@@ -449,7 +449,7 @@
 				</span>
 				<div class="ui relaxed divided list">
 					{{range .BlockedByDependencies}}
-						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-content-center tw-justify-between">
+						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 							<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 								<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 									#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
@@ -458,7 +458,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right tw-flex tw-content-center gt-m-2">
+							<div class="item-right tw-flex tw-items-center gt-m-2">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -469,7 +469,7 @@
 					{{end}}
 					{{if $.CanCreateIssueDependencies}}
 						{{range .BlockedByDependenciesNotPermitted}}
-							<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-content-center tw-justify-between">
+							<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 								<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 									<div class="gt-ellipsis">
 										<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
@@ -481,7 +481,7 @@
 										{{.Repository.OwnerName}}/{{.Repository.Name}}
 									</div>
 								</div>
-								<div class="item-right tw-flex tw-content-center gt-m-2">
+								<div class="item-right tw-flex tw-items-center gt-m-2">
 									{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 										<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 											{{svg "octicon-trash" 16}}
@@ -491,7 +491,7 @@
 							</div>
 						{{end}}
 					{{else if .BlockedByDependenciesNotPermitted}}
-						<div class="item tw-flex tw-content-center tw-justify-between gt-ellipsis">
+						<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
 							<span>{{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span>
 						</div>
 					{{end}}
@@ -548,7 +548,7 @@
 	<div class="divider"></div>
 	<div class="ui equal width compact grid">
 		{{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
-		<div class="row tw-content-center" data-tooltip-content="{{$issueReferenceLink}}">
+		<div class="row tw-items-center" data-tooltip-content="{{$issueReferenceLink}}">
 			<span class="text column truncate">{{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}</span>
 			<button class="ui two wide button column gt-p-3" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
 		</div>
diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl
index c0336b9b97..32465bc394 100644
--- a/templates/repo/migrate/migrate.tmpl
+++ b/templates/repo/migrate/migrate.tmpl
@@ -5,7 +5,7 @@
 			{{template "repo/migrate/helper" .}}
 			<div class="ui cards migrate-entries">
 				{{range .Services}}
-					<a class="ui card migrate-entry tw-flex tw-content-center" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
+					<a class="ui card migrate-entry tw-flex tw-items-center" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
 						{{if eq .Name "github"}}
 							{{svg "octicon-mark-github" 184 "gt-p-4"}}
 						{{else if eq .Name "gitlab"}}
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl
index b227ce4439..eea1057a50 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 	{{template "repo/header" .}}
 	<div class="ui container padded">
-		<div class="tw-flex tw-justify-between tw-content-center gt-mb-4">
+		<div class="tw-flex tw-justify-between tw-items-center gt-mb-4">
 			{{template "repo/issue/navbar" .}}
 			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 		</div>
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index fb00acde32..2a653c7c69 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -15,7 +15,7 @@
 			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
-		<span class="item tw-ml-auto gt-pr-0 gt-font-bold tw-flex tw-content-center gt-gap-3">
+		<span class="item tw-ml-auto gt-pr-0 gt-font-bold tw-flex tw-items-center gt-gap-3">
 			<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
 			<span class="diff-stats-bar">
 				<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 29059ea4a4..6c77ee12f7 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -16,7 +16,7 @@
 						{{end}}
 					</div>
 					<div class="ui twelve wide column detail">
-						<div class="tw-flex tw-content-center tw-justify-between tw-flex-wrap gt-mb-3">
+						<div class="tw-flex tw-items-center tw-justify-between tw-flex-wrap gt-mb-3">
 							<h4 class="release-list-title gt-word-break">
 								{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>{{end}}
 								{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}}
diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl
index 18a3c8c6b8..cc69cecd6d 100644
--- a/templates/repo/release_tag_header.tmpl
+++ b/templates/repo/release_tag_header.tmpl
@@ -3,7 +3,7 @@
 
 {{if $canReadReleases}}
 	<div class="tw-flex">
-		<div class="tw-flex-1 tw-flex tw-content-center">
+		<div class="tw-flex-1 tw-flex tw-items-center">
 			<h2 class="ui compact small menu header small-menu-items">
 				<a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
 				{{if $canReadCode}}
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index 2610ae02fe..ee6bdfbf2f 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -41,7 +41,7 @@
 			<div class="ui attached segment">
 				<div class="flex-list">
 					{{range .ProtectedBranches}}
-						<div class="flex-item tw-content-center">
+						<div class="flex-item tw-items-center">
 							<div class="flex-item-main">
 								<div class="flex-item-title">
 									<div class="ui basic primary label">{{.RuleName}}</div>
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index 8783de2544..2a4ec577e7 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -7,7 +7,7 @@
 		<div class="ui attached segment">
 			<div class="flex-list">
 				{{range .Collaborators}}
-					<div class="flex-item tw-content-center">
+					<div class="flex-item tw-items-center">
 						<div class="flex-item-leading">
 							<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
 						</div>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index aa40a1cd1b..6fa7184265 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -837,7 +837,7 @@
 					</div>
 				</div>
 				{{if not .Repository.IsMirror}}
-					<div class="flex-item tw-content-center">
+					<div class="flex-item tw-items-center">
 						<div class="flex-item-main">
 							{{if .Repository.IsArchived}}
 								<div class="flex-item-title">{{ctx.Locale.Tr "repo.settings.unarchive.header"}}</div>
diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl
index 0348334623..06c02c5f75 100644
--- a/templates/repo/tag/list.tmpl
+++ b/templates/repo/tag/list.tmpl
@@ -5,7 +5,7 @@
 		{{template "base/alert" .}}
 		{{template "repo/release_tag_header" .}}
 		<h4 class="ui top attached header">
-			<div class="five wide column tw-flex tw-content-center">
+			<div class="five wide column tw-flex tw-items-center">
 				{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.tags"}}
 			</div>
 		</h4>
@@ -18,12 +18,12 @@
 							<td class="tag">
 								<h3 class="release-tag-name gt-mb-3">
 									{{if $canReadReleases}}
-										<a class="tw-flex tw-content-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
+										<a class="tw-flex tw-items-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
 									{{else}}
-										<a class="tw-flex tw-content-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
+										<a class="tw-flex tw-items-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
 									{{end}}
 								</h3>
-								<div class="download tw-flex tw-content-center">
+								<div class="download tw-flex tw-items-center">
 									{{if $.Permission.CanRead $.UnitTypeCode}}
 										{{if .CreatedUnix}}
 											<span class="gt-mr-3">{{svg "octicon-clock" 16 "gt-mr-2"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index d8ef710400..ebe82ff161 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -25,8 +25,8 @@
 		</div>
 	{{end}}
 
-	<h4 class="file-header ui top attached header tw-flex tw-content-center tw-justify-between tw-flex-wrap">
-		<div class="file-header-left tw-flex tw-content-center gt-py-3 gt-pr-4">
+	<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
+		<div class="file-header-left tw-flex tw-items-center gt-py-3 gt-pr-4">
 			{{if .ReadmeInList}}
 				{{svg "octicon-book" 16 "gt-mr-3"}}
 				<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong>
@@ -34,7 +34,7 @@
 				{{template "repo/file_info" .}}
 			{{end}}
 		</div>
-		<div class="file-header-right file-actions tw-flex tw-content-center tw-flex-wrap">
+		<div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap">
 			{{if .HasSourceRenderedToggle}}
 				<div class="ui compact icon buttons">
 					<a href="?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl
index fa7352e510..52bf165e38 100644
--- a/templates/repo/wiki/pages.tmpl
+++ b/templates/repo/wiki/pages.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository wiki pages">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<h2 class="ui header tw-flex tw-content-center tw-justify-between">
+		<h2 class="ui header tw-flex tw-items-center tw-justify-between">
 			<span>{{ctx.Locale.Tr "repo.wiki.pages"}}</span>
 			<span>
 				{{if and .CanWriteWiki (not .Repository.IsMirror)}}
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index aa05a97fb0..b5ae36f4f0 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -4,7 +4,7 @@
 	{{$title := .title}}
 	<div class="ui container">
 		<div class="repo-button-row">
-			<div class="tw-flex tw-content-center">
+			<div class="tw-flex tw-items-center">
 				<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 					<div class="ui basic small button">
 						<span class="text">
diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl
index b920e10bb2..43d6092e8d 100644
--- a/templates/shared/searchbottom.tmpl
+++ b/templates/shared/searchbottom.tmpl
@@ -1,5 +1,5 @@
-<div class="ui bottom attached table segment tw-flex tw-content-center tw-justify-between">
-		<div class="tw-flex tw-content-center gt-ml-4">
+<div class="ui bottom attached table segment tw-flex tw-items-center tw-justify-between">
+		<div class="tw-flex tw-items-center gt-ml-4">
 			{{if .result.Language}}
 					<i class="color-icon gt-mr-3" style="background-color: {{.result.Color}}"></i>{{.result.Language}}
 			{{end}}
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index ea36d0cec2..c943a1944d 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -14,7 +14,7 @@
 	{{if .Secrets}}
 	<div class="flex-list">
 		{{range .Secrets}}
-		<div class="flex-item tw-content-center">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{svg "octicon-key" 32}}
 			</div>
diff --git a/templates/shared/user/org_profile_avatar.tmpl b/templates/shared/user/org_profile_avatar.tmpl
index 07e7b8aed5..2ff1e40ca8 100644
--- a/templates/shared/user/org_profile_avatar.tmpl
+++ b/templates/shared/user/org_profile_avatar.tmpl
@@ -2,7 +2,7 @@
 	<div class="ui container">
 		<div class="ui vertically grid head">
 			<div class="column">
-				<div class="ui header tw-flex tw-content-center gt-word-break">
+				<div class="ui header tw-flex tw-items-center gt-word-break">
 					{{ctx.AvatarUtils.Avatar . 100}}
 					<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
 					<span class="org-visibility">
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index dc8c7d7a80..fc2ac98e29 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -16,7 +16,7 @@
 	{{if .Variables}}
 	<div class="flex-list">
 		{{range .Variables}}
-		<div class="flex-item tw-content-center">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{svg "octicon-pencil" 32}}
 			</div>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index f8eb81423c..1950f750ce 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -55,9 +55,9 @@
 	</div>
 	<div id="oauth2-login-navigator" class="gt-py-2">
 		<div class="tw-flex tw-flex-col tw-justify-center">
-			<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-content-center gt-gap-3">
+			<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center gt-gap-3">
 				{{range $provider := .OAuth2Providers}}
-					<a class="{{$provider.Name}} ui button tw-flex tw-content-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+					<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
 						{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 					</a>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index a911537996..b96e9bfb02 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -60,9 +60,9 @@
 			</div>
 			<div id="oauth2-login-navigator" class="gt-py-2">
 				<div class="tw-flex tw-flex-col tw-justify-center">
-					<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-content-center gt-gap-3">
+					<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center gt-gap-3">
 						{{range $provider := .OAuth2Providers}}
-							<a class="{{$provider.Name}} ui button tw-flex tw-content-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+							<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
 								{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 							</a>
diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl
index 375ebba9ae..7c543cb16d 100644
--- a/templates/user/auth/webauthn.tmpl
+++ b/templates/user/auth/webauthn.tmpl
@@ -10,7 +10,7 @@
 				{{template "base/alert" .}}
 				<p>{{ctx.Locale.Tr "webauthn_sign_in"}}</p>
 			</div>
-			<div class="ui attached segment tw-flex tw-content-center tw-justify-center gt-gap-2 gt-py-3">
+			<div class="ui attached segment tw-flex tw-items-center tw-justify-center gt-gap-2 gt-py-3">
 				<div class="is-loading" style="width: 40px; height: 40px"></div>
 				{{ctx.Locale.Tr "webauthn_press_button"}}
 			</div>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 05f2b30efb..3a260c3d10 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -79,7 +79,7 @@
 									{{svg "octicon-milestone" 16}}
 									<a class="muted" href="{{.Repo.Link}}/milestone/{{.ID}}">{{.Name}}</a>
 								</h3>
-								<div class="tw-flex tw-content-center">
+								<div class="tw-flex tw-items-center">
 									<span class="gt-mr-3">{{.Completeness}}%</span>
 									<progress value="{{.Completeness}}" max="100"></progress>
 								</div>
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index 9da9e16d93..371da129ce 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -1,7 +1,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
 	<div class="ui container">
 		{{$notificationUnreadCount := call .NotificationUnreadCount}}
-		<div class="tw-flex tw-content-center tw-justify-between gt-mb-4">
+		<div class="tw-flex tw-items-center tw-justify-between gt-mb-4">
 			<div class="small-menu-items ui compact tiny menu">
 				<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
 					{{ctx.Locale.Tr "notification.unread"}}
@@ -25,7 +25,7 @@
 		<div class="gt-p-0">
 			<div id="notification_table">
 				{{if not .Notifications}}
-					<div class="tw-flex tw-content-center tw-flex-col gt-p-4">
+					<div class="tw-flex tw-items-center tw-flex-col gt-p-4">
 						{{svg "octicon-inbox" 56 "gt-mb-4"}}
 						{{if eq .Status 1}}
 							{{ctx.Locale.Tr "notification.no_unread"}}
@@ -35,7 +35,7 @@
 					</div>
 				{{else}}
 					{{range $notification := .Notifications}}
-						<div class="notifications-item tw-flex tw-content-center tw-flex-wrap gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
+						<div class="notifications-item tw-flex tw-items-center tw-flex-wrap gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
 							<div class="notifications-icon gt-ml-3 gt-mr-2 tw-self-start gt-mt-2">
 								{{if .Issue}}
 									{{template "shared/issueicon" .Issue}}
@@ -60,14 +60,14 @@
 									</span>
 								</div>
 							</a>
-							<div class="notifications-updated tw-content-center gt-mr-3">
+							<div class="notifications-updated tw-items-center gt-mr-3">
 								{{if .Issue}}
 									{{TimeSinceUnix .Issue.UpdatedUnix ctx.Locale}}
 								{{else}}
 									{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
 								{{end}}
 							</div>
-							<div class="notifications-buttons tw-content-center tw-justify-end gt-gap-2 gt-px-2">
+							<div class="notifications-buttons tw-items-center tw-justify-end gt-gap-2 gt-px-2">
 								{{if ne .Status 3}}
 									<form action="{{AppSubUrl}}/notifications/status" method="post">
 										{{$.CsrfTokenHtml}}
diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl
index 1125a66d47..bfbebb104d 100644
--- a/templates/user/settings/applications_oauth2_list.tmpl
+++ b/templates/user/settings/applications_oauth2_list.tmpl
@@ -4,7 +4,7 @@
 			{{ctx.Locale.Tr "settings.oauth2_application_create_description"}}
 		</div>
 		{{range .Applications}}
-			<div class="flex-item tw-content-center">
+			<div class="flex-item tw-items-center">
 				<div class="flex-item-leading">
 					{{svg "octicon-apps" 32}}
 				</div>
diff --git a/templates/user/settings/security/openid.tmpl b/templates/user/settings/security/openid.tmpl
index 63bc56ba9b..b0473c9df5 100644
--- a/templates/user/settings/security/openid.tmpl
+++ b/templates/user/settings/security/openid.tmpl
@@ -7,7 +7,7 @@
 			{{ctx.Locale.Tr "settings.openid_desc"}}
 		</div>
 		{{range .OpenIDs}}
-			<div class="flex-item tw-content-center">
+			<div class="flex-item tw-items-center">
 				<div class="flex-item-leading">
 					{{svg "fontawesome-openid" 20}}
 				</div>
diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl
index e0d04c1767..8ef33df25b 100644
--- a/templates/webhook/new.tmpl
+++ b/templates/webhook/new.tmpl
@@ -1,7 +1,7 @@
 <h4 class="ui top attached header">
 	{{.CustomHeaderTitle}}
 	<div class="ui right type dropdown">
-		<div class="text tw-flex tw-content-center">
+		<div class="text tw-flex tw-items-center">
 			{{template "shared/webhook/icon" (dict "Size" 20 "HookType" .ctxData.HookType)}}
 			{{ctx.Locale.Tr (print "repo.settings.web_hook_name_" .ctxData.HookType)}}
 		</div>
diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go
index b5af43e492..ce0c440167 100644
--- a/tests/integration/release_test.go
+++ b/tests/integration/release_test.go
@@ -234,7 +234,7 @@ func TestViewTagsList(t *testing.T) {
 
 	tagNames := make([]string, 0, 5)
 	tags.Each(func(i int, s *goquery.Selection) {
-		tagNames = append(tagNames, s.Find(".tag a.tw-flex.tw-content-center").Text())
+		tagNames = append(tagNames, s.Find(".tag a.tw-flex.tw-items-center").Text())
 	})
 
 	assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue
index 4eccddffdf..7ada543fea 100644
--- a/web_src/js/components/ActionRunStatus.vue
+++ b/web_src/js/components/ActionRunStatus.vue
@@ -28,7 +28,7 @@ export default {
 };
 </script>
 <template>
-  <span class="tw-flex tw-content-center" :data-tooltip-content="localeStatus" v-if="status">
+  <span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status">
     <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
     <SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
     <SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index f4edce955a..0f8b43b395 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -345,12 +345,12 @@ export default sfc; // activate the IDE's Vue plugin
       <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textOrganization }}</a>
     </div>
     <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
-      <h4 class="ui top attached header tw-flex tw-content-center">
-        <div class="tw-flex-1 tw-flex tw-content-center">
+      <h4 class="ui top attached header tw-flex tw-items-center">
+        <div class="tw-flex-1 tw-flex tw-items-center">
           {{ textMyRepos }}
           <span class="ui grey label gt-ml-3">{{ reposTotalCount }}</span>
         </div>
-        <a class="tw-flex tw-content-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
+        <a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
           <svg-icon name="octicon-plus"/>
         </a>
       </h4>
@@ -411,7 +411,7 @@ export default sfc; // activate the IDE's Vue plugin
       </div>
       <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="tw-flex tw-content-center gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
+          <li class="tw-flex tw-items-center gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
             <a class="repo-list-link muted" :href="repo.link">
               <svg-icon :name="repoIcon(repo)" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ repo.full_name }}</div>
@@ -419,7 +419,7 @@ export default sfc; // activate the IDE's Vue plugin
                 <svg-icon name="octicon-archive" :size="16"/>
               </div>
             </a>
-            <a class="tw-flex tw-content-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
+            <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
               <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
               <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'gt-ml-3 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
             </a>
@@ -458,18 +458,18 @@ export default sfc; // activate the IDE's Vue plugin
       </div>
     </div>
     <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs">
-      <h4 class="ui top attached header tw-flex tw-content-center">
-        <div class="tw-flex-1 tw-flex tw-content-center">
+      <h4 class="ui top attached header tw-flex tw-items-center">
+        <div class="tw-flex-1 tw-flex tw-items-center">
           {{ textMyOrgs }}
           <span class="ui grey label gt-ml-3">{{ organizationsTotalCount }}</span>
         </div>
-        <a class="tw-flex tw-content-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
+        <a class="tw-flex tw-items-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
           <svg-icon name="octicon-plus"/>
         </a>
       </h4>
       <div v-if="organizations.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="tw-flex tw-content-center gt-py-3" v-for="org in organizations" :key="org.name">
+          <li class="tw-flex tw-items-center gt-py-3" v-for="org in organizations" :key="org.name">
             <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
               <svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ org.name }}</div>
@@ -479,7 +479,7 @@ export default sfc; // activate the IDE's Vue plugin
                 </span>
               </div>
             </a>
-            <div class="text light grey tw-flex tw-content-center gt-ml-3">
+            <div class="text light grey tw-flex tw-items-center gt-ml-3">
               {{ org.num_repos }}
               <svg-icon name="octicon-repo" :size="16" class-name="gt-ml-2 gt-mt-1"/>
             </div>
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 64493b348a..3a0e287808 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -38,7 +38,7 @@ export default {
 <template>
   <ol class="diff-stats gt-m-0" ref="root" v-if="store.fileListIsVisible">
     <li v-for="file in store.files" :key="file.NameHash">
-      <div class="gt-font-semibold tw-flex tw-content-center pull-right">
+      <div class="gt-font-semibold tw-flex tw-items-center pull-right">
         <span v-if="file.IsBin" class="gt-ml-1 gt-mr-3">{{ store.binaryFileMessage }}</span>
         {{ file.IsBin ? '' : file.Addition + file.Deletion }}
         <span v-if="!file.IsBin" class="diff-stats-bar gt-mx-3" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
@@ -50,7 +50,7 @@ export default {
       <a class="file gt-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
     </li>
     <li v-if="store.isIncomplete" class="gt-pt-2">
-      <span class="file tw-flex tw-content-center tw-justify-between">{{ store.tooManyFilesMessage }}
+      <span class="file tw-flex tw-items-center tw-justify-between">{{ store.tooManyFilesMessage }}
         <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
       </span>
     </li>
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index e3e0c13fb9..588c05990a 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -246,7 +246,7 @@ export default sfc; // activate IDE's Vue plugin
 <template>
   <div class="ui dropdown custom">
     <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex gt-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
-      <span class="text tw-flex tw-content-center gt-mr-2">
+      <span class="text tw-flex tw-items-center gt-mr-2">
         <template v-if="release">{{ textReleaseCompare }}</template>
         <template v-else>
           <svg-icon v-if="isViewTag" name="octicon-tag"/>
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
index 4e11d51a4a..f51dac0a6d 100644
--- a/web_src/js/components/RepoCodeFrequency.vue
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -144,7 +144,7 @@ export default {
 </script>
 <template>
   <div>
-    <div class="ui header tw-flex tw-content-center tw-justify-between">
+    <div class="ui header tw-flex tw-items-center tw-justify-between">
       {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
     </div>
     <div class="tw-flex ui segment main-graph">
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 731b21bea3..adaf7f28f1 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -303,7 +303,7 @@ export default {
 </script>
 <template>
   <div>
-    <div class="ui header tw-flex tw-content-center tw-justify-between">
+    <div class="ui header tw-flex tw-items-center tw-justify-between">
       <div>
         <relative-time
           v-if="xAxisMin > 0"
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
index 1818d57943..601252419a 100644
--- a/web_src/js/components/RepoRecentCommits.vue
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -121,7 +121,7 @@ export default {
 </script>
 <template>
   <div>
-    <div class="ui header tw-flex tw-content-center tw-justify-between">
+    <div class="ui header tw-flex tw-items-center tw-justify-between">
       {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
     </div>
     <div class="tw-flex ui segment main-graph">
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index 33ea55f027..e7768b066e 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -16,7 +16,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
   $dialog = $(`
 <div class="ui modal content-history-detail-dialog">
   ${svg('octicon-x', 16, 'close icon inside')}
-  <div class="header tw-flex tw-content-center tw-justify-between">
+  <div class="header tw-flex tw-items-center tw-justify-between">
     <div>${itemTitleHtml}</div>
     <div class="ui dropdown dialog-header-options gt-mr-5 gt-hidden">
       ${i18nTextOptions}

From 5c91d7920f4aff08768e274269e211e926aa3d36 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 22 Mar 2024 21:56:38 +0200
Subject: [PATCH 480/679] Remove jQuery `.attr` from the project page (#30004)

- Switched from jQuery `.attr` to plain javascript `.getAttribute`
- Tested the issue movement between columns, column background color
setting, and column deletion. It all works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-projects.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index e95d875ec4..fc688bb695 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -33,7 +33,7 @@ async function moveIssue({item, from, to, oldIndex}) {
 
   const columnSorting = {
     issues: Array.from(columnCards, (card, i) => ({
-      issueID: parseInt($(card).attr('data-issue')),
+      issueID: parseInt(card.getAttribute('data-issue')),
       sorting: i,
     })),
   };
@@ -134,7 +134,7 @@ export function initRepoProject() {
         if ($projectColorInput.val()) {
           setLabelColor($projectHeader, $projectColorInput.val());
         }
-        $boardColumn.attr('style', `background: ${$projectColorInput.val()}!important`);
+        $boardColumn[0].style = `background: ${$projectColorInput.val()} !important`;
         $('.ui.modal').modal('hide');
       }
     });
@@ -159,9 +159,9 @@ export function initRepoProject() {
   });
 
   $('.show-delete-project-column-modal').each(function () {
-    const $deleteColumnModal = $(`${$(this).attr('data-modal')}`);
+    const $deleteColumnModal = $(`${this.getAttribute('data-modal')}`);
     const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button');
-    const deleteUrl = $(this).attr('data-url');
+    const deleteUrl = this.getAttribute('data-url');
 
     $deleteColumnButton.on('click', async (e) => {
       e.preventDefault();

From dade40407e333fbc6811bdd738c3d191b5dc0ef1 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 01:17:28 +0200
Subject: [PATCH 481/679] Remove jQuery from the citation modal (except
 fomantic) (#30008)

- Switched to plain JavaScript
- Tested the citation modal functionality and it works as before

# Demo using JavaScript without jQuery

![demo](https://github.com/go-gitea/gitea/assets/20454870/65bba1eb-dd4c-477f-8a2d-08e65f1e9f42)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/citation.js | 40 +++++++++++++++++----------------
 1 file changed, 21 insertions(+), 19 deletions(-)

diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js
index 49992b225f..918a467136 100644
--- a/web_src/js/features/citation.js
+++ b/web_src/js/features/citation.js
@@ -1,8 +1,9 @@
 import $ from 'jquery';
+import {getCurrentLocale} from '../utils.js';
 
 const {pageData} = window.config;
 
-async function initInputCitationValue($citationCopyApa, $citationCopyBibtex) {
+async function initInputCitationValue(citationCopyApa, citationCopyBibtex) {
   const [{Cite, plugins}] = await Promise.all([
     import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
     import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
@@ -14,11 +15,11 @@ async function initInputCitationValue($citationCopyApa, $citationCopyBibtex) {
   config.constants.fieldTypes.doi = ['field', 'literal'];
   config.constants.fieldTypes.version = ['field', 'literal'];
   const citationFormatter = new Cite(citationFileContent);
-  const lang = document.documentElement.lang || 'en-US';
+  const lang = getCurrentLocale() || 'en-US';
   const apaOutput = citationFormatter.format('bibliography', {template: 'apa', lang});
   const bibtexOutput = citationFormatter.format('bibtex', {lang});
-  $citationCopyBibtex.attr('data-text', bibtexOutput);
-  $citationCopyApa.attr('data-text', apaOutput);
+  citationCopyBibtex.setAttribute('data-text', bibtexOutput);
+  citationCopyApa.setAttribute('data-text', apaOutput);
 }
 
 export async function initCitationFileCopyContent() {
@@ -26,44 +27,45 @@ export async function initCitationFileCopyContent() {
 
   if (!pageData.citationFileContent) return;
 
-  const $citationCopyApa = $('#citation-copy-apa');
-  const $citationCopyBibtex = $('#citation-copy-bibtex');
-  const $inputContent = $('#citation-copy-content');
+  const citationCopyApa = document.getElementById('citation-copy-apa');
+  const citationCopyBibtex = document.getElementById('citation-copy-bibtex');
+  const inputContent = document.getElementById('citation-copy-content');
+
+  if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return;
 
-  if ((!$citationCopyApa.length && !$citationCopyBibtex.length) || !$inputContent.length) return;
   const updateUi = () => {
     const isBibtex = (localStorage.getItem('citation-copy-format') || defaultCitationFormat) === 'bibtex';
-    const copyContent = (isBibtex ? $citationCopyBibtex : $citationCopyApa).attr('data-text');
-
-    $inputContent.val(copyContent);
-    $citationCopyBibtex.toggleClass('primary', isBibtex);
-    $citationCopyApa.toggleClass('primary', !isBibtex);
+    const copyContent = (isBibtex ? citationCopyBibtex : citationCopyApa).getAttribute('data-text');
+    inputContent.value = copyContent;
+    citationCopyBibtex.classList.toggle('primary', isBibtex);
+    citationCopyApa.classList.toggle('primary', !isBibtex);
   };
 
-  $('#cite-repo-button').on('click', async (e) => {
+  document.getElementById('cite-repo-button')?.addEventListener('click', async (e) => {
     const dropdownBtn = e.target.closest('.ui.dropdown.button');
     dropdownBtn.classList.add('is-loading');
 
     try {
       try {
-        await initInputCitationValue($citationCopyApa, $citationCopyBibtex);
+        await initInputCitationValue(citationCopyApa, citationCopyBibtex);
       } catch (e) {
         console.error(`initCitationFileCopyContent error: ${e}`, e);
         return;
       }
       updateUi();
 
-      $citationCopyApa.on('click', () => {
+      citationCopyApa.addEventListener('click', () => {
         localStorage.setItem('citation-copy-format', 'apa');
         updateUi();
       });
-      $citationCopyBibtex.on('click', () => {
+
+      citationCopyBibtex.addEventListener('click', () => {
         localStorage.setItem('citation-copy-format', 'bibtex');
         updateUi();
       });
 
-      $inputContent.on('click', () => {
-        $inputContent.trigger('select');
+      inputContent.addEventListener('click', () => {
+        inputContent.select();
       });
     } finally {
       dropdownBtn.classList.remove('is-loading');

From d4ac1bd26e3ebc8e3bc7e84313e566634b672477 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 01:22:01 +0200
Subject: [PATCH 482/679] Remove jQuery `.attr` from the commit graph (#30006)

Switched from jQuery `.attr` to plain javascript `.getAttribute` and
`.setAttribute`

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-graph.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
index 4fbf95b9d5..f2c0d78f34 100644
--- a/web_src/js/features/repo-graph.js
+++ b/web_src/js/features/repo-graph.js
@@ -18,13 +18,13 @@ export function initRepoGraphGit() {
       window.history.replaceState({}, '', window.location.pathname);
     }
     $('.pagination a').each((_, that) => {
-      const href = $(that).attr('href');
+      const href = that.getAttribute('href');
       if (!href) return;
       const url = new URL(href, window.location);
       const params = url.searchParams;
       params.set('mode', 'monochrome');
       url.search = `?${params.toString()}`;
-      $(that).attr('href', url.href);
+      that.setAttribute('href', url.href);
     });
   });
   $('#flow-color-colored').on('click', () => {
@@ -32,13 +32,13 @@ export function initRepoGraphGit() {
     $('#flow-color-monochrome').removeClass('active');
     $('#git-graph-container').addClass('colored').removeClass('monochrome');
     $('.pagination a').each((_, that) => {
-      const href = $(that).attr('href');
+      const href = that.getAttribute('href');
       if (!href) return;
       const url = new URL(href, window.location);
       const params = url.searchParams;
       params.delete('mode');
       url.search = `?${params.toString()}`;
-      $(that).attr('href', url.href);
+      that.setAttribute('href', url.href);
     });
     const params = new URLSearchParams(window.location.search);
     params.delete('mode');

From bc92478575d9c1e84aa4ba4052dffcdc109a0323 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 01:26:56 +0200
Subject: [PATCH 483/679] Remove jQuery `.attr` from the branch/tag selector
 (#30010)

- Switched from jQuery `.attr` to plain javascript `.setAttribute`
- Tested the cherry-pick from the branch/tag selector and it works as
before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/components/RepoBranchTagSelector.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index 588c05990a..34e8859609 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -83,7 +83,7 @@ const sfc = {
         this.isViewBranch = false;
         this.$refs.dropdownRefName.textContent = item.name;
         if (this.setAction) {
-          $(`#${this.branchForm}`).attr('action', url);
+          document.getElementById(this.branchForm)?.setAttribute('action', url);
         } else {
           $(`#${this.branchForm} input[name="refURL"]`).val(url);
         }

From 3ccda41a539b8ba7841919ee12dc2877ddc03818 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 23 Mar 2024 00:54:09 +0100
Subject: [PATCH 484/679] Introduce `.secondary-nav` and handle `.page-content`
 spacing universally (#29982)

Fixes: https://github.com/go-gitea/gitea/issues/29981. Introduce
`.secondary-nav` as a universal way for styling and margin adjustments
inside `.page-content`.

If the first child of `.page-content` is `.secondary-nav`, we add margin
below it, otherwise we add padding to the first child. Notable changes:

- `--color-header-wrapper` is replaced with `--color-secondary-nav-bg`.
- `navbar` class is removed.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/admin/layout_head.tmpl              |  2 +-
 templates/explore/navbar.tmpl                 |  2 +-
 templates/repo/header.tmpl                    |  4 ++--
 templates/user/auth/link_account.tmpl         |  2 +-
 templates/user/auth/signin_navbar.tmpl        |  2 +-
 templates/user/auth/signup_openid_navbar.tmpl |  2 +-
 templates/user/dashboard/navbar.tmpl          |  3 +--
 tests/e2e/example.test.e2e.js                 |  2 +-
 tests/e2e/utils_e2e.js                        |  2 +-
 web_src/css/base.css                          | 19 ++++++++++++++-----
 web_src/css/dashboard.css                     |  7 +++----
 web_src/css/explore.css                       |  6 ++----
 web_src/css/modules/navbar.css                |  4 ++++
 web_src/css/repo.css                          | 10 ----------
 web_src/css/repo/header.css                   | 11 +++++------
 web_src/css/themes/theme-gitea-dark.css       |  2 +-
 web_src/css/themes/theme-gitea-light.css      |  2 +-
 web_src/css/user.css                          |  4 ----
 18 files changed, 40 insertions(+), 46 deletions(-)

diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl
index b326c82a6c..c1f5fb3314 100644
--- a/templates/admin/layout_head.tmpl
+++ b/templates/admin/layout_head.tmpl
@@ -1,6 +1,6 @@
 {{template "base/head" .ctxData}}
 <div role="main" aria-label="{{.ctxData.Title}}" class="page-content {{.pageClass}}">
-	<div class="ui container gt-mb-4">
+	<div class="ui container">
 		{{template "base/alert" .ctxData}}
 	</div>
 	<div class="ui container fluid padded flex-container">
diff --git a/templates/explore/navbar.tmpl b/templates/explore/navbar.tmpl
index 8841613b9f..8e619fa66f 100644
--- a/templates/explore/navbar.tmpl
+++ b/templates/explore/navbar.tmpl
@@ -1,4 +1,4 @@
-<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav">
 	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsExploreRepositories}}active {{end}}item" href="{{AppSubUrl}}/explore/repos">
 			{{svg "octicon-repo"}} {{ctx.Locale.Tr "explore.repos"}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 6edfee6c7c..187a56bdcc 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -1,4 +1,4 @@
-<div class="header-wrapper">
+<div class="secondary-nav">
 {{with .Repository}}
 	<div class="ui container">
 		<div class="repo-header">
@@ -128,7 +128,7 @@
 		{{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}}
 	</div>
 {{end}}
-	<overflow-menu class="ui container secondary pointing tabular top attached borderless menu navbar tw-pt-0 tw-my-0">
+	<overflow-menu class="ui container secondary pointing tabular top attached borderless menu tw-pt-0 tw-my-0">
 		{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}}
 			<div class="overflow-menu-items">
 				{{if .Permission.CanRead $.UnitTypeCode}}
diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl
index 81ea92c959..8dd49ccd60 100644
--- a/templates/user/auth/link_account.tmpl
+++ b/templates/user/auth/link_account.tmpl
@@ -1,6 +1,6 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content user link-account">
-	<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar tw-bg-header-wrapper">
+	<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav">
 		<div class="overflow-menu-items tw-justify-center">
 			<!-- TODO handle .ShowRegistrationButton once other login bugs are fixed -->
 			{{if not .AllowOnlyInternalRegistration}}
diff --git a/templates/user/auth/signin_navbar.tmpl b/templates/user/auth/signin_navbar.tmpl
index a576883065..7f52185a7d 100644
--- a/templates/user/auth/signin_navbar.tmpl
+++ b/templates/user/auth/signin_navbar.tmpl
@@ -1,5 +1,5 @@
 {{if or .EnableOpenIDSignIn .EnableSSPI}}
-<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar tw-bg-header-wrapper">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar secondary-nav">
 	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsLogin}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login">
 			{{ctx.Locale.Tr "auth.login_userpass"}}
diff --git a/templates/user/auth/signup_openid_navbar.tmpl b/templates/user/auth/signup_openid_navbar.tmpl
index 9cf81b048f..89068ddde1 100644
--- a/templates/user/auth/signup_openid_navbar.tmpl
+++ b/templates/user/auth/signup_openid_navbar.tmpl
@@ -1,4 +1,4 @@
-<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar tw-bg-header-wrapper">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav">
 	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsOpenIDConnect}}active {{end}}item" href="{{AppSubUrl}}/user/openid/connect">
 			{{ctx.Locale.Tr "auth.openid_connect_title"}}
diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl
index 917e024a6f..464228289e 100644
--- a/templates/user/dashboard/navbar.tmpl
+++ b/templates/user/dashboard/navbar.tmpl
@@ -1,4 +1,4 @@
-<div class="dashboard-navbar">
+<div class="secondary-nav tw-border-b tw-border-b-secondary">
 	<div class="ui secondary stackable menu">
 		<div class="item">
 			<div class="ui floating dropdown jump">
@@ -105,4 +105,3 @@
 	{{end}}
 	</div>
 </div>
-<div class="divider tw-mt-0"></div>
diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.js
index c741663a38..57c69a2917 100644
--- a/tests/e2e/example.test.e2e.js
+++ b/tests/e2e/example.test.e2e.js
@@ -23,7 +23,7 @@ test('Test Register Form', async ({page}, workerInfo) => {
   await page.click('form button.ui.primary.button:visible');
   // Make sure we routed to the home page. Else login failed.
   await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
-  await expect(page.locator('.dashboard-navbar span>img.ui.avatar')).toBeVisible();
+  await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
   await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
 
   save_visual(page);
diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.js
index fba13ab426..d60c78b16e 100644
--- a/tests/e2e/utils_e2e.js
+++ b/tests/e2e/utils_e2e.js
@@ -52,7 +52,7 @@ export async function save_visual(page) {
       fullPage: true,
       timeout: 20000,
       mask: [
-        page.locator('.dashboard-navbar span>img.ui.avatar'),
+        page.locator('.secondary-nav span>img.ui.avatar'),
         page.locator('.ui.dropdown.jump.item span>img.ui.avatar'),
       ],
     });
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 71e61eeb41..dba379e7c8 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -738,10 +738,16 @@ img.ui.avatar,
   padding-bottom: 80px;
 }
 
-.page-content.new:is(.repo,.migrate,.org),
-.page-content.profile:is(.user,.organization),
-.page-content.user:is(.settings,.notification) {
-  padding-top: 15px;
+/* add margin below .secondary nav when it is the first child */
+.page-content > :first-child.secondary-nav {
+  margin-bottom: 14px;
+}
+
+/* add padding to all content when there is no .secondary.nav. this uses padding instead of
+   margin because with the negative margin on .ui.grid we would have to set margin-top: 0,
+   but that does not work universally for all pages */
+.page-content > :first-child:not(.secondary-nav) {
+  padding-top: 14px;
 }
 
 /* overwrite semantic width of containers inside the main page content div (div with class "page-content") */
@@ -1323,7 +1329,6 @@ strong.attention-caution, svg.attention-caution {
 }
 
 overflow-menu {
-  margin-bottom: 15px !important;
   border-bottom: 1px solid var(--color-secondary) !important;
   display: flex;
 }
@@ -1337,6 +1342,10 @@ overflow-menu .overflow-menu-items .item {
   margin-bottom: 0 !important; /* reset fomantic's margin, because the active menu has special bottom border */
 }
 
+overflow-menu .ui.label {
+  margin-left: 7px !important; /* save some space */
+}
+
 .activity-bar-graph {
   background-color: var(--color-primary);
   color: var(--color-primary-contrast);
diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css
index 0fa81b1c2a..e50f1abf42 100644
--- a/web_src/css/dashboard.css
+++ b/web_src/css/dashboard.css
@@ -77,15 +77,14 @@
   margin: 0 1px; /* Accommodate for Semantic's 1px hacks on .attached elements */
 }
 
-.dashboard .dashboard-navbar {
+.dashboard .secondary-nav {
   padding: 1px 12px; /* match .overflow-menu-items in height */
-  background: var(--color-header-wrapper);
 }
 
-.dashboard .dashboard-navbar .org-visibility .label {
+.dashboard .secondary-nav .org-visibility .label {
   margin-left: 5px;
 }
 
-.dashboard .dashboard-navbar .ui.dropdown {
+.dashboard .secondary-nav .ui.dropdown {
   max-width: 100%;
 }
diff --git a/web_src/css/explore.css b/web_src/css/explore.css
index 08858337c0..5cdee823c0 100644
--- a/web_src/css/explore.css
+++ b/web_src/css/explore.css
@@ -1,10 +1,8 @@
-.explore .navbar {
-  margin-bottom: 15px !important;
-  background-color: var(--color-header-wrapper) !important;
+.explore .secondary-nav {
   border-width: 1px !important;
 }
 
-.explore .navbar .svg {
+.explore .secondary-nav .svg {
   width: 16px;
   text-align: center;
   margin-right: 5px;
diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index 88f4c57043..f8553d7cf0 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -136,3 +136,7 @@
   justify-content: center;
   z-index: 1; /* prevent menu button background from overlaying icon */
 }
+
+.secondary-nav {
+  background: var(--color-secondary-nav-bg) !important; /* important because of .ui.secondary.menu */
+}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 4503bd69e3..ca8de42a06 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -4,16 +4,6 @@
   user-select: none;
 }
 
-.repository .navbar {
-  display: flex;
-  justify-content: space-between;
-}
-
-.repository .navbar .ui.label {
-  margin-left: 7px;
-  padding: 3px 5px;
-}
-
 .repository .owner.dropdown {
   min-width: 40% !important;
 }
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index 13fb40e35d..55e704ed10 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -1,4 +1,8 @@
-.header-wrapper .fork-flag {
+.repository .secondary-nav {
+  padding-top: 12px;
+}
+
+.repository .secondary-nav .fork-flag {
   margin-top: 0.5rem;
   font-size: 12px;
 }
@@ -63,8 +67,3 @@
 .repo-buttons .ui.labeled.button.disabled > .button {
   pointer-events: none !important;
 }
-
-.repository .header-wrapper {
-  padding-top: 12px;
-  background-color: var(--color-header-wrapper);
-}
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index 9cf8907e45..c769c51cdc 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -197,7 +197,6 @@
   --color-input-toggle-background: #2e353b;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: #181c20;
   --color-light: #00001728;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
   --color-light-border: #e8e8ff28;
@@ -227,6 +226,7 @@
   --color-nav-bg: #16191c;
   --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
+  --color-secondary-nav-bg: #181c20;
   --color-label-text: var(--color-text);
   --color-label-bg: #73828e4b;
   --color-label-hover-bg: #73828ea0;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 2ac83eefed..2d9ab8e721 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -197,7 +197,6 @@
   --color-input-toggle-background: #d0d7de;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: #f9fafb;
   --color-light: #00001706;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
   --color-light-border: #0000171d;
@@ -227,6 +226,7 @@
   --color-nav-bg: #f6f7fa;
   --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
+  --color-secondary-nav-bg: #f9fafb;
   --color-label-text: var(--color-text);
   --color-label-bg: #949da64b;
   --color-label-hover-bg: #949da6a0;
diff --git a/web_src/css/user.css b/web_src/css/user.css
index 4267ca0b7d..af8a2f5adc 100644
--- a/web_src/css/user.css
+++ b/web_src/css/user.css
@@ -112,10 +112,6 @@
   border: 1px solid var(--color-secondary);
 }
 
-#notification_div {
-  padding-top: 15px;
-}
-
 #notification_table {
   background: var(--color-box-body);
   border: 1px solid var(--color-secondary);

From d0d7b4b6d124dd8fd420a5f3850e37794e09e302 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 13:51:34 +0200
Subject: [PATCH 485/679] Remove jQuery `.attr` from the image diff again
 (#30022)

- Follows https://github.com/go-gitea/gitea/pull/29917

Missed these

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/imagediff.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index 7b77b30ccc..d567632f5f 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -98,8 +98,10 @@ export function initImageDiff() {
         const text = await resp.text();
         const bounds = getDefaultSvgBoundsIfUndefined(text, info.path);
         if (bounds) {
-          info.$images.attr('width', bounds.width);
-          info.$images.attr('height', bounds.height);
+          info.$images.each(function() {
+            this.setAttribute('width', bounds.width);
+            this.setAttribute('height', bounds.height);
+          });
           hideElem(info.$boundsInfo);
         }
       }

From 26dbca741114587f4191050a76ee1a36282a2018 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 14:28:53 +0200
Subject: [PATCH 486/679] Remove jQuery `.attr` from the repository settings
 (#30018)

- Switched from jQuery `.attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested the collaborator access mode change, team search box, and
branch protection form. They all work as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-settings.js | 30 +++++++++++++++-------------
 1 file changed, 16 insertions(+), 14 deletions(-)

diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 58b714fbb7..dc1db8ab29 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -8,22 +8,22 @@ const {appSubUrl, csrfToken} = window.config;
 
 export function initRepoSettingsCollaboration() {
   // Change collaborator access mode
-  $('.page-content.repository .ui.dropdown.access-mode').each((_, e) => {
-    const $dropdown = $(e);
+  $('.page-content.repository .ui.dropdown.access-mode').each((_, el) => {
+    const $dropdown = $(el);
     const $text = $dropdown.find('> .text');
     $dropdown.dropdown({
       async action(_text, value) {
-        const lastValue = $dropdown.attr('data-last-value');
+        const lastValue = el.getAttribute('data-last-value');
         try {
-          $dropdown.attr('data-last-value', value);
+          el.setAttribute('data-last-value', value);
           $dropdown.dropdown('hide');
           const data = new FormData();
-          data.append('uid', $dropdown.attr('data-uid'));
+          data.append('uid', el.getAttribute('data-uid'));
           data.append('mode', value);
-          await POST($dropdown.attr('data-url'), {data});
+          await POST(el.getAttribute('data-url'), {data});
         } catch {
           $text.text('(error)'); // prevent from misleading users when error occurs
-          $dropdown.attr('data-last-value', lastValue);
+          el.setAttribute('data-last-value', lastValue);
         }
       },
       onChange(_value, text, _$choice) {
@@ -32,9 +32,9 @@ export function initRepoSettingsCollaboration() {
       onHide() {
         // set to the really selected value, defer to next tick to make sure `action` has finished its work because the calling order might be onHide -> action
         setTimeout(() => {
-          const $item = $dropdown.dropdown('get item', $dropdown.attr('data-last-value'));
+          const $item = $dropdown.dropdown('get item', el.getAttribute('data-last-value'));
           if ($item) {
-            $dropdown.dropdown('set selected', $dropdown.attr('data-last-value'));
+            $dropdown.dropdown('set selected', el.getAttribute('data-last-value'));
           } else {
             $text.text('(none)'); // prevent from misleading users when the access mode is undefined
           }
@@ -45,11 +45,13 @@ export function initRepoSettingsCollaboration() {
 }
 
 export function initRepoSettingSearchTeamBox() {
-  const $searchTeamBox = $('#search-team-box');
-  $searchTeamBox.search({
+  const searchTeamBox = document.getElementById('search-team-box');
+  if (!searchTeamBox) return;
+
+  $(searchTeamBox).search({
     minCharacters: 2,
     apiSettings: {
-      url: `${appSubUrl}/org/${$searchTeamBox.attr('data-org-name')}/teams/-/search?q={query}`,
+      url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`,
       headers: {'X-Csrf-Token': csrfToken},
       onResponse(response) {
         const items = [];
@@ -77,11 +79,11 @@ export function initRepoSettingGitHook() {
 export function initRepoSettingBranches() {
   if (!$('.repository.settings.branches').length) return;
   $('.toggle-target-enabled').on('change', function () {
-    const $target = $($(this).attr('data-target'));
+    const $target = $(this.getAttribute('data-target'));
     $target.toggleClass('disabled', !this.checked);
   });
   $('.toggle-target-disabled').on('change', function () {
-    const $target = $($(this).attr('data-target'));
+    const $target = $(this.getAttribute('data-target'));
     if (this.checked) $target.addClass('disabled'); // only disable, do not auto enable
   });
   $('#dismiss_stale_approvals').on('change', function () {

From 74c1378dfb5f1831ca2bf1f0b18ab150134f4beb Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 14:37:18 +0200
Subject: [PATCH 487/679] Remove jQuery `.attr` from the diff page (#30021)

- Switched from jQuery `.attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested the review box counter and Previous/Next code review
conversation buttons. They work as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-diff.js | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 20a6577932..262ce2abff 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -13,16 +13,20 @@ import {POST, GET} from '../modules/fetch.js';
 const {pageData, i18n} = window.config;
 
 function initRepoDiffReviewButton() {
-  const $reviewBox = $('#review-box');
-  const $counter = $reviewBox.find('.review-comments-counter');
+  const reviewBox = document.getElementById('review-box');
+  if (!reviewBox) return;
+
+  const $reviewBox = $(reviewBox);
+  const counter = reviewBox.querySelector('.review-comments-counter');
+  if (!counter) return;
 
   $(document).on('click', 'button[name="pending_review"]', (e) => {
     const $form = $(e.target).closest('form');
     // Watch for the form's submit event.
     $form.on('submit', () => {
-      const num = parseInt($counter.attr('data-pending-comment-number')) + 1 || 1;
-      $counter.attr('data-pending-comment-number', num);
-      $counter.text(num);
+      const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
+      counter.setAttribute('data-pending-comment-number', num);
+      counter.textContent = num;
       // Force the browser to reflow the DOM. This is to ensure that the browser replay the animation
       $reviewBox.removeClass('pulse');
       $reviewBox.width();
@@ -65,7 +69,7 @@ function initRepoDiffConversationForm() {
         formData.append(submitter.name, submitter.value);
       }
 
-      const response = await POST($form.attr('action'), {data: formData});
+      const response = await POST(e.target.getAttribute('action'), {data: formData});
       const $newConversationHolder = $(await response.text());
       const {path, side, idx} = $newConversationHolder.data();
 
@@ -118,7 +122,7 @@ export function initRepoDiffConversationNav() {
     const index = $conversations.index($conversation);
     const previousIndex = index > 0 ? index - 1 : $conversations.length - 1;
     const $previousConversation = $conversations.eq(previousIndex);
-    const anchor = $previousConversation.find('.comment').first().attr('id');
+    const anchor = $previousConversation.find('.comment').first()[0].getAttribute('id');
     window.location.href = `#${anchor}`;
   });
   $(document).on('click', '.next-conversation', (e) => {
@@ -127,7 +131,7 @@ export function initRepoDiffConversationNav() {
     const index = $conversations.index($conversation);
     const nextIndex = index < $conversations.length - 1 ? index + 1 : 0;
     const $nextConversation = $conversations.eq(nextIndex);
-    const anchor = $nextConversation.find('.comment').first().attr('id');
+    const anchor = $nextConversation.find('.comment').first()[0].getAttribute('id');
     window.location.href = `#${anchor}`;
   });
 }
@@ -173,8 +177,7 @@ function initRepoDiffShowMore() {
   $(document).on('click', 'a#diff-show-more-files', (e) => {
     e.preventDefault();
 
-    const $target = $(e.target);
-    const linkLoadMore = $target.attr('data-href');
+    const linkLoadMore = e.target.getAttribute('data-href');
     loadMoreFiles(linkLoadMore);
   });
 

From 1cdc6c3a4ea28396788b2697f9cf257df161ff9a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 23 Mar 2024 21:07:47 +0800
Subject: [PATCH 488/679] Escape paths for find file correctly (#30026)

Fix #30020
---
 routers/web/repo/find.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go
index 07b3722798..9da4237c1e 100644
--- a/routers/web/repo/find.go
+++ b/routers/web/repo/find.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -17,7 +18,7 @@ const (
 // FindFiles render the page to find repository files
 func FindFiles(ctx *context.Context) {
 	path := ctx.Params("*")
-	ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + path
-	ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + path
+	ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path)
+	ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path)
 	ctx.HTML(http.StatusOK, tplFindFiles)
 }

From b9c57fb78e8e0d80d786d8e1da433b6c7ebf2f1c Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Sat, 23 Mar 2024 16:45:13 +0100
Subject: [PATCH 489/679] Determine fuzziness of bleve indexer by keyword
 length (#29706)

also bleve did match on fuzzy search and the other way around. this also fix that bug.
---
 modules/indexer/code/bleve/bleve.go     | 15 +++++++--------
 modules/indexer/internal/bleve/query.go | 10 ++--------
 modules/indexer/issues/bleve/bleve.go   | 25 +++++++++++++------------
 tests/integration/repo_search_test.go   | 16 +++++++---------
 4 files changed, 29 insertions(+), 37 deletions(-)

diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go
index d7f735e957..c607d780ef 100644
--- a/modules/indexer/code/bleve/bleve.go
+++ b/modules/indexer/code/bleve/bleve.go
@@ -39,6 +39,8 @@ import (
 const (
 	unicodeNormalizeName = "unicodeNormalize"
 	maxBatchSize         = 16
+	// fuzzyDenominator determines the levenshtein distance per each character of a keyword
+	fuzzyDenominator = 4
 )
 
 func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
@@ -239,15 +241,12 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
 		keywordQuery query.Query
 	)
 
+	phraseQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
+	phraseQuery.FieldVal = "Content"
+	phraseQuery.Analyzer = repoIndexerAnalyzer
+	keywordQuery = phraseQuery
 	if opts.IsKeywordFuzzy {
-		phraseQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
-		phraseQuery.FieldVal = "Content"
-		phraseQuery.Analyzer = repoIndexerAnalyzer
-		keywordQuery = phraseQuery
-	} else {
-		prefixQuery := bleve.NewPrefixQuery(opts.Keyword)
-		prefixQuery.FieldVal = "Content"
-		keywordQuery = prefixQuery
+		phraseQuery.Fuzziness = len(opts.Keyword) / fuzzyDenominator
 	}
 
 	if len(opts.RepoIDs) > 0 {
diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go
index b96875343e..21422b281c 100644
--- a/modules/indexer/internal/bleve/query.go
+++ b/modules/indexer/internal/bleve/query.go
@@ -20,17 +20,11 @@ func NumericEqualityQuery(value int64, field string) *query.NumericRangeQuery {
 }
 
 // MatchPhraseQuery generates a match phrase query for the given phrase, field and analyzer
-func MatchPhraseQuery(matchPhrase, field, analyzer string) *query.MatchPhraseQuery {
+func MatchPhraseQuery(matchPhrase, field, analyzer string, fuzziness int) *query.MatchPhraseQuery {
 	q := bleve.NewMatchPhraseQuery(matchPhrase)
 	q.FieldVal = field
 	q.Analyzer = analyzer
-	return q
-}
-
-// PrefixQuery generates a match prefix query for the given prefix and field
-func PrefixQuery(matchPrefix, field string) *query.PrefixQuery {
-	q := bleve.NewPrefixQuery(matchPrefix)
-	q.FieldVal = field
+	q.Fuzziness = fuzziness
 	return q
 }
 
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index 927ad58cd4..1f54be721b 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -35,7 +35,11 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
 	})
 }
 
-const maxBatchSize = 16
+const (
+	maxBatchSize = 16
+	// fuzzyDenominator determines the levenshtein distance per each character of a keyword
+	fuzzyDenominator = 4
+)
 
 // IndexerData an update to the issue indexer
 type IndexerData internal.IndexerData
@@ -156,19 +160,16 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	var queries []query.Query
 
 	if options.Keyword != "" {
+		fuzziness := 0
 		if options.IsFuzzyKeyword {
-			queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
-				inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer),
-				inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer),
-				inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer),
-			}...))
-		} else {
-			queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
-				inner_bleve.PrefixQuery(options.Keyword, "title"),
-				inner_bleve.PrefixQuery(options.Keyword, "content"),
-				inner_bleve.PrefixQuery(options.Keyword, "comments"),
-			}...))
+			fuzziness = len(options.Keyword) / fuzzyDenominator
 		}
+
+		queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
+			inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer, fuzziness),
+			inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer, fuzziness),
+			inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer, fuzziness),
+		}...))
 	}
 
 	if len(options.RepoIDs) > 0 || options.AllPublic {
diff --git a/tests/integration/repo_search_test.go b/tests/integration/repo_search_test.go
index cf199e98c2..56cc45d901 100644
--- a/tests/integration/repo_search_test.go
+++ b/tests/integration/repo_search_test.go
@@ -32,7 +32,7 @@ func TestSearchRepo(t *testing.T) {
 	repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
 	assert.NoError(t, err)
 
-	executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
+	code_indexer.UpdateRepoIndexer(repo)
 
 	testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"})
 
@@ -42,12 +42,14 @@ func TestSearchRepo(t *testing.T) {
 	repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob")
 	assert.NoError(t, err)
 
-	executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
+	code_indexer.UpdateRepoIndexer(repo)
 
 	testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"})
-	testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt"})
-	testSearch(t, "/user2/glob/search?q=file4&page=1", []string{})
-	testSearch(t, "/user2/glob/search?q=file5&page=1", []string{})
+	testSearch(t, "/user2/glob/search?q=loren&page=1&t=match", []string{"a.txt"})
+	testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt", "a.txt"})
+	testSearch(t, "/user2/glob/search?q=file3&page=1&t=match", []string{"x/b.txt", "a.txt"})
+	testSearch(t, "/user2/glob/search?q=file4&page=1&t=match", []string{"x/b.txt", "a.txt"})
+	testSearch(t, "/user2/glob/search?q=file5&page=1&t=match", []string{"x/b.txt", "a.txt"})
 }
 
 func testSearch(t *testing.T, url string, expected []string) {
@@ -57,7 +59,3 @@ func testSearch(t *testing.T, url string, expected []string) {
 	filenames := resultFilenames(t, NewHTMLParser(t, resp.Body))
 	assert.EqualValues(t, expected, filenames)
 }
-
-func executeIndexer(t *testing.T, repo *repo_model.Repository, op func(*repo_model.Repository)) {
-	op(repo)
-}

From d9e33959b38d1463f69f6f8807bc50095cf6dbdb Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 20:18:45 +0200
Subject: [PATCH 490/679] Remove jQuery from the issue "go to" button (#30028)

- Switched to plain JavaScript
- Tested the "go to" button functionality and it works as before

# Demo using JavaScript without jQuery

![demo](https://github.com/go-gitea/gitea/assets/20454870/76add18f-3294-4117-98b7-a97f576370e2)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/common-issue-list.js | 31 +++++++++++-------------
 1 file changed, 14 insertions(+), 17 deletions(-)

diff --git a/web_src/js/features/common-issue-list.js b/web_src/js/features/common-issue-list.js
index 8182f99f29..0c0f6c563d 100644
--- a/web_src/js/features/common-issue-list.js
+++ b/web_src/js/features/common-issue-list.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
 import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.js';
 import {GET} from '../modules/fetch.js';
 
@@ -30,42 +29,40 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) {
 }
 
 export function initCommonIssueListQuickGoto() {
-  const $goto = $('#issue-list-quick-goto');
-  if (!$goto.length) return;
+  const goto = document.getElementById('issue-list-quick-goto');
+  if (!goto) return;
 
-  const $form = $goto.closest('form');
-  const $input = $form.find('input[name=q]');
-  const repoLink = $goto.attr('data-repo-link');
+  const form = goto.closest('form');
+  const input = form.querySelector('input[name=q]');
+  const repoLink = goto.getAttribute('data-repo-link');
 
-  $form.on('submit', (e) => {
+  form.addEventListener('submit', (e) => {
     // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
-    let doQuickGoto = !isElemHidden($goto);
+    let doQuickGoto = !isElemHidden(goto);
     const submitter = submitEventSubmitter(e);
-    if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false;
+    if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false;
     if (!doQuickGoto) return;
 
     // if there is a goto button, use its link
     e.preventDefault();
-    window.location.href = $goto.attr('data-issue-goto-link');
+    window.location.href = goto.getAttribute('data-issue-goto-link');
   });
 
   const onInput = async () => {
-    const searchText = $input.val();
-
+    const searchText = input.value;
     // try to check whether the parsed goto link is valid
     let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
     if (targetUrl) {
       const res = await GET(`${targetUrl}/info`);
       if (res.status !== 200) targetUrl = '';
     }
-
     // if the input value has changed, then ignore the result
-    if ($input.val() !== searchText) return;
+    if (input.value !== searchText) return;
 
-    toggleElem($goto, Boolean(targetUrl));
-    $goto.attr('data-issue-goto-link', targetUrl);
+    toggleElem(goto, Boolean(targetUrl));
+    goto.setAttribute('data-issue-goto-link', targetUrl);
   };
 
-  $input.on('input', onInputDebounce(onInput));
+  input.addEventListener('input', onInputDebounce(onInput));
   onInput();
 }

From fabe01478ab449cc4870b7e2a9a1db3911bb14bd Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 23 Mar 2024 19:45:11 +0100
Subject: [PATCH 491/679] Migrate font-weight helpers to tailwind (#30027)

Commands ran:

```sh
perl -p -i -e 's#gt-font-light#tw-font-light#g' web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-font-normal#tw-font-normal#g' web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-font-medium#tw-font-medium#g' web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-font-semibold#tw-font-semibold#g' web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-font-bold#tw-font-bold#g' web_src/js/**/* templates/**/*
```
---
 tailwind.config.js                              | 7 +++++++
 templates/repo/diff/box.tmpl                    | 2 +-
 templates/repo/diff/comments.tmpl               | 2 +-
 templates/repo/header.tmpl                      | 2 +-
 templates/repo/issue/view_content.tmpl          | 2 +-
 templates/repo/issue/view_content/comments.tmpl | 4 ++--
 templates/repo/pulls/tab_menu.tmpl              | 2 +-
 templates/repo/sub_menu.tmpl                    | 2 +-
 templates/shared/user/authorlink.tmpl           | 2 +-
 templates/user/settings/account.tmpl            | 2 +-
 web_src/css/helpers.css                         | 6 ------
 web_src/js/components/DiffFileList.vue          | 2 +-
 12 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/tailwind.config.js b/tailwind.config.js
index e2e8f23656..01fc9ee24c 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -66,5 +66,12 @@ export default {
       '3xl': '24px',
       'full': 'var(--border-radius-circle)', // 50%
     },
+    fontWeight: {
+      light: 'var(--font-weight-light)',
+      normal: 'var(--font-weight-normal)',
+      medium: 'var(--font-weight-medium)',
+      semibold: 'var(--font-weight-semibold)',
+      bold: 'var(--font-weight-bold)',
+    },
   },
 };
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 42eaf9d30e..4ea1d2c621 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -119,7 +119,7 @@
 										{{svg "octicon-chevron-down" 18}}
 									{{end}}
 								</button>
-								<div class="gt-font-semibold tw-flex tw-items-center gt-mono">
+								<div class="tw-font-semibold tw-flex tw-items-center gt-mono">
 									{{if $file.IsBin}}
 										<span class="gt-ml-1 gt-mr-3">
 											{{ctx.Locale.Tr "repo.diff.bin"}}
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 4b626ba2b0..070fe92317 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -11,7 +11,7 @@
 		<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between">
 			<div class="comment-header-left tw-flex tw-items-center">
 				{{if .OriginalAuthor}}
-					<span class="text black gt-font-semibold gt-mr-2">
+					<span class="text black tw-font-semibold gt-mr-2">
 						{{svg (MigrationIcon $.root.Repository.GetOriginalURLHostname)}}
 						{{.OriginalAuthor}}
 					</span>
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 187a56bdcc..54f72943e4 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -8,7 +8,7 @@
 				</div>
 				<div class="flex-item-main">
 					<div class="flex-item-title gt-font-18">
-						<a class="muted gt-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a>
+						<a class="muted tw-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a>
 					</div>
 				</div>
 				<div class="flex-item-trailing">
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 7966fc7e1c..759de75662 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -23,7 +23,7 @@
 					<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3">
 						<div class="comment-header-left tw-flex tw-items-center">
 							{{if .Issue.OriginalAuthor}}
-								<span class="text black gt-font-semibold">
+								<span class="text black tw-font-semibold">
 									{{svg (MigrationIcon .Repository.GetOriginalURLHostname)}}
 									{{.Issue.OriginalAuthor}}
 								</span>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index c0c4df925b..b137dd0a9c 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -28,7 +28,7 @@
 					<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3">
 						<div class="comment-header-left tw-flex tw-items-center">
 							{{if .OriginalAuthor}}
-								<span class="text black gt-font-semibold gt-mr-2">
+								<span class="text black tw-font-semibold gt-mr-2">
 									{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 									{{.OriginalAuthor}}
 								</span>
@@ -411,7 +411,7 @@
 								{{end}}
 								<span class="text grey muted-links">
 									{{if .OriginalAuthor}}
-										<span class="text black gt-font-semibold">
+										<span class="text black tw-font-semibold">
 											{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 											{{.OriginalAuthor}}
 										</span>
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index 2a653c7c69..7f4c460b8e 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -15,7 +15,7 @@
 			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
-		<span class="item tw-ml-auto gt-pr-0 gt-font-bold tw-flex tw-items-center gt-gap-3">
+		<span class="item tw-ml-auto gt-pr-0 tw-font-bold tw-flex tw-items-center gt-gap-3">
 			<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
 			<span class="diff-stats-bar">
 				<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index 8edb0c1516..654a65fa5c 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -25,7 +25,7 @@
 		{{range .LanguageStats}}
 		<div class="item">
 			<i class="color-icon" style="background-color: {{.Color}}"></i>
-			<span class="gt-font-semibold">
+			<span class="tw-font-semibold">
 				{{if eq .Language "other"}}
 					{{ctx.Locale.Tr "repo.language_other"}}
 				{{else}}
diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl
index 64ccc62cd0..4d8ad736be 100644
--- a/templates/shared/user/authorlink.tmpl
+++ b/templates/shared/user/authorlink.tmpl
@@ -1 +1 @@
-<a class="author text black gt-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label gt-p-2">bot</span>{{end}}
+<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label gt-p-2">bot</span>{{end}}
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index efc09156e6..fb46dfbd2d 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -136,7 +136,7 @@
 			<div class="ui red message">
 				<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "settings.delete_prompt"}}</p>
 				{{if .UserDeleteWithComments}}
-				<p class="text left gt-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime}}</p>
+				<p class="text left tw-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime}}</p>
 				{{end}}
 			</div>
 			<form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post">
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index c7097e631b..dd32c3fb31 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -36,12 +36,6 @@ Gitea's private styles use `g-` prefix.
   text-overflow: ellipsis;
 }
 
-.gt-font-light { font-weight: var(--font-weight-light) !important; }
-.gt-font-normal { font-weight: var(--font-weight-normal) !important; }
-.gt-font-medium { font-weight: var(--font-weight-medium) !important; }
-.gt-font-semibold { font-weight: var(--font-weight-semibold) !important; }
-.gt-font-bold { font-weight: var(--font-weight-bold) !important; }
-
 .interact-fg { color: inherit !important; }
 .interact-fg:hover { color: var(--color-primary) !important; }
 .interact-fg:active { color: var(--color-primary-active) !important; }
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 3a0e287808..66fe49c50b 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -38,7 +38,7 @@ export default {
 <template>
   <ol class="diff-stats gt-m-0" ref="root" v-if="store.fileListIsVisible">
     <li v-for="file in store.files" :key="file.NameHash">
-      <div class="gt-font-semibold tw-flex tw-items-center pull-right">
+      <div class="tw-font-semibold tw-flex tw-items-center pull-right">
         <span v-if="file.IsBin" class="gt-ml-1 gt-mr-3">{{ store.binaryFileMessage }}</span>
         {{ file.IsBin ? '' : file.Addition + file.Deletion }}
         <span v-if="!file.IsBin" class="diff-stats-bar gt-mx-3" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">

From 0bef9a2775af0e27a0754207fc87537b96c2792e Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 23 Mar 2024 21:11:15 +0100
Subject: [PATCH 492/679] Replace all simple inline styles with tailwind
 (#30032)

These should be all simple inline styles that were left in the
templates.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 templates/admin/notice.tmpl                                   | 2 +-
 templates/devtest/flex-list.tmpl                              | 2 +-
 templates/devtest/tmplerr.tmpl                                | 4 ++--
 templates/repo/diff/box.tmpl                                  | 2 +-
 templates/repo/issue/view_content/reference_issue_dialog.tmpl | 2 +-
 templates/repo/settings/deploy_keys.tmpl                      | 2 +-
 templates/repo/settings/options.tmpl                          | 4 ++--
 templates/user/auth/webauthn.tmpl                             | 2 +-
 web_src/css/review.css                                        | 4 ++++
 9 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index c788ebc602..5ea003e5ec 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -20,7 +20,7 @@
 						<td><div class="ui checkbox tw-flex" data-id="{{.ID}}"><input type="checkbox"></div></td>
 						<td>{{.ID}}</td>
 						<td>{{ctx.Locale.Tr .TrStr}}</td>
-						<td class="view-detail auto-ellipsis" style="width: 80%;"><span class="notice-description">{{.Description}}</span></td>
+						<td class="view-detail auto-ellipsis tw-w-4/5"><span class="notice-description">{{.Description}}</span></td>
 						<td nowrap>{{DateTime "short" .CreatedUnix}}</td>
 						<td class="view-detail"><a href="#">{{svg "octicon-note" 16}}</a></td>
 					</tr>
diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl
index 0c7b27cd84..fdb9eb6b39 100644
--- a/templates/devtest/flex-list.tmpl
+++ b/templates/devtest/flex-list.tmpl
@@ -73,7 +73,7 @@
 						</div>
 						<div class="flex-item-trailing">
 							<a class="muted" href="{{$.Link}}">
-								<span class="flex-text-inline"><i class="color-icon gt-mr-3" style="background-color: aqua"></i>Go</span>
+								<span class="flex-text-inline"><i class="color-icon gt-mr-3 tw-bg-blue"></i>Go</span>
 							</a>
 							<a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-star" 16}}45000</a>
 							<a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-git-branch" 16}}1234</a>
diff --git a/templates/devtest/tmplerr.tmpl b/templates/devtest/tmplerr.tmpl
index 09cf05fc1f..dd938c895e 100644
--- a/templates/devtest/tmplerr.tmpl
+++ b/templates/devtest/tmplerr.tmpl
@@ -1,10 +1,10 @@
 {{template "base/head" .}}
 <div class="page-content devtest">
 	<div class="tw-flex">
-		<div style="width: 80%; ">
+		<div class="tw-w-4/5">
 			hello hello hello hello hello hello hello hello hello hello
 		</div>
-		<div style="width: 20%;">
+		<div class="tw-w-1/5">
 			{{template "devtest/tmplerr-sub" .}}
 		</div>
 	</div>
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 4ea1d2c621..0761e3f7bd 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -178,7 +178,7 @@
 						<div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}>
 							<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} gt-hidden{{end}}">
 								{{if or $file.IsIncomplete $file.IsBin}}
-									<div class="diff-file-body binary" style="padding: 5px 10px;">
+									<div class="diff-file-body binary">
 										{{if $file.IsIncomplete}}
 											{{if $file.IsIncompleteLineTooLong}}
 												{{ctx.Locale.Tr "repo.diff.file_suppressed_line_too_long"}}
diff --git a/templates/repo/issue/view_content/reference_issue_dialog.tmpl b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
index b771e08909..5f338f6768 100644
--- a/templates/repo/issue/view_content/reference_issue_dialog.tmpl
+++ b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
@@ -2,7 +2,7 @@
 	<div class="header">
 		{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}
 	</div>
-	<div class="content" style="text-align:left">
+	<div class="content tw-text-left">
 		<form class="ui form form-fetch-action" action="{{printf "%s/issues/new" .Repository.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="ui segment content">
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index a79a196825..f66b94c332 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -31,7 +31,7 @@
 							<label for="is_writable">
 								{{ctx.Locale.Tr "repo.settings.is_writable"}}
 							</label>
-							<small style="padding-left: 26px;">{{ctx.Locale.Tr "repo.settings.is_writable_info"}}</small>
+							<small class="tw-pl-[26px]">{{ctx.Locale.Tr "repo.settings.is_writable_info"}}</small>
 						</div>
 					</div>
 					<button class="ui primary button">
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 6fa7184265..1e32853d81 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -110,7 +110,7 @@
 					<table class="ui table">
 						<thead>
 							<tr>
-								<th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th>
+								<th class="tw-w-2/5">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}</th>
 								<th></th>
@@ -207,7 +207,7 @@
 					<table class="ui table">
 						<thead>
 							<tr>
-								<th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}</th>
+								<th class="tw-w-2/5">{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}</th>
 								<th></th>
diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl
index 7c543cb16d..867248718d 100644
--- a/templates/user/auth/webauthn.tmpl
+++ b/templates/user/auth/webauthn.tmpl
@@ -11,7 +11,7 @@
 				<p>{{ctx.Locale.Tr "webauthn_sign_in"}}</p>
 			</div>
 			<div class="ui attached segment tw-flex tw-items-center tw-justify-center gt-gap-2 gt-py-3">
-				<div class="is-loading" style="width: 40px; height: 40px"></div>
+				<div class="is-loading tw-w-[40px] tw-h-[40px]"></div>
 				{{ctx.Locale.Tr "webauthn_press_button"}}
 			</div>
 			{{if .HasTwoFactor}}
diff --git a/web_src/css/review.css b/web_src/css/review.css
index 5336775547..cf3a4d48f7 100644
--- a/web_src/css/review.css
+++ b/web_src/css/review.css
@@ -194,6 +194,10 @@
   margin: 0 0 0 3em;
 }
 
+.diff-file-body.binary {
+  padding: 5px 10px;
+}
+
 .file-comment {
   color: var(--color-text);
 }

From 75e2e5c736687ae1897cf760a432b572feed56f5 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 23 Mar 2024 22:22:15 +0100
Subject: [PATCH 493/679] Migrate font-size helpers to tailwind (#30029)

Migrate `gt-font-*` to `tw-text-*` All tailwind-original class names are
also available and render like they would with 16px root font size.

We currently have root font size at 14px, but I would like to eventually
migrate us to 16px so that the tailwind docs apply to us unchangend and
because 16px is the recommended root font size for web pages in general.
Also the number 16 is much better dividable than 14 so will result in
more integers.
---
 tailwind.config.js                             | 18 ++++++++++++++++++
 templates/org/header.tmpl                      |  2 +-
 templates/repo/header.tmpl                     |  2 +-
 templates/repo/home.tmpl                       |  4 ++--
 templates/user/dashboard/feeds.tmpl            |  2 +-
 .../user/notification/notification_div.tmpl    |  4 ++--
 web_src/css/helpers.css                        |  8 --------
 web_src/js/components/RepoContributors.vue     |  2 +-
 8 files changed, 26 insertions(+), 16 deletions(-)

diff --git a/tailwind.config.js b/tailwind.config.js
index 01fc9ee24c..0754ab3631 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -73,5 +73,23 @@ export default {
       semibold: 'var(--font-weight-semibold)',
       bold: 'var(--font-weight-bold)',
     },
+    fontSize: { // not using `rem` units because our root is currently 14px
+      'xs': '12px',
+      'sm': '14px',
+      'base': '16px',
+      'lg': '18px',
+      'xl': '20px',
+      '2xl': '24px',
+      '3xl': '30px',
+      '4xl': '36px',
+      '5xl': '48px',
+      '6xl': '60px',
+      '7xl': '72px',
+      '8xl': '96px',
+      '9xl': '128px',
+      ...Object.fromEntries(Array.from({length: 100}, (_, i) => {
+        return [`${i}`, `${i === 0 ? '0' : `${i}px`}`];
+      })),
+    },
   },
 };
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 204ba7e3c1..81373aa75c 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -7,7 +7,7 @@
 				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
 				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
 			</span>
-			<span class="tw-flex tw-items-center gt-gap-2 tw-ml-auto gt-font-16 tw-whitespace-nowrap">
+			<span class="tw-flex tw-items-center gt-gap-2 tw-ml-auto tw-text-16 tw-whitespace-nowrap">
 				{{if .EnableFeed}}
 					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
 						{{svg "octicon-rss" 24}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 54f72943e4..002d06c23a 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -7,7 +7,7 @@
 					{{template "repo/icon" .}}
 				</div>
 				<div class="flex-item-main">
-					<div class="flex-item-title gt-font-18">
+					<div class="flex-item-title tw-text-18">
 						<a class="muted tw-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a>
 					</div>
 				</div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 5a9e02ca60..40732db94a 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -6,7 +6,7 @@
 		{{template "repo/code/recently_pushed_new_branches" .}}
 		{{if and (not .HideRepoInfo) (not .IsBlame)}}
 		<div class="ui repo-description gt-word-break">
-			<div id="repo-desc" class="gt-font-16">
+			<div id="repo-desc" class="tw-text-16">
 				{{$description := .Repository.DescriptionHTML $.Context}}
 				{{if $description}}<span class="description">{{$description | RenderCodeBlock}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{ctx.Locale.Tr "repo.no_desc"}}</span>{{end}}
 				<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
@@ -29,7 +29,7 @@
 		</div>
 		<div class="tw-flex tw-items-center tw-flex-wrap gt-gap-2" id="repo-topics">
 			{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
-			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg gt-font-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
+			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
 		</div>
 		{{end}}
 		{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index 382b0d4542..c58d7e22d7 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -107,7 +107,7 @@
 					<a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | RenderEmoji $.Context | RenderCodeBlock}}</a>
 					{{$comment := index .GetIssueInfos 1}}
 					{{if $comment}}
-						<div class="markup gt-font-14">{{RenderMarkdownToHtml ctx $comment}}</div>
+						<div class="markup tw-text-14">{{RenderMarkdownToHtml ctx $comment}}</div>
 					{{end}}
 				{{else if .GetOpType.InActions "merge_pull_request"}}
 					<div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div>
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index 371da129ce..da5a920fd1 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -44,13 +44,13 @@
 								{{end}}
 							</div>
 							<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}">
-								<div class="notifications-top-row gt-font-13">
+								<div class="notifications-top-row tw-text-13">
 									{{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}}
 									{{if eq .Status 3}}
 										{{svg "octicon-pin" 13 "text blue gt-mt-1 gt-ml-2"}}
 									{{end}}
 								</div>
-								<div class="notifications-bottom-row gt-font-16 gt-py-1">
+								<div class="notifications-bottom-row tw-text-16 gt-py-1">
 									<span class="issue-title">
 										{{if .Issue}}
 											{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index dd32c3fb31..2d6bd576ae 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -163,14 +163,6 @@ Gitea's private styles use `g-` prefix.
 .gt-gap-y-4 { row-gap: 1rem !important; }
 .gt-gap-y-5 { row-gap: 2rem !important; }
 
-.gt-font-12 { font-size: 12px !important }
-.gt-font-13 { font-size: 13px !important }
-.gt-font-14 { font-size: 14px !important }
-.gt-font-15 { font-size: 15px !important }
-.gt-font-16 { font-size: 16px !important }
-.gt-font-17 { font-size: 17px !important }
-.gt-font-18 { font-size: 18px !important }
-
 /*
 gt-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element.
 do not use:
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index adaf7f28f1..6de68a4aec 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -384,7 +384,7 @@ export default {
             <h4 v-else class="contributor-name">
               {{ contributor.name }}
             </h4>
-            <p class="gt-font-12 tw-flex gt-gap-2">
+            <p class="tw-text-12 tw-flex gt-gap-2">
               <strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong>
               <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
               <strong v-if="contributor.total_deletions" class="text red">

From 900dd79d8adaf2569df0f1346b6e6e91ed4b5ad3 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 23 Mar 2024 23:31:19 +0200
Subject: [PATCH 494/679] Remove jQuery `.attr` from the common global
 functions (#30023)

- Switched from jQuery `.attr` to plain javascript `getAttribute`
- Tested the show/hide modal buttons, they work as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/common-global.js | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 2469361c6e..e27935a86e 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -301,8 +301,8 @@ export function initGlobalLinkActions() {
     const $this = $(this);
     const dataArray = $this.data();
     let filter = '';
-    if ($this.attr('data-modal-id')) {
-      filter += `#${$this.attr('data-modal-id')}`;
+    if (this.getAttribute('data-modal-id')) {
+      filter += `#${this.getAttribute('data-modal-id')}`;
     }
 
     const $dialog = $(`.delete.modal${filter}`);
@@ -352,8 +352,7 @@ function initGlobalShowModal() {
   // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
   $('.show-modal').on('click', function (e) {
     e.preventDefault();
-    const $el = $(this);
-    const modalSelector = $el.attr('data-modal');
+    const modalSelector = this.getAttribute('data-modal');
     const $modal = $(modalSelector);
     if (!$modal.length) {
       throw new Error('no modal for this action');
@@ -406,7 +405,7 @@ export function initGlobalButtons() {
     // a '.show-panel' element can show a panel, by `data-panel="selector"`
     // if it has "toggle" class, it toggles the panel
     e.preventDefault();
-    const sel = $(this).attr('data-panel');
+    const sel = this.getAttribute('data-panel');
     if (this.classList.contains('toggle')) {
       toggleElem(sel);
     } else {
@@ -417,12 +416,12 @@ export function initGlobalButtons() {
   $('.hide-panel').on('click', function (e) {
     // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
     e.preventDefault();
-    let sel = $(this).attr('data-panel');
+    let sel = this.getAttribute('data-panel');
     if (sel) {
       hideElem($(sel));
       return;
     }
-    sel = $(this).attr('data-panel-closest');
+    sel = this.getAttribute('data-panel-closest');
     if (sel) {
       hideElem($(this).closest(sel));
       return;

From e3e08dcc5184cdbdac5023fabaafba123a995c3e Mon Sep 17 00:00:00 2001
From: DrMaxNix <mail@drmaxnix.de>
Date: Sat, 23 Mar 2024 22:59:58 +0100
Subject: [PATCH 495/679] Respect DEFAULT_ORG_MEMBER_VISIBLE setting when
 adding creator to org (#30013)

This PR adds `setting.Service.DefaultOrgMemberVisible` value to dataset
of user when the initial org creator is being added to the created org.

Fixes #30012.
---
 models/organization/org.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/models/organization/org.go b/models/organization/org.go
index a3082e9ac7..ba0fd756e3 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -319,8 +319,9 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
 
 	// Add initial creator to organization and owner team.
 	if err = db.Insert(ctx, &OrgUser{
-		UID:   owner.ID,
-		OrgID: org.ID,
+		UID:      owner.ID,
+		OrgID:    org.ID,
+		IsPublic: setting.Service.DefaultOrgMemberVisible,
 	}); err != nil {
 		return fmt.Errorf("insert org-user relation: %w", err)
 	}

From e4a481e0ca7e952efdf7a96ccd33f80f527d8c86 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 03:01:57 +0100
Subject: [PATCH 496/679] Remove remaining jQuery .css code (#30015)

The linter missed these because they were set on a object. Tested and I
also renamed those properties to add `$` indicating a jQuery selection.

---------

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/imagediff.js | 89 ++++++++++++++++----------------
 1 file changed, 45 insertions(+), 44 deletions(-)

diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index d567632f5f..53bf2109ba 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -54,8 +54,8 @@ export function initImageDiff() {
     };
 
     return {
-      image1: $(image1),
-      image2: $(image2),
+      $image1: $(image1),
+      $image2: $(image2),
       size1,
       size2,
       max,
@@ -124,18 +124,18 @@ export function initImageDiff() {
         factor = (diffContainerWidth - 24) / 2 / sizes.max.width;
       }
 
-      const widthChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalWidth !== sizes.image2[0].naturalWidth;
-      const heightChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalHeight !== sizes.image2[0].naturalHeight;
-      if (sizes.image1.length !== 0) {
-        $container.find('.bounds-info-after .bounds-info-width').text(`${sizes.image1[0].naturalWidth}px`).addClass(widthChanged ? 'green' : '');
-        $container.find('.bounds-info-after .bounds-info-height').text(`${sizes.image1[0].naturalHeight}px`).addClass(heightChanged ? 'green' : '');
+      const widthChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalWidth !== sizes.$image2[0].naturalWidth;
+      const heightChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalHeight !== sizes.$image2[0].naturalHeight;
+      if (sizes.$image1.length !== 0) {
+        $container.find('.bounds-info-after .bounds-info-width').text(`${sizes.$image1[0].naturalWidth}px`).addClass(widthChanged ? 'green' : '');
+        $container.find('.bounds-info-after .bounds-info-height').text(`${sizes.$image1[0].naturalHeight}px`).addClass(heightChanged ? 'green' : '');
       }
-      if (sizes.image2.length !== 0) {
-        $container.find('.bounds-info-before .bounds-info-width').text(`${sizes.image2[0].naturalWidth}px`).addClass(widthChanged ? 'red' : '');
-        $container.find('.bounds-info-before .bounds-info-height').text(`${sizes.image2[0].naturalHeight}px`).addClass(heightChanged ? 'red' : '');
+      if (sizes.$image2.length !== 0) {
+        $container.find('.bounds-info-before .bounds-info-width').text(`${sizes.$image2[0].naturalWidth}px`).addClass(widthChanged ? 'red' : '');
+        $container.find('.bounds-info-before .bounds-info-height').text(`${sizes.$image2[0].naturalHeight}px`).addClass(heightChanged ? 'red' : '');
       }
 
-      const image1 = sizes.image1[0];
+      const image1 = sizes.$image1[0];
       if (image1) {
         const container = image1.parentNode;
         image1.style.width = `${sizes.size1.width * factor}px`;
@@ -145,7 +145,7 @@ export function initImageDiff() {
         container.style.height = `${sizes.size1.height * factor + 2}px`;
       }
 
-      const image2 = sizes.image2[0];
+      const image2 = sizes.$image2[0];
       if (image2) {
         const container = image2.parentNode;
         image2.style.width = `${sizes.size2.width * factor}px`;
@@ -162,7 +162,7 @@ export function initImageDiff() {
         factor = (diffContainerWidth - 12) / sizes.max.width;
       }
 
-      const image1 = sizes.image1[0];
+      const image1 = sizes.$image1[0];
       if (image1) {
         const container = image1.parentNode;
         const swipeFrame = container.parentNode;
@@ -175,7 +175,7 @@ export function initImageDiff() {
         swipeFrame.style.width = `${sizes.max.width * factor + 2}px`;
       }
 
-      const image2 = sizes.image2[0];
+      const image2 = sizes.$image2[0];
       if (image2) {
         const container = image2.parentNode;
         const swipeFrame = container.parentNode;
@@ -222,38 +222,39 @@ export function initImageDiff() {
         factor = (diffContainerWidth - 12) / sizes.max.width;
       }
 
-      sizes.image1.css({
-        width: sizes.size1.width * factor,
-        height: sizes.size1.height * factor,
-      });
-      sizes.image2.css({
-        width: sizes.size2.width * factor,
-        height: sizes.size2.height * factor,
-      });
-      sizes.image1.parent().css({
-        margin: `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`,
-        width: sizes.size1.width * factor + 2,
-        height: sizes.size1.height * factor + 2,
-      });
-      sizes.image2.parent().css({
-        margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
-        width: sizes.size2.width * factor + 2,
-        height: sizes.size2.height * factor + 2,
-      });
+      const image1 = sizes.$image1[0];
+      if (image1) {
+        const container = image1.parentNode;
+        image1.style.width = `${sizes.size1.width * factor}px`;
+        image1.style.height = `${sizes.size1.height * factor}px`;
+        container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`;
+        container.style.width = `${sizes.size1.width * factor + 2}px`;
+        container.style.height = `${sizes.size1.height * factor + 2}px`;
+      }
 
-      // some inner elements are `position: absolute`, so the container's height must be large enough
-      // the "css(width, height)" is somewhat hacky and not easy to understand, it could be improved in the future
-      sizes.image2.parent().parent().css({
-        width: sizes.max.width * factor + 2,
-        height: sizes.max.height * factor + 2,
-      });
+      const image2 = sizes.$image2[0];
+      if (image2) {
+        const container = image2.parentNode;
+        const overlayFrame = container.parentNode;
+        image2.style.width = `${sizes.size2.width * factor}px`;
+        image2.style.height = `${sizes.size2.height * factor}px`;
+        container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
+        container.style.width = `${sizes.size2.width * factor + 2}px`;
+        container.style.height = `${sizes.size2.height * factor + 2}px`;
 
-      const $range = $container.find("input[type='range']");
-      const onInput = () => sizes.image1.parent().css({
-        opacity: $range.val() / 100,
-      });
-      $range.on('input', onInput);
-      onInput();
+        // some inner elements are `position: absolute`, so the container's height must be large enough
+        overlayFrame.style.width = `${sizes.max.width * factor + 2}px`;
+        overlayFrame.style.height = `${sizes.max.height * factor + 2}px`;
+      }
+
+      const rangeInput = $container[0].querySelector('input[type="range"]');
+      function updateOpacity() {
+        if (sizes?.$image1?.[0]) {
+          sizes.$image1[0].parentNode.style.opacity = `${rangeInput.value / 100}`;
+        }
+      }
+      rangeInput?.addEventListener('input', updateOpacity);
+      updateOpacity();
     }
   });
 }

From 9c6e6f4d1b7acf6adc4b201da2dbc51d092c9a17 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 08:14:48 +0100
Subject: [PATCH 497/679] Enable a few stylelint rules (#30038)

No violations but still good to have them.
---
 .stylelintrc.yaml | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
index c7725159f1..60cce7dbf7 100644
--- a/.stylelintrc.yaml
+++ b/.stylelintrc.yaml
@@ -30,7 +30,7 @@ rules:
   "@stylistic/block-opening-brace-newline-after": null
   "@stylistic/block-opening-brace-newline-before": null
   "@stylistic/block-opening-brace-space-after": null
-  "@stylistic/block-opening-brace-space-before": null
+  "@stylistic/block-opening-brace-space-before": always
   "@stylistic/color-hex-case": lower
   "@stylistic/declaration-bang-space-after": never
   "@stylistic/declaration-bang-space-before": null
@@ -140,7 +140,7 @@ rules:
   function-disallowed-list: null
   function-linear-gradient-no-nonstandard-direction: true
   function-name-case: lower
-  function-no-unknown: null
+  function-no-unknown: true
   function-url-no-scheme-relative: null
   function-url-quotes: always
   function-url-scheme-allowed-list: null
@@ -168,7 +168,7 @@ rules:
   no-duplicate-selectors: true
   no-empty-source: true
   no-invalid-double-slash-comments: true
-  no-invalid-position-at-import-rule: null
+  no-invalid-position-at-import-rule: [true, ignoreAtRules: [tailwind]]
   no-irregular-whitespace: true
   no-unknown-animations: null
   no-unknown-custom-properties: null
@@ -181,6 +181,7 @@ rules:
   rule-empty-line-before: null
   rule-selector-property-disallowed-list: null
   scale-unlimited/declaration-strict-value: [[/color$/, font-weight], {ignoreValues: /^(inherit|transparent|unset|initial|currentcolor|none)$/, ignoreFunctions: false, disableFix: true, expandShorthand: true}]
+  selector-anb-no-unmatchable: true
   selector-attribute-name-disallowed-list: null
   selector-attribute-operator-allowed-list: null
   selector-attribute-operator-disallowed-list: null

From db01bf6cc88a8a7b5132b9306b3af1649566b10f Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 13:14:03 +0100
Subject: [PATCH 498/679] Various code view improvements (#30014)

1. Restore missing styles for message close icon
2. Move `code-line-button` so that it does not go off-screen on small
viewports
3. Make `code-line-button` look and behave like other buttons
4. Make `code-line-button` work in blame
5. Make the active selection span the whole line, not just the code part
6. Tweak colors, make dark theme code bg darker, make line numbers same
color in diff and file view.
7. Move code background to parent, fixing border radius and other
problems
8. Enable code wrap in blame
9. Improve blame responsiveness
10. Remove `--color-code-sidebar-bg` in blame, now it uses same
background as code
11. Rename `--color-active-line` to `--color-highlight-bg`
12. Add `--color-highlight-bg`
13. Fix button group borders on hover and border-right on last button.

<img width="1343" alt="Screenshot 2024-03-23 at 22 34 13"
src="https://github.com/go-gitea/gitea/assets/115237/fcbb919f-5dc3-43f0-97f6-870d6f412554">
<img width="1334" alt="Screenshot 2024-03-23 at 22 34 26"
src="https://github.com/go-gitea/gitea/assets/115237/ca44c3b7-4328-4645-ba49-b0dc6a5ac06d">

<img width="1338" alt="Screenshot 2024-03-23 at 22 34 57"
src="https://github.com/go-gitea/gitea/assets/115237/00eb0b5a-1ec7-4669-a94a-4602b9d1c1ac">
<img width="1337" alt="Screenshot 2024-03-23 at 22 34 42"
src="https://github.com/go-gitea/gitea/assets/115237/752edc4a-064f-413c-9dff-c086187fcd85">

Fixes: https://github.com/go-gitea/gitea/issues/18074
---
 routers/web/repo/blame.go                |  2 +-
 templates/devtest/gitea-ui.tmpl          |  7 ++
 templates/repo/blame.tmpl                | 12 +++-
 web_src/css/base.css                     | 80 ++++++++++++---------
 web_src/css/chroma/base.css              |  4 --
 web_src/css/modules/button.css           | 12 +++-
 web_src/css/modules/message.css          | 12 ++++
 web_src/css/repo.css                     |  1 -
 web_src/css/repo/linebutton.css          |  8 +--
 web_src/css/themes/theme-gitea-dark.css  | 12 ++--
 web_src/css/themes/theme-gitea-light.css |  8 +--
 web_src/js/features/repo-code.js         | 90 ++++++++++++------------
 12 files changed, 145 insertions(+), 103 deletions(-)

diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index b088b8387e..549ccdeabe 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -278,7 +278,7 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames
 
 				var avatar string
 				if commit.User != nil {
-					avatar = string(avatarUtils.Avatar(commit.User, 18, "gt-mr-3"))
+					avatar = string(avatarUtils.Avatar(commit.User, 18))
 				} else {
 					avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "gt-mr-3"))
 				}
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index f71b6611c5..6341076323 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -2,6 +2,13 @@
 <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}">
 <div class="page-content devtest ui container">
 	<div>
+		<h1>Link</h1>
+		<div>
+			<a href="#">normal</a>
+			<a class="muted" href="#">muted</a>
+			<a class="suppressed" href="#">suppressed</a>
+			<a class="silenced" href="#">silenced</a>
+		</div>
 		<h1>Button</h1>
 		<div>
 			Style:
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 05cdf53b44..fc6b65f142 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -41,11 +41,11 @@
 											{{$row.Avatar}}
 										</div>
 										<div class="blame-message">
-											<a href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
+											<a class="suppressed tw-text-text" href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
 												{{$row.CommitMessage}}
 											</a>
 										</div>
-										<div class="blame-time">
+										<div class="blame-time not-mobile">
 											{{$row.CommitSince}}
 										</div>
 									</div>
@@ -53,7 +53,7 @@
 							</td>
 							<td class="lines-blame-btn">
 								{{if $row.PreviousSha}}
-									<a href="{{$row.PreviousShaURL}}" data-tooltip-content='{{ctx.Locale.Tr "repo.blame_prior"}}'>
+									<a role="button" class="muted" href="{{$row.PreviousShaURL}}" data-tooltip-content='{{ctx.Locale.Tr "repo.blame_prior"}}'>
 										{{svg "octicon-versions"}}
 									</a>
 								{{end}}
@@ -75,6 +75,12 @@
 					{{end}}
 				</tbody>
 			</table>
+			<div class="code-line-menu tippy-target">
+				{{if $.Permission.CanRead $.UnitTypeIssues}}
+					<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
+				{{end}}
+				<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
+			</div>
 		</div>
 	</div>
 </div>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index dba379e7c8..cb49fa9c58 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -215,10 +215,14 @@ progress::-moz-progress-bar {
 a {
   color: var(--color-primary);
   cursor: pointer;
-  text-decoration: none;
+  text-decoration-line: none;
   text-decoration-skip-ink: all;
 }
 
+a:hover {
+  text-decoration-line: underline;
+}
+
 /* a = always colored, underlined on hover */
 /* a.muted = colored on hover, underlined on hover */
 /* a.suppressed = never colored, underlined on hover */
@@ -245,7 +249,7 @@ a.suppressed:hover {
 }
 
 a.silenced:hover {
-  text-decoration: none;
+  text-decoration-line: none;
 }
 
 a.label,
@@ -253,7 +257,7 @@ a.label,
 .ui .menu a,
 .ui.cards a.card,
 .issue-keyword a {
-  text-decoration: none !important;
+  text-decoration-line: none !important;
 }
 
 .ui.search > .results {
@@ -1418,18 +1422,15 @@ a.ui.active.label:hover {
 }
 
 .lines-blame-btn {
-  padding-left: 10px;
-  padding-right: 10px;
-  text-align: right !important;
-  background-color: var(--color-code-sidebar-bg);
-  width: 2%;
+  padding: 0 0 0 5px;
+  display: flex;
+  justify-content: center;
 }
 
 .lines-num {
-  padding-left: 10px;
-  padding-right: 10px;
+  padding: 0 8px;
   text-align: right !important;
-  color: var(--color-text-light-1);
+  color: var(--color-text-light-2);
   width: 1%;
   font-family: var(--fonts-monospace);
 }
@@ -1483,22 +1484,34 @@ a.ui.active.label:hover {
 }
 
 .lines-code {
-  background-color: var(--color-code-bg);
   padding-left: 5px;
 }
 
-.lines-code.active,
-.lines-code .active {
-  background: var(--color-active-line) !important;
+.file-view tr.active {
+  color: inherit !important;
+  background: inherit !important;
 }
 
-.blame .lines-num {
-  padding: 0 !important;
-  background-color: var(--color-code-sidebar-bg);
+.file-view tr.active .lines-num,
+.file-view tr.active .lines-code {
+  background: var(--color-highlight-bg) !important;
 }
 
-.blame .lines-code {
-  padding: 0 !important;
+.file-view tr.active:last-of-type .lines-code {
+  border-bottom-right-radius: var(--border-radius);
+}
+
+.file-view tr.active .lines-num {
+  position: relative;
+}
+
+.file-view tr.active .lines-num::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  width: 2px;
+  height: 100%;
+  background: var(--color-highlight-fg);
 }
 
 .code-inner {
@@ -1509,24 +1522,21 @@ a.ui.active.label:hover {
 }
 
 .blame .code-inner {
-  white-space: pre;
-  word-break: normal;
-  word-wrap: normal; /* not using overflow-wrap because safari does not treat is an an alias */
+  white-space: pre-wrap;
+  overflow-wrap: anywhere;
 }
 
 .lines-commit {
   vertical-align: top;
-  color: var(--color-text-light-2);
+  color: var(--color-text-light-1);
   padding: 0 !important;
-  background: var(--color-code-sidebar-bg);
   width: 1%;
 }
 
 .lines-commit .blame-info {
-  width: 350px;
-  max-width: 350px;
+  width: min(26vw, 300px);
   display: block;
-  padding: 0 0 0 10px;
+  padding: 0 0 0 6px;
   line-height: 20px;
   box-sizing: content-box;
 }
@@ -1548,11 +1558,10 @@ a.ui.active.label:hover {
   flex-shrink: 0;
 }
 
-.lines-commit .ui.avatar {
-  height: 18px;
-  width: 18px;
-  display: block;
-  margin-top: 1px;
+.blame-avatar {
+  display: flex;
+  align-items: center;
+  margin-right: 4px;
 }
 
 .top-line-blame {
@@ -1568,6 +1577,11 @@ a.ui.active.label:hover {
   border-bottom: 1px solid var(--color-secondary);
 }
 
+.code-view {
+  background: var(--color-code-bg);
+  border-radius: var(--border-radius);
+}
+
 .code-view table {
   width: 100%;
 }
diff --git a/web_src/css/chroma/base.css b/web_src/css/chroma/base.css
index 26d128775f..bce13332f8 100644
--- a/web_src/css/chroma/base.css
+++ b/web_src/css/chroma/base.css
@@ -1,7 +1,3 @@
-.chroma {
-  background-color: var(--color-code-bg);
-}
-
 /* LineTableTD */
 .chroma .lntd {
   vertical-align: top;
diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css
index 26f8fcf94c..e72260d99b 100644
--- a/web_src/css/modules/button.css
+++ b/web_src/css/modules/button.css
@@ -11,6 +11,7 @@
 .ui.button:hover {
   background: var(--color-hover);
   color: var(--color-text);
+  border-color: var(--color-secondary-dark-2);
 }
 
 .page-content .ui.button {
@@ -61,11 +62,17 @@ It needs some tricks to tweak the left/right borders with active state */
   border-right: none;
 }
 
-.ui.buttons .button:first-child {
+.ui.buttons .button:hover + .button {
+  border-left: 1px solid var(--color-secondary-dark-2);
+}
+
+.ui.buttons .button:first-child,
+.ui.buttons .button.gt-hidden:first-child + .button {
   border-left: 1px solid var(--color-light-border);
 }
 
-.ui.buttons .button:last-child {
+.ui.buttons .button:last-child,
+.ui.buttons .button:nth-last-child(2):has(+ .button.gt-hidden) {
   border-right: 1px solid var(--color-light-border);
 }
 
@@ -105,6 +112,7 @@ It needs some tricks to tweak the left/right borders with active state */
 .ui.basic.button:hover {
   color: var(--color-text);
   background: var(--color-hover);
+  border-color: var(--color-secondary-dark-2);
 }
 
 .ui.basic.buttons .button:active,
diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css
index a29603cd91..c62dbddd25 100644
--- a/web_src/css/modules/message.css
+++ b/web_src/css/modules/message.css
@@ -100,3 +100,15 @@
   color: var(--color-warning-text);
   border-color: var(--color-warning-border);
 }
+
+.ui.message > .close.icon {
+  cursor: pointer;
+  position: absolute;
+  top: 9px;
+  right: 9px;
+  opacity: .7;
+}
+
+.ui.message > .close.icon:hover {
+  opacity: 1;
+}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index ca8de42a06..2014dfc370 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1602,7 +1602,6 @@
 
 .repository .diff-file-box .file-body.file-code .lines-num {
   text-align: right;
-  color: var(--color-text-light);
   width: 1%;
   min-width: 50px;
 }
diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css
index 79be5a7a9e..e99d0399d1 100644
--- a/web_src/css/repo/linebutton.css
+++ b/web_src/css/repo/linebutton.css
@@ -3,18 +3,16 @@
 }
 
 .code-line-button {
-  background-color: var(--color-menu);
-  color: var(--color-text-light);
   border: 1px solid var(--color-secondary);
   border-radius: var(--border-radius);
-  padding: 1px 10px;
+  padding: 1px 4px !important;
   position: absolute;
   font-family: var(--fonts-regular);
   left: 0;
-  transform: translateX(-50%);
+  transform: translateX(calc(-50% + 6px));
   cursor: pointer;
 }
 
 .code-line-button:hover {
-  color: var(--color-primary);
+  background: var(--color-secondary) !important;
 }
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index c769c51cdc..626590ca54 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -183,7 +183,7 @@
   --color-body: #1c1f25;
   --color-box-header: #1a1d1f;
   --color-box-body: #14171a;
-  --color-box-body-highlight: #121517;
+  --color-box-body-highlight: #1c2227;
   --color-text-dark: #f8f8f9;
   --color-text: #d1d5d8;
   --color-text-light: #bdc3c7;
@@ -207,11 +207,10 @@
   --color-markup-table-row: #e8e8ff06;
   --color-markup-code-block: #e8e8ff16;
   --color-button: #151a1e;
-  --color-code-bg: #191d20;
-  --color-code-sidebar-bg: #1b1f22;
+  --color-code-bg: #14171a;
   --color-shadow: #00001758;
-  --color-secondary-bg: #2f3135;
-  --color-expand-button: #414348;
+  --color-secondary-bg: #2f3138;
+  --color-expand-button: #2b353e;
   --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-5);
   --color-project-board-bg: var(--color-secondary-light-2);
@@ -233,7 +232,8 @@
   --color-label-active-bg: #73828eff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-5);
-  --color-active-line: #534d1b;
+  --color-highlight-fg: #87651e;
+  --color-highlight-bg: #352c1c;
   --color-overlay-backdrop: #080808c0;
   accent-color: var(--color-accent);
   color-scheme: dark;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 2d9ab8e721..f6913fbe22 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -183,7 +183,7 @@
   --color-body: #ffffff;
   --color-box-header: #f1f3f5;
   --color-box-body: #ffffff;
-  --color-box-body-highlight: #f4faff;
+  --color-box-body-highlight: #ecf5fd;
   --color-text-dark: #01050a;
   --color-text: #181c21;
   --color-text-light: #30363b;
@@ -208,10 +208,9 @@
   --color-markup-code-block: #00001710;
   --color-button: #f8f9fb;
   --color-code-bg: #fafdff;
-  --color-code-sidebar-bg: #f2f5f8;
   --color-shadow: #00001726;
   --color-secondary-bg: #f2f5f8;
-  --color-expand-button: #d8efff;
+  --color-expand-button: #cfe8fa;
   --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-6);
   --color-project-board-bg: var(--color-secondary-light-4);
@@ -233,7 +232,8 @@
   --color-label-active-bg: #949da6ff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-6);
-  --color-active-line: #fffbdd;
+  --color-highlight-fg: #eed200;
+  --color-highlight-bg: #fffbdd;
   --color-overlay-backdrop: #080808c0;
   accent-color: var(--color-accent);
   color-scheme: light;
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index 08fae763b8..befa090004 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -16,8 +16,16 @@ function changeHash(hash) {
   }
 }
 
-function selectRange($list, $select, $from) {
-  $list.removeClass('active');
+function isBlame() {
+  return Boolean(document.querySelector('div.blame'));
+}
+
+function getLineEls() {
+  return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`);
+}
+
+function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
+  $linesEls.closest('tr').removeClass('active');
 
   // add hashchange to permalink
   const $refInNewIssue = $('a.ref-in-new-issue');
@@ -25,7 +33,7 @@ function selectRange($list, $select, $from) {
   const $viewGitBlame = $('a.view_git_blame');
 
   const updateIssueHref = function (anchor) {
-    if ($refInNewIssue.length === 0) {
+    if (!$refInNewIssue.length) {
       return;
     }
     const urlIssueNew = $refInNewIssue.attr('data-url-issue-new');
@@ -35,9 +43,7 @@ function selectRange($list, $select, $from) {
   };
 
   const updateViewGitBlameFragment = function (anchor) {
-    if ($viewGitBlame.length === 0) {
-      return;
-    }
+    if (!$viewGitBlame.length) return;
     let href = $viewGitBlame.attr('href');
     href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`;
     if (anchor.length !== 0) {
@@ -47,17 +53,15 @@ function selectRange($list, $select, $from) {
   };
 
   const updateCopyPermalinkUrl = function(anchor) {
-    if ($copyPermalink.length === 0) {
-      return;
-    }
+    if (!$copyPermalink.length) return;
     let link = $copyPermalink.attr('data-url');
     link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`;
     $copyPermalink.attr('data-url', link);
   };
 
-  if ($from) {
-    let a = parseInt($select.attr('rel').slice(1));
-    let b = parseInt($from.attr('rel').slice(1));
+  if ($selectionStartEls) {
+    let a = parseInt($selectionEndEl.attr('rel').slice(1));
+    let b = parseInt($selectionStartEls.attr('rel').slice(1));
     let c;
     if (a !== b) {
       if (a > b) {
@@ -69,7 +73,9 @@ function selectRange($list, $select, $from) {
       for (let i = a; i <= b; i++) {
         classes.push(`[rel=L${i}]`);
       }
-      $list.filter(classes.join(',')).addClass('active');
+      $linesEls.filter(classes.join(',')).each(function () {
+        $(this).closest('tr').addClass('active');
+      });
       changeHash(`#L${a}-L${b}`);
 
       updateIssueHref(`L${a}-L${b}`);
@@ -78,12 +84,12 @@ function selectRange($list, $select, $from) {
       return;
     }
   }
-  $select.addClass('active');
-  changeHash(`#${$select.attr('rel')}`);
+  $selectionEndEl.closest('tr').addClass('active');
+  changeHash(`#${$selectionEndEl.attr('rel')}`);
 
-  updateIssueHref($select.attr('rel'));
-  updateViewGitBlameFragment($select.attr('rel'));
-  updateCopyPermalinkUrl($select.attr('rel'));
+  updateIssueHref($selectionEndEl.attr('rel'));
+  updateViewGitBlameFragment($selectionEndEl.attr('rel'));
+  updateCopyPermalinkUrl($selectionEndEl.attr('rel'));
 }
 
 function showLineButton() {
@@ -96,10 +102,10 @@ function showLineButton() {
   }
 
   // find active row and add button
-  const tr = document.querySelector('.code-view td.lines-code.active').closest('tr');
-  const td = tr.querySelector('td');
+  const tr = document.querySelector('.code-view tr.active');
+  const td = tr.querySelector('td.lines-num');
   const btn = document.createElement('button');
-  btn.classList.add('code-line-button');
+  btn.classList.add('code-line-button', 'ui', 'basic', 'button');
   btn.innerHTML = svg('octicon-kebab-horizontal');
   td.prepend(btn);
 
@@ -123,14 +129,18 @@ function showLineButton() {
 export function initRepoCodeView() {
   if ($('.code-view .lines-num').length > 0) {
     $(document).on('click', '.lines-num span', function (e) {
-      const $select = $(this);
-      let $list;
-      if ($('div.blame').length) {
-        $list = $('.code-view td.lines-code.blame-code');
-      } else {
-        $list = $('.code-view td.lines-code');
+      const linesEls = getLineEls();
+      const selectedEls = Array.from(linesEls).filter((el) => {
+        return el.matches(`[rel=${this.getAttribute('id')}]`);
+      });
+
+      let from;
+      if (e.shiftKey) {
+        from = Array.from(linesEls).filter((el) => {
+          return el.closest('tr').classList.contains('active');
+        });
       }
-      selectRange($list, $list.filter(`[rel=${$select.attr('id')}]`), (e.shiftKey ? $list.filter('.active').eq(0) : null));
+      selectRange($(linesEls), $(selectedEls), from ? $(from) : null);
 
       if (window.getSelection) {
         window.getSelection().removeAllRanges();
@@ -138,28 +148,20 @@ export function initRepoCodeView() {
         document.selection.empty();
       }
 
-      // show code view menu marker (don't show in blame page)
-      if ($('div.blame').length === 0) {
-        showLineButton();
-      }
+      showLineButton();
     });
 
     $(window).on('hashchange', () => {
       let m = window.location.hash.match(rangeAnchorRegex);
-      let $list;
-      if ($('div.blame').length) {
-        $list = $('.code-view td.lines-code.blame-code');
-      } else {
-        $list = $('.code-view td.lines-code');
-      }
+      const $linesEls = $(getLineEls());
       let $first;
       if (m) {
-        $first = $list.filter(`[rel=${m[1]}]`);
+        $first = $linesEls.filter(`[rel=${m[1]}]`);
         if ($first.length) {
-          selectRange($list, $first, $list.filter(`[rel=${m[2]}]`));
+          selectRange($linesEls, $first, $linesEls.filter(`[rel=${m[2]}]`));
 
           // show code view menu marker (don't show in blame page)
-          if ($('div.blame').length === 0) {
+          if (!isBlame()) {
             showLineButton();
           }
 
@@ -169,12 +171,12 @@ export function initRepoCodeView() {
       }
       m = window.location.hash.match(singleAnchorRegex);
       if (m) {
-        $first = $list.filter(`[rel=L${m[2]}]`);
+        $first = $linesEls.filter(`[rel=L${m[2]}]`);
         if ($first.length) {
-          selectRange($list, $first);
+          selectRange($linesEls, $first);
 
           // show code view menu marker (don't show in blame page)
-          if ($('div.blame').length === 0) {
+          if (!isBlame()) {
             showLineButton();
           }
 

From f22fe4e1944d8084dec7c04f064a8e782fca94d4 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 14:32:19 +0100
Subject: [PATCH 499/679] Remove fomantic header module (#30033)

Likely still a few useless classes left, but I think I at least don't
have missed any.

---------

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 templates/repo/diff/box.tmpl              |   4 +-
 templates/shared/search/code/results.tmpl |   2 +-
 web_src/css/base.css                      |  86 ---
 web_src/css/index.css                     |   1 +
 web_src/css/modules/header.css            | 171 +++++
 web_src/fomantic/build/semantic.css       | 735 ----------------------
 web_src/fomantic/semantic.json            |   1 -
 7 files changed, 175 insertions(+), 825 deletions(-)
 create mode 100644 web_src/css/modules/header.css

diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 0761e3f7bd..51c0aae007 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -110,7 +110,7 @@
 					{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
 					{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
 					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
-						<h4 class="diff-file-header sticky-2nd-row ui top attached normal header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
+						<h4 class="diff-file-header sticky-2nd-row ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between tw-flex-wrap">
 							<div class="diff-file-name tw-flex tw-items-center gt-gap-2 tw-flex-wrap">
 								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
@@ -218,7 +218,7 @@
 
 				{{if .Diff.IsIncomplete}}
 					<div class="diff-file-box diff-box file-content gt-mt-3" id="diff-incomplete">
-						<h4 class="ui top attached normal header tw-flex tw-items-center tw-justify-between">
+						<h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between">
 							{{ctx.Locale.Tr "repo.diff.too_many_files"}}
 							<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
 						</h4>
diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl
index 3bae959006..42e029da82 100644
--- a/templates/shared/search/code/results.tmpl
+++ b/templates/shared/search/code/results.tmpl
@@ -12,7 +12,7 @@
 	{{range $result := .SearchResults}}
 		{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
 		<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
-			<h4 class="ui top attached normal header tw-flex tw-flex-wrap">
+			<h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap">
 				{{if not $.Repo}}
 					<span class="file tw-flex-1">
 						<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index cb49fa9c58..226ec70386 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -297,10 +297,6 @@ a.label,
   background-color: var(--color-markup-code-block);
 }
 
-.ui.dividing.header {
-  border-bottom-color: var(--color-secondary);
-}
-
 /* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */
 .ui.input > input {
   line-height: var(--line-height-default);
@@ -576,22 +572,10 @@ ol.ui.list li,
   visibility: visible !important;
 }
 
-.ui.error.header {
-  background: var(--color-error-bg) !important;
-  color: var(--color-error-text) !important;
-  border-color: var(--color-error-border) !important;
-}
-
 .ui.error.segment {
   border-color: var(--color-error-border) !important;
 }
 
-.ui.warning.header {
-  background: var(--color-warning-bg) !important;
-  color: var(--color-warning-text) !important;
-  border-color: var(--color-warning-border) !important;
-}
-
 .ui.warning.segment {
   border-color: var(--color-warning-border) !important;
 }
@@ -1085,10 +1069,6 @@ input:-webkit-autofill:active,
   margin-bottom: 0;
 }
 
-.ui .normal.header {
-  font-weight: var(--font-weight-normal);
-}
-
 .ui .form .autofill-dummy {
   position: absolute;
   width: 1px;
@@ -1246,17 +1226,6 @@ input:-webkit-autofill:active,
   margin-right: 0;
 }
 
-.ui.icon.header svg {
-  width: 3em;
-  height: 3em;
-  float: none;
-  display: block;
-  line-height: var(--line-height-default);
-  padding: 0;
-  margin: 0 auto 0.5rem;
-  opacity: 1;
-}
-
 .ui.floating.dropdown .overflow.menu .scrolling.menu.items {
   border-radius: 0 !important;
   box-shadow: none !important;
@@ -1284,11 +1253,6 @@ input:-webkit-autofill:active,
   border-radius: var(--border-radius);
 }
 
-.attention-header {
-  padding: 0.5em 0.75em !important;
-  color: var(--color-text) !important;
-}
-
 .attention-icon {
   margin: 2px 6px 0 0;
 }
@@ -1758,35 +1722,6 @@ a.ui.basic.label:hover {
   color: var(--color-text-light);
 }
 
-.ui.attached.header {
-  position: relative;
-  background: var(--color-box-header);
-  border-color: var(--color-secondary);
-}
-
-/* fix misaligned right buttons on box headers */
-.ui.attached.header > .ui.right {
-  position: absolute;
-  right: 0.78571429rem;
-  top: 0;
-  bottom: 0;
-  display: flex;
-  align-items: center;
-  gap: 0.25em;
-}
-
-/* the default ".ui.attached.header > .ui.right" is only able to contain "tiny" buttons, other buttons are too large */
-.ui.attached.header > .ui.right .ui.tiny.button {
-  padding: 6px 10px;
-  font-weight: var(--font-weight-normal);
-}
-
-/* if a .top.attached.header is followed by a .segment, add some margin */
-.ui.segments + .ui.top.attached.header,
-.ui.attached.segment + .ui.top.attached.header {
-  margin-top: 1rem;
-}
-
 .rss-icon {
   display: inline-flex;
   color: var(--color-text-light-1);
@@ -1842,11 +1777,6 @@ table th[data-sortt-desc] .svg {
   background: var(--color-secondary-dark-1) !important;
 }
 
-/* https://github.com/go-gitea/gitea/pull/11486 */
-.ui.sub.header {
-  text-transform: none;
-}
-
 .ui.tabular.menu {
   border-color: var(--color-secondary);
 }
@@ -1897,22 +1827,6 @@ table th[data-sortt-desc] .svg {
   height: 0;
 }
 
-.ui.header {
-  color: var(--color-text);
-}
-
-.ui.header .ui.label {
-  margin-left: 0.25rem;
-}
-
-.ui.header > .ui.label.compact {
-  margin-top: inherit;
-}
-
-.ui.header .sub.header {
-  color: var(--color-text-light-1);
-}
-
 .flash-error details code,
 .flash-warning details code {
   display: block;
diff --git a/web_src/css/index.css b/web_src/css/index.css
index bf568bff4d..e8df321829 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -14,6 +14,7 @@
 @import "./modules/svg.css";
 @import "./modules/flexcontainer.css";
 @import "./modules/message.css";
+@import "./modules/header.css";
 
 @import "./shared/flex-list.css";
 @import "./shared/milestone.css";
diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css
new file mode 100644
index 0000000000..091d536cfc
--- /dev/null
+++ b/web_src/css/modules/header.css
@@ -0,0 +1,171 @@
+/* based on Fomantic UI header module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.header {
+  color: var(--color-text);
+  border: none;
+  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
+  padding: 0;
+  font-family: var(--fonts-regular);
+  font-weight: var(--font-weight-medium);
+  line-height: 1.28571429;
+  text-transform: none;
+}
+
+.ui.header:first-child {
+  margin-top: -0.14285714em;
+}
+
+.ui.header:last-child {
+  margin-bottom: 0;
+}
+
+.ui.header .ui.label {
+  margin-left: 0.25rem;
+  vertical-align: middle;
+}
+
+.ui.header > .ui.label.compact {
+  margin-top: inherit;
+}
+
+.ui.header .sub.header {
+  display: block;
+  font-weight: var(--font-weight-normal);
+  padding: 0;
+  margin: 0;
+  font-size: 1rem;
+  line-height: 1.2;
+  color: var(--color-text-light-1);
+}
+
+.ui.header > i.icon {
+  display: table-cell;
+  opacity: 1;
+  font-size: 1.5em;
+  padding-top: 0;
+  vertical-align: middle;
+}
+
+.ui.header > i.icon:only-child {
+  display: inline-block;
+  padding: 0;
+  margin-right: 0.75rem;
+}
+
+.ui.header + p {
+  margin-top: 0;
+}
+
+h2.ui.header {
+  font-size: 1.71428571rem;
+}
+h2.ui.header .sub.header {
+  font-size: 1.14285714rem;
+}
+
+h4.ui.header {
+  font-size: 1.07142857rem;
+}
+h4.ui.header .sub.header {
+  font-size: 1rem;
+}
+
+.ui.sub.header {
+  padding: 0;
+  margin-bottom: 0.14285714rem;
+  font-weight: var(--font-weight-normal);
+  font-size: 0.85714286em;
+}
+
+.ui.icon.header svg {
+  width: 3em;
+  height: 3em;
+  float: none;
+  display: block;
+  line-height: var(--line-height-default);
+  padding: 0;
+  margin: 0 auto 0.5rem;
+  opacity: 1;
+}
+
+.ui.header:not(h1,h2,h3,h4,h5,h6) {
+  font-size: 1.28571429em;
+}
+
+.ui.attached.header {
+  position: relative;
+  background: var(--color-box-header);
+  padding: 0.78571429rem 1rem;
+  margin: 0 -1px;
+  border-radius: 0;
+  border: 1px solid var(--color-secondary);
+}
+
+.ui.attached:not(.top).header {
+  border-top: none;
+}
+
+.ui.top.attached.header {
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.bottom.attached.header {
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.attached.header:not(h1,h2,h3,h4,h5,h6) {
+  font-size: 1em;
+}
+
+/* fix misaligned right buttons on box headers */
+.ui.attached.header > .ui.right {
+  position: absolute;
+  right: 0.78571429rem;
+  top: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  gap: 0.25em;
+}
+
+/* the default ".ui.attached.header > .ui.right" is only able to contain "tiny" buttons, other buttons are too large */
+.ui.attached.header > .ui.right .ui.tiny.button {
+  padding: 6px 10px;
+  font-weight: var(--font-weight-normal);
+}
+
+/* if a .top.attached.header is followed by a .segment, add some margin */
+.ui.segments + .ui.top.attached.header,
+.ui.attached.segment + .ui.top.attached.header {
+  margin-top: 1rem;
+}
+
+.ui.dividing.header {
+  border-bottom-color: var(--color-secondary);
+}
+
+.ui.dividing.header .sub.header {
+  padding-bottom: 0.21428571rem;
+}
+
+.ui.dividing.header i.icon {
+  margin-bottom: 0;
+}
+
+.ui.error.header {
+  background: var(--color-error-bg) !important;
+  color: var(--color-error-text) !important;
+  border-color: var(--color-error-border) !important;
+}
+
+.ui.warning.header {
+  background: var(--color-warning-bg) !important;
+  color: var(--color-warning-text) !important;
+  border-color: var(--color-warning-border) !important;
+}
+
+.attention-header {
+  padding: 0.5em 0.75em !important;
+  color: var(--color-text) !important;
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 099bb94c39..4d82244277 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -7340,741 +7340,6 @@ select.ui.dropdown {
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Header
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Header
-*******************************/
-
-/* Standard */
-
-.ui.header {
-  border: none;
-  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
-  padding: 0 0;
-  font-family: var(--fonts-regular);
-  font-weight: 500;
-  line-height: 1.28571429em;
-  text-transform: none;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.header:first-child {
-  margin-top: -0.14285714em;
-}
-
-.ui.header:last-child {
-  margin-bottom: 0;
-}
-
-/*--------------
-     Sub Header
-  ---------------*/
-
-.ui.header .sub.header {
-  display: block;
-  font-weight: normal;
-  padding: 0;
-  margin: 0;
-  font-size: 1rem;
-  line-height: 1.2em;
-  color: rgba(0, 0, 0, 0.6);
-}
-
-/*--------------
-      Icon
----------------*/
-
-.ui.header > i.icon {
-  display: table-cell;
-  opacity: 1;
-  font-size: 1.5em;
-  padding-top: 0;
-  vertical-align: middle;
-}
-
-/* With Text Node */
-
-.ui.header > i.icon:only-child {
-  display: inline-block;
-  padding: 0;
-  margin-right: 0.75rem;
-}
-
-/*-------------------
-        Image
---------------------*/
-
-.ui.header > .image:not(.icon),
-.ui.header > img {
-  display: inline-block;
-  margin-top: 0.14285714em;
-  width: 2.5em;
-  height: auto;
-  vertical-align: middle;
-}
-
-.ui.header > .image:not(.icon):only-child,
-.ui.header > img:only-child {
-  margin-right: 0.75rem;
-}
-
-/*--------------
-     Content
----------------*/
-
-.ui.header .content {
-  display: inline-block;
-  vertical-align: top;
-}
-
-/* After Image */
-
-.ui.header > img + .content,
-.ui.header > .image + .content {
-  padding-left: 0.75rem;
-  vertical-align: middle;
-}
-
-/* After Icon */
-
-.ui.header > i.icon + .content {
-  padding-left: 0.75rem;
-  display: table-cell;
-  vertical-align: middle;
-}
-
-/*--------------
- Loose Coupling
----------------*/
-
-.ui.header .ui.label {
-  font-size: '';
-  margin-left: 0.5rem;
-  vertical-align: middle;
-}
-
-/* Positioning */
-
-.ui.header + p {
-  margin-top: 0;
-}
-
-/*******************************
-            Types
-*******************************/
-
-/*--------------
-     Page
----------------*/
-
-h1.ui.header {
-  font-size: 2rem;
-}
-
-h1.ui.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-h2.ui.header {
-  font-size: 1.71428571rem;
-}
-
-h2.ui.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-h3.ui.header {
-  font-size: 1.28571429rem;
-}
-
-h3.ui.header .sub.header {
-  font-size: 1rem;
-}
-
-h4.ui.header {
-  font-size: 1.07142857rem;
-}
-
-h4.ui.header .sub.header {
-  font-size: 1rem;
-}
-
-h5.ui.header {
-  font-size: 1rem;
-}
-
-h5.ui.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-h6.ui.header {
-  font-size: 0.85714286rem;
-}
-
-h6.ui.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-/*--------------
- Content Heading
----------------*/
-
-.ui.mini.header {
-  font-size: 0.85714286em;
-}
-
-.ui.mini.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-.ui.mini.sub.header {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.header {
-  font-size: 1em;
-}
-
-.ui.tiny.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-.ui.tiny.sub.header {
-  font-size: 0.78571429em;
-}
-
-.ui.small.header {
-  font-size: 1.07142857em;
-}
-
-.ui.small.header .sub.header {
-  font-size: 1rem;
-}
-
-.ui.small.sub.header {
-  font-size: 0.78571429em;
-}
-
-.ui.large.header {
-  font-size: 1.71428571em;
-}
-
-.ui.large.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.large.sub.header {
-  font-size: 0.92857143em;
-}
-
-.ui.big.header {
-  font-size: 1.85714286em;
-}
-
-.ui.big.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.sub.header {
-  font-size: 1em;
-}
-
-.ui.huge.header {
-  font-size: 2em;
-  min-height: 1em;
-}
-
-.ui.huge.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.huge.sub.header {
-  font-size: 1em;
-}
-
-.ui.massive.header {
-  font-size: 2.28571429em;
-  min-height: 1em;
-}
-
-.ui.massive.header .sub.header {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.sub.header {
-  font-size: 1.14285714em;
-}
-
-/*--------------
-     Sub Heading
-  ---------------*/
-
-.ui.sub.header {
-  padding: 0;
-  margin-bottom: 0.14285714rem;
-  font-weight: 500;
-  font-size: 0.85714286em;
-  text-transform: uppercase;
-  color: '';
-}
-
-/*-------------------
-          Icon
-  --------------------*/
-
-.ui.icon.header {
-  display: inline-block;
-  text-align: center;
-  margin: 2rem 0 1rem;
-}
-
-.ui.icon.header:after {
-  content: '';
-  display: block;
-  height: 0;
-  clear: both;
-  visibility: hidden;
-}
-
-.ui.icon.header:first-child {
-  margin-top: 0;
-}
-
-.ui.icon.header > i.icon {
-  float: none;
-  display: block;
-  width: auto;
-  height: auto;
-  line-height: 1;
-  padding: 0;
-  font-size: 3em;
-  margin: 0 auto 0.5rem;
-  opacity: 1;
-}
-
-.ui.icon.header .corner.icon {
-  font-size: calc(3em * 0.45);
-}
-
-.ui.icon.header .content {
-  display: block;
-  padding: 0;
-}
-
-.ui.icon.header > i.circular.icon {
-  font-size: 2em;
-}
-
-.ui.icon.header > i.square.icon {
-  font-size: 2em;
-}
-
-.ui.block.icon.header > i.icon {
-  margin-bottom: 0;
-}
-
-.ui.icon.header.aligned {
-  margin-left: auto;
-  margin-right: auto;
-  display: block;
-}
-
-/*******************************
-            States
-*******************************/
-
-.ui.disabled.header {
-  opacity: var(--opacity-disabled);
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.primary.header {
-  color: #2185D0;
-}
-
-a.ui.primary.header:hover {
-  color: #1678c2;
-}
-
-.ui.primary.dividing.header {
-  border-bottom: 2px solid #2185D0;
-}
-
-.ui.secondary.header {
-  color: #1B1C1D;
-}
-
-a.ui.secondary.header:hover {
-  color: #27292a;
-}
-
-.ui.secondary.dividing.header {
-  border-bottom: 2px solid #1B1C1D;
-}
-
-.ui.red.header {
-  color: #DB2828;
-}
-
-a.ui.red.header:hover {
-  color: #d01919;
-}
-
-.ui.red.dividing.header {
-  border-bottom: 2px solid #DB2828;
-}
-
-.ui.orange.header {
-  color: #F2711C;
-}
-
-a.ui.orange.header:hover {
-  color: #f26202;
-}
-
-.ui.orange.dividing.header {
-  border-bottom: 2px solid #F2711C;
-}
-
-.ui.yellow.header {
-  color: #FBBD08;
-}
-
-a.ui.yellow.header:hover {
-  color: #eaae00;
-}
-
-.ui.yellow.dividing.header {
-  border-bottom: 2px solid #FBBD08;
-}
-
-.ui.olive.header {
-  color: #B5CC18;
-}
-
-a.ui.olive.header:hover {
-  color: #a7bd0d;
-}
-
-.ui.olive.dividing.header {
-  border-bottom: 2px solid #B5CC18;
-}
-
-.ui.green.header {
-  color: #21BA45;
-}
-
-a.ui.green.header:hover {
-  color: #16ab39;
-}
-
-.ui.green.dividing.header {
-  border-bottom: 2px solid #21BA45;
-}
-
-.ui.teal.header {
-  color: #00B5AD;
-}
-
-a.ui.teal.header:hover {
-  color: #009c95;
-}
-
-.ui.teal.dividing.header {
-  border-bottom: 2px solid #00B5AD;
-}
-
-.ui.blue.header {
-  color: #2185D0;
-}
-
-a.ui.blue.header:hover {
-  color: #1678c2;
-}
-
-.ui.blue.dividing.header {
-  border-bottom: 2px solid #2185D0;
-}
-
-.ui.violet.header {
-  color: #6435C9;
-}
-
-a.ui.violet.header:hover {
-  color: #5829bb;
-}
-
-.ui.violet.dividing.header {
-  border-bottom: 2px solid #6435C9;
-}
-
-.ui.purple.header {
-  color: #A333C8;
-}
-
-a.ui.purple.header:hover {
-  color: #9627ba;
-}
-
-.ui.purple.dividing.header {
-  border-bottom: 2px solid #A333C8;
-}
-
-.ui.pink.header {
-  color: #E03997;
-}
-
-a.ui.pink.header:hover {
-  color: #e61a8d;
-}
-
-.ui.pink.dividing.header {
-  border-bottom: 2px solid #E03997;
-}
-
-.ui.brown.header {
-  color: #A5673F;
-}
-
-a.ui.brown.header:hover {
-  color: #975b33;
-}
-
-.ui.brown.dividing.header {
-  border-bottom: 2px solid #A5673F;
-}
-
-.ui.grey.header {
-  color: #767676;
-}
-
-a.ui.grey.header:hover {
-  color: #838383;
-}
-
-.ui.grey.dividing.header {
-  border-bottom: 2px solid #767676;
-}
-
-.ui.black.header {
-  color: #1B1C1D;
-}
-
-a.ui.black.header:hover {
-  color: #27292a;
-}
-
-.ui.black.dividing.header {
-  border-bottom: 2px solid #1B1C1D;
-}
-
-/*-------------------
-         Aligned
-  --------------------*/
-
-.ui.left.aligned.header {
-  text-align: left;
-}
-
-.ui.right.aligned.header {
-  text-align: right;
-}
-
-.ui.centered.header,
-.ui.center.aligned.header {
-  text-align: center;
-}
-
-.ui.justified.header {
-  text-align: justify;
-}
-
-.ui.justified.header:after {
-  display: inline-block;
-  content: '';
-  width: 100%;
-}
-
-/*-------------------
-         Floated
-  --------------------*/
-
-.ui.floated.header,
-.ui[class*="left floated"].header {
-  float: left;
-  margin-top: 0;
-  margin-right: 0.5em;
-}
-
-.ui[class*="right floated"].header {
-  float: right;
-  margin-top: 0;
-  margin-left: 0.5em;
-}
-
-/*-------------------
-         Fitted
-  --------------------*/
-
-.ui.fitted.header {
-  padding: 0;
-}
-
-/*-------------------
-        Dividing
-  --------------------*/
-
-.ui.dividing.header {
-  padding-bottom: 0.21428571rem;
-  border-bottom: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.dividing.header .sub.header {
-  padding-bottom: 0.21428571rem;
-}
-
-.ui.dividing.header i.icon {
-  margin-bottom: 0;
-}
-
-/*-------------------
-          Block
-  --------------------*/
-
-.ui.block.header {
-  background: #F3F4F5;
-  padding: 0.78571429rem 1rem;
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-  border-radius: 0.28571429rem;
-}
-
-.ui.block.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) {
-  font-size: 1rem;
-}
-
-.ui.mini.block.header {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.block.header {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.block.header {
-  font-size: 0.92857143rem;
-}
-
-.ui.large.block.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.block.header {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.block.header {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.block.header {
-  font-size: 1.71428571rem;
-}
-
-/*-------------------
-         Attached
-  --------------------*/
-
-.ui.attached.header {
-  background: #FFFFFF;
-  padding: 0.78571429rem 1rem;
-  margin: 0 -1px 0 -1px;
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-  border-radius: 0;
-}
-
-.ui.attached.block.header {
-  background: #F3F4F5;
-}
-
-.ui.attached:not(.top).header {
-  border-top: none;
-}
-
-.ui.top.attached.header {
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-.ui.bottom.attached.header {
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-/* Attached Sizes */
-
-.ui.attached.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) {
-  font-size: 1em;
-}
-
-.ui.mini.attached.header {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.attached.header {
-  font-size: 0.85714286em;
-}
-
-.ui.small.attached.header {
-  font-size: 0.92857143em;
-}
-
-.ui.large.attached.header {
-  font-size: 1.14285714em;
-}
-
-.ui.big.attached.header {
-  font-size: 1.28571429em;
-}
-
-.ui.huge.attached.header {
-  font-size: 1.42857143em;
-}
-
-.ui.massive.attached.header {
-  font-size: 1.71428571em;
-}
-
-/*-------------------
-        Sizing
---------------------*/
-
-.ui.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) {
-  font-size: 1.28571429em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 367bdf3642..876996a4b4 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -28,7 +28,6 @@
     "dimmer",
     "dropdown",
     "form",
-    "header",
     "input",
     "label",
     "list",

From 5bd0773741be8f9476be0c76e8733f8c9dd65d19 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 14:38:10 +0100
Subject: [PATCH 500/679] Dont show expansion for empty actions steps (#29977)

This hides the chevron icon and makes the step header unclickable for
skipped steps because there is no content to expand on those.

Before:

<img width="272" alt="Screenshot 2024-03-21 at 20 06 47"
src="https://github.com/go-gitea/gitea/assets/115237/9bb328d1-6f74-48a9-af19-de9b351e3707">

After:
<img width="295" alt="Screenshot 2024-03-21 at 20 03 07"
src="https://github.com/go-gitea/gitea/assets/115237/72a26e14-5a28-4606-8c3c-184b405872c8">

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/components/RepoActionView.vue | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 2a4a6d77ff..83933ef24d 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -268,6 +268,10 @@ const sfc = {
       return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
     },
 
+    isExpandable(status) {
+      return ['success', 'running', 'failure', 'cancelled'].includes(status);
+    },
+
     closeDropdown() {
       if (this.menuVisible) this.menuVisible = false;
     },
@@ -459,12 +463,12 @@ export function initRepositoryActionView() {
         </div>
         <div class="job-step-container" ref="steps" v-if="currentJob.steps.length">
           <div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
-            <div class="job-step-summary" @click.stop="toggleStepLogs(i)" :class="currentJobStepsStates[i].expanded ? 'selected' : ''">
+            <div class="job-step-summary" @click.stop="jobStep.status !== 'skipped' && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
               <!-- If the job is done and the job step log is loaded for the first time, show the loading icon
                 currentJobStepsStates[i].cursor === null means the log is loaded for the first time
               -->
               <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
-              <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" class="gt-mr-3"/>
+              <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['gt-mr-3', !isExpandable(jobStep.status) && 'tw-invisible']"/>
               <ActionRunStatus :status="jobStep.status" class="gt-mr-3"/>
 
               <span class="step-summary-msg gt-ellipsis">{{ jobStep.summary }}</span>
@@ -715,13 +719,21 @@ export function initRepositoryActionView() {
 }
 
 .job-step-container .job-step-summary {
-  cursor: pointer;
   padding: 5px 10px;
   display: flex;
   align-items: center;
   border-radius: var(--border-radius);
 }
 
+.job-step-container .job-step-summary.step-expandable {
+  cursor: pointer;
+}
+
+.job-step-container .job-step-summary.step-expandable:hover {
+  color: var(--color-console-fg);
+  background-color: var(--color-console-hover-bg);
+}
+
 .job-step-container .job-step-summary .step-summary-msg {
   flex: 1;
 }
@@ -730,12 +742,6 @@ export function initRepositoryActionView() {
   margin-left: 16px;
 }
 
-.job-step-container .job-step-summary:hover {
-  color: var(--color-console-fg);
-  background-color: var(--color-console-hover-bg);
-
-}
-
 .job-step-container .job-step-summary.selected {
   color: var(--color-console-fg);
   background-color: var(--color-console-active-bg);

From 2d281704de8c5b67592dd7f9362620927230ef2b Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 15:04:18 +0100
Subject: [PATCH 501/679] Remove fomantic container module (#30036)

Small CSS module. There was a ordering conflict between `.ui.menu` and
`.ui.container` which I've solved by adding the `.ui.menu` rule into
base.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/base.css                |  35 +------
 web_src/css/index.css               |   1 +
 web_src/css/modules/container.css   |  78 ++++++++++++++
 web_src/fomantic/build/semantic.css | 157 ----------------------------
 web_src/fomantic/semantic.json      |   1 -
 5 files changed, 83 insertions(+), 189 deletions(-)
 create mode 100644 web_src/css/modules/container.css

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 226ec70386..51a4852e7e 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -352,6 +352,10 @@ ol.ui.list li,
   border-right-color: var(--color-primary);
 }
 
+.ui.menu {
+  display: flex;
+}
+
 .ui.menu,
 .ui.vertical.menu {
   background: var(--color-menu);
@@ -738,37 +742,6 @@ img.ui.avatar,
   padding-top: 14px;
 }
 
-/* overwrite semantic width of containers inside the main page content div (div with class "page-content") */
-.page-content .ui.ui.ui.container:not(.fluid) {
-  width: 1280px;
-  max-width: calc(100% - 64px);
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.ui.container.fluid.padded {
-  padding: 0 32px;
-}
-
-/* enable fluid page widths for medium size viewports */
-@media (min-width: 768px) and (max-width: 1200px) {
-  .page-content .ui.ui.ui.container:not(.fluid) {
-    max-width: calc(100% - 32px);
-  }
-  .ui.container.fluid.padded {
-    padding: 0 16px;
-  }
-}
-
-@media (max-width: 767.98px) {
-  .page-content .ui.ui.ui.container:not(.fluid) {
-    max-width: calc(100% - 16px);
-  }
-  .ui.container.fluid.padded {
-    padding: 0 8px;
-  }
-}
-
 .ui.pagination.menu .active.item {
   color: var(--color-text);
   background: var(--color-active);
diff --git a/web_src/css/index.css b/web_src/css/index.css
index e8df321829..7ba19e62d4 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -14,6 +14,7 @@
 @import "./modules/svg.css";
 @import "./modules/flexcontainer.css";
 @import "./modules/message.css";
+@import "./modules/container.css";
 @import "./modules/header.css";
 
 @import "./shared/flex-list.css";
diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css
new file mode 100644
index 0000000000..dc854f89d0
--- /dev/null
+++ b/web_src/css/modules/container.css
@@ -0,0 +1,78 @@
+/* based on Fomantic UI container module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.container {
+  display: block;
+  max-width: 100%;
+}
+
+@media (max-width: 767.98px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: auto;
+    margin-left: 1em;
+    margin-right: 1em;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991.98px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: 723px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199.98px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: 933px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+@media (min-width: 1200px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: 1127px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+.ui.fluid.container {
+  width: 100%;
+}
+
+.ui[class*="center aligned"].container {
+  text-align: center;
+}
+
+/* overwrite width of containers inside the main page content div (div with class "page-content") */
+.page-content .ui.ui.ui.container:not(.fluid) {
+  width: 1280px;
+  max-width: calc(100% - 64px);
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.ui.container.fluid.padded {
+  padding: 0 32px;
+}
+
+/* enable fluid page widths for medium size viewports */
+@media (min-width: 768px) and (max-width: 1200px) {
+  .page-content .ui.ui.ui.container:not(.fluid) {
+    max-width: calc(100% - 32px);
+  }
+  .ui.container.fluid.padded {
+    padding: 0 16px;
+  }
+}
+
+@media (max-width: 767.98px) {
+  .page-content .ui.ui.ui.container:not(.fluid) {
+    max-width: calc(100% - 16px);
+  }
+  .ui.container.fluid.padded {
+    padding: 0 8px;
+  }
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 4d82244277..d9abf343b8 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -3032,163 +3032,6 @@
 .plus:before { content: '\e802'; }
 */
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Container
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Container
-*******************************/
-
-/* All Sizes */
-
-.ui.container {
-  display: block;
-  max-width: 100%;
-}
-
-/* Mobile */
-
-@media only screen and (max-width: 767.98px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: auto;
-    margin-left: 1em;
-    margin-right: 1em;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: auto;
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: auto;
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: auto;
-  }
-}
-
-/* Tablet */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: 723px;
-    margin-left: auto;
-    margin-right: auto;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: calc(723px + 2rem);
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: calc(723px + 3rem);
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: calc(723px + 5rem);
-  }
-}
-
-/* Small Monitor */
-
-@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: 933px;
-    margin-left: auto;
-    margin-right: auto;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: calc(933px + 2rem);
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: calc(933px + 3rem);
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: calc(933px + 5rem);
-  }
-}
-
-/* Large Monitor */
-
-@media only screen and (min-width: 1200px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: 1127px;
-    margin-left: auto;
-    margin-right: auto;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: calc(1127px + 2rem);
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: calc(1127px + 3rem);
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: calc(1127px + 5rem);
-  }
-}
-
-/*******************************
-             Types
-*******************************/
-
-/* Text Container */
-
-.ui.text.container {
-  font-family: var(--fonts-regular);
-  max-width: 700px;
-  line-height: 1.5;
-  font-size: 1.14285714rem;
-}
-
-/* Fluid */
-
-.ui.fluid.container {
-  width: 100%;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-.ui[class*="left aligned"].container {
-  text-align: left;
-}
-
-.ui[class*="center aligned"].container {
-  text-align: center;
-}
-
-.ui[class*="right aligned"].container {
-  text-align: right;
-}
-
-.ui.justified.container {
-  text-align: justify;
-  -webkit-hyphens: auto;
-  hyphens: auto;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 876996a4b4..9c7cb54cb7 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -24,7 +24,6 @@
     "api",
     "button",
     "checkbox",
-    "container",
     "dimmer",
     "dropdown",
     "form",

From 90a4f9a49eecc4b672df0c29f5034be25244191c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 15:31:35 +0100
Subject: [PATCH 502/679] Migrate `gap` helpers to tailwind (#30034)

Commands ran:

```sh
perl -p -i -e 's#gt-gap-0#tw-gap-0#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-1#tw-gap-0.5#g' web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-2#tw-gap-1#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-3#tw-gap-2#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-4#tw-gap-4#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-5#tw-gap-8#g'   web_src/js/**/* templates/**/*

perl -p -i -e 's#gt-gap-x-0#tw-gap-x-0#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-x-1#tw-gap-x-0.5#g' web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-x-2#tw-gap-x-1#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-x-3#tw-gap-x-2#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-x-4#tw-gap-x-4#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-x-5#tw-gap-x-8#g'   web_src/js/**/* templates/**/*

perl -p -i -e 's#gt-gap-y-0#tw-gap-y-0#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-y-1#tw-gap-y-0.5#g' web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-y-2#tw-gap-y-1#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-y-3#tw-gap-y-2#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-y-4#tw-gap-y-4#g'   web_src/js/**/* templates/**/*
perl -p -i -e 's#gt-gap-y-5#tw-gap-y-8#g'   web_src/js/**/* templates/**/*
---
 templates/admin/user/list.tmpl                |  2 +-
 templates/org/header.tmpl                     |  2 +-
 templates/org/team/members.tmpl               |  2 +-
 templates/org/team/repositories.tmpl          |  2 +-
 templates/repo/diff/box.tmpl                  |  6 +++---
 templates/repo/diff/conversation.tmpl         |  4 ++--
 templates/repo/home.tmpl                      |  4 ++--
 templates/repo/issue/milestone_issues.tmpl    |  4 ++--
 .../repo/issue/view_content/sidebar.tmpl      |  6 +++---
 templates/repo/pulls/tab_menu.tmpl            |  2 +-
 templates/user/auth/signin_inner.tmpl         |  2 +-
 templates/user/auth/signup_inner.tmpl         |  2 +-
 templates/user/auth/webauthn.tmpl             |  2 +-
 templates/user/dashboard/feeds.tmpl           |  4 ++--
 .../user/notification/notification_div.tmpl   |  4 ++--
 templates/user/settings/account.tmpl          |  2 +-
 web_src/css/helpers.css                       | 21 -------------------
 web_src/js/components/DiffCommitSelector.vue  |  2 +-
 web_src/js/components/RepoContributors.vue    |  2 +-
 19 files changed, 27 insertions(+), 48 deletions(-)

diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 427eef7a78..528d047507 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -103,7 +103,7 @@
 								<td><span>{{ctx.Locale.Tr "admin.users.never_login"}}</span></td>
 							{{end}}
 							<td>
-								<div class="tw-flex gt-gap-3">
+								<div class="tw-flex tw-gap-2">
 									<a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.users.details"}}">{{svg "octicon-person"}}</a>
 									<a href="{{$.Link}}/{{.ID}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a>
 								</div>
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 81373aa75c..6eb7feb33f 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -7,7 +7,7 @@
 				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
 				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
 			</span>
-			<span class="tw-flex tw-items-center gt-gap-2 tw-ml-auto tw-text-16 tw-whitespace-nowrap">
+			<span class="tw-flex tw-items-center tw-gap-1 tw-ml-auto tw-text-16 tw-whitespace-nowrap">
 				{{if .EnableFeed}}
 					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
 						{{svg "octicon-rss" 24}}
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index 65430cbda3..aa358841da 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -9,7 +9,7 @@
 				{{template "org/team/navbar" .}}
 				{{if .IsOrganizationOwner}}
 					<div class="ui attached segment">
-						<form class="ui form ignore-dirty tw-flex tw-flex-wrap gt-gap-3" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post">
+						<form class="ui form ignore-dirty tw-flex tw-flex-wrap tw-gap-2" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post">
 							{{.CsrfTokenHtml}}
 							<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
 							<div id="search-user-box" class="ui search gt-mr-3"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl
index 0c59eafbe1..202279240b 100644
--- a/templates/org/team/repositories.tmpl
+++ b/templates/org/team/repositories.tmpl
@@ -9,7 +9,7 @@
 				{{template "org/team/navbar" .}}
 				{{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}}
 				{{if $canAddRemove}}
-					<div class="ui attached segment tw-flex tw-flex-wrap gt-gap-3">
+					<div class="ui attached segment tw-flex tw-flex-wrap tw-gap-2">
 						<form class="ui form ignore-dirty tw-flex-1 tw-flex" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post">
 							{{.CsrfTokenHtml}}
 							<div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search">
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 51c0aae007..9fd8593c53 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -1,7 +1,7 @@
 {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
 <div>
 	<div class="diff-detail-box diff-box">
-		<div class="tw-flex tw-items-center tw-flex-wrap gt-gap-3 gt-ml-1">
+		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 gt-ml-1">
 			{{if $showFileTree}}
 				<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
 					{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
@@ -111,7 +111,7 @@
 					{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
 					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
 						<h4 class="diff-file-header sticky-2nd-row ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between tw-flex-wrap">
-							<div class="diff-file-name tw-flex tw-items-center gt-gap-2 tw-flex-wrap">
+							<div class="diff-file-name tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
 								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
 										{{svg "octicon-chevron-right" 18}}
@@ -144,7 +144,7 @@
 									<span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
 								{{end}}
 							</div>
-							<div class="diff-file-header-actions tw-flex tw-items-center gt-gap-2 tw-flex-wrap">
+							<div class="diff-file-header-actions tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
 								{{if $showFileViewToggle}}
 									<div class="ui compact icon buttons">
 										<button class="ui tiny basic button file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code"}}</button>
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index 85bfe4923a..e689deb1bf 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -9,7 +9,7 @@
 	<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
 		{{if $resolved}}
 			<div class="ui attached header resolved-placeholder tw-flex tw-items-center tw-justify-between">
-				<div class="ui grey text tw-flex tw-items-center tw-flex-wrap gt-gap-2">
+				<div class="ui grey text tw-flex tw-items-center tw-flex-wrap tw-gap-1">
 					{{svg "octicon-check" 16 "icon gt-mr-2"}}
 					<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
 					{{if $invalid}}
@@ -22,7 +22,7 @@
 						</a>
 					{{end}}
 				</div>
-				<div class="tw-flex tw-items-center gt-gap-3">
+				<div class="tw-flex tw-items-center tw-gap-2">
 					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-items-center">
 						{{svg "octicon-unfold" 16 "gt-mr-3"}}
 						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 40732db94a..7b70f70bee 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -27,7 +27,7 @@
 				</div>
 			{{end}}
 		</div>
-		<div class="tw-flex tw-items-center tw-flex-wrap gt-gap-2" id="repo-topics">
+		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics">
 			{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
 			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
 		</div>
@@ -61,7 +61,7 @@
 		{{end}}
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
-			<div class="tw-flex tw-items-center tw-flex-wrap gt-gap-y-3">
+			<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-y-2">
 				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
 				{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 					{{$cmpBranch := ""}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index 2028375c03..8ba7eecf4d 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -26,9 +26,9 @@
 				{{.Milestone.RenderedContent}}
 		</div>
 		{{end}}
-		<div class="tw-flex tw-flex-col gt-gap-3">
+		<div class="tw-flex tw-flex-col tw-gap-2">
 			<progress class="milestone-progress-big" value="{{.Milestone.Completeness}}" max="100"></progress>
-			<div class="tw-flex gt-gap-4">
+			<div class="tw-flex tw-gap-4">
 				<div classs="tw-flex tw-items-center">
 					{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}}
 					{{if .IsClosed}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 833b5aa92f..99ea699f4a 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -58,7 +58,7 @@
 								<span class="text">{{svg "octicon-people" 20 "gt-mr-3"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
 							{{end}}
 						</div>
-						<div class="tw-flex tw-items-center gt-gap-3">
+						<div class="tw-flex tw-items-center tw-gap-2">
 							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
 								<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
 									{{svg "octicon-x" 20}}
@@ -106,7 +106,7 @@
 								{{.OriginalAuthor}}
 							</a>
 						</div>
-						<div class="tw-flex tw-items-center gt-gap-3">
+						<div class="tw-flex tw-items-center tw-gap-2">
 							{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
 						</div>
 					</div>
@@ -310,7 +310,7 @@
 						<div class="ui mini modal issue-start-time-modal">
 							<div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div>
 							<div class="content">
-								<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid gt-gap-3">
+								<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2">
 									{{$.CsrfTokenHtml}}
 									<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
 									<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index 7f4c460b8e..0ddb17a934 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -15,7 +15,7 @@
 			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
-		<span class="item tw-ml-auto gt-pr-0 tw-font-bold tw-flex tw-items-center gt-gap-3">
+		<span class="item tw-ml-auto gt-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2">
 			<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
 			<span class="diff-stats-bar">
 				<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 1950f750ce..ad86b0b881 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -55,7 +55,7 @@
 	</div>
 	<div id="oauth2-login-navigator" class="gt-py-2">
 		<div class="tw-flex tw-flex-col tw-justify-center">
-			<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center gt-gap-3">
+			<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
 				{{range $provider := .OAuth2Providers}}
 					<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index b96e9bfb02..26d9091b68 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -60,7 +60,7 @@
 			</div>
 			<div id="oauth2-login-navigator" class="gt-py-2">
 				<div class="tw-flex tw-flex-col tw-justify-center">
-					<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center gt-gap-3">
+					<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
 						{{range $provider := .OAuth2Providers}}
 							<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl
index 867248718d..24dd75eed5 100644
--- a/templates/user/auth/webauthn.tmpl
+++ b/templates/user/auth/webauthn.tmpl
@@ -10,7 +10,7 @@
 				{{template "base/alert" .}}
 				<p>{{ctx.Locale.Tr "webauthn_sign_in"}}</p>
 			</div>
-			<div class="ui attached segment tw-flex tw-items-center tw-justify-center gt-gap-2 gt-py-3">
+			<div class="ui attached segment tw-flex tw-items-center tw-justify-center tw-gap-1 gt-py-3">
 				<div class="is-loading tw-w-[40px] tw-h-[40px]"></div>
 				{{ctx.Locale.Tr "webauthn_press_button"}}
 			</div>
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index c58d7e22d7..fbe151607c 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -4,7 +4,7 @@
 			<div class="flex-item-leading">
 				{{ctx.AvatarUtils.AvatarByAction .}}
 			</div>
-			<div class="flex-item-main gt-gap-3">
+			<div class="flex-item-main tw-gap-2">
 				<div>
 					{{if gt .ActUser.ID 0}}
 						<a href="{{AppSubUrl}}/{{(.GetActUserName ctx) | PathEscape}}" title="{{.GetActDisplayNameTitle ctx}}">{{.GetActDisplayName ctx}}</a>
@@ -84,7 +84,7 @@
 					{{$push := ActionContent2Commits .}}
 					{{$repoLink := (.GetRepoLink ctx)}}
 					{{$repo := .Repo}}
-					<div class="tw-flex tw-flex-col gt-gap-2">
+					<div class="tw-flex tw-flex-col tw-gap-1">
 						{{range $push.Commits}}
 							{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
 							<div class="flex-text-block">
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index da5a920fd1..f0fe6ac6f4 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -35,7 +35,7 @@
 					</div>
 				{{else}}
 					{{range $notification := .Notifications}}
-						<div class="notifications-item tw-flex tw-items-center tw-flex-wrap gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
+						<div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
 							<div class="notifications-icon gt-ml-3 gt-mr-2 tw-self-start gt-mt-2">
 								{{if .Issue}}
 									{{template "shared/issueicon" .Issue}}
@@ -67,7 +67,7 @@
 									{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
 								{{end}}
 							</div>
-							<div class="notifications-buttons tw-items-center tw-justify-end gt-gap-2 gt-px-2">
+							<div class="notifications-buttons tw-items-center tw-justify-end tw-gap-1 gt-px-2">
 								{{if ne .Status 3}}
 									<form action="{{AppSubUrl}}/notifications/status" method="post">
 										{{$.CsrfTokenHtml}}
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index fb46dfbd2d..6645963984 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -46,7 +46,7 @@
 					<form action="{{AppSubUrl}}/user/settings/account/email" class="ui form" method="post">
 						{{$.CsrfTokenHtml}}
 						<input name="_method" type="hidden" value="NOTIFICATION">
-						<div class="tw-flex tw-flex-wrap gt-gap-3">
+						<div class="tw-flex tw-flex-wrap tw-gap-2">
 							<div class="ui selection dropdown">
 								<input name="preference" type="hidden" value="{{.EmailNotificationsPreference}}">
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 2d6bd576ae..c9c051faf4 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -142,27 +142,6 @@ Gitea's private styles use `g-` prefix.
 .gt-py-4 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
 .gt-py-5 { padding-top: 2rem !important; padding-bottom: 2rem !important; }
 
-.gt-gap-0 { gap: 0 !important; }
-.gt-gap-1 { gap: .125rem !important; }
-.gt-gap-2 { gap: .25rem !important; }
-.gt-gap-3 { gap: .5rem !important; }
-.gt-gap-4 { gap: 1rem !important; }
-.gt-gap-5 { gap: 2rem !important; }
-
-.gt-gap-x-0 { column-gap: 0 !important; }
-.gt-gap-x-1 { column-gap: .125rem !important; }
-.gt-gap-x-2 { column-gap: .25rem !important; }
-.gt-gap-x-3 { column-gap: .5rem !important; }
-.gt-gap-x-4 { column-gap: 1rem !important; }
-.gt-gap-x-5 { column-gap: 2rem !important; }
-
-.gt-gap-y-0 { row-gap: 0 !important; }
-.gt-gap-y-1 { row-gap: .125rem !important; }
-.gt-gap-y-2 { row-gap: .25rem !important; }
-.gt-gap-y-3 { row-gap: .5rem !important; }
-.gt-gap-y-4 { row-gap: 1rem !important; }
-.gt-gap-y-5 { row-gap: 2rem !important; }
-
 /*
 gt-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element.
 do not use:
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index df712a2cb4..35245f2190 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -240,7 +240,7 @@ export default {
           @click.meta.exact="commitClicked(commit.id, true)"
           @click.shift.exact.stop.prevent="commitClickedShift(commit)"
         >
-          <div class="tw-flex-1 tw-flex tw-flex-col gt-gap-2">
+          <div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1">
             <div class="gt-ellipsis commit-list-summary">
               {{ commit.summary }}
             </div>
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 6de68a4aec..02db9e3e3e 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -384,7 +384,7 @@ export default {
             <h4 v-else class="contributor-name">
               {{ contributor.name }}
             </h4>
-            <p class="tw-text-12 tw-flex gt-gap-2">
+            <p class="tw-text-12 tw-flex tw-gap-1">
               <strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong>
               <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
               <strong v-if="contributor.total_deletions" class="text red">

From 4734d43e1422da04f9ff79ea0212f7e9472b55a1 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 25 Mar 2024 00:05:00 +0800
Subject: [PATCH 503/679] Support repo code search without setting up an
 indexer (#29998)

By using git's ability, end users (especially small instance users) do
not need to enable the indexer, they could also benefit from the code
searching feature.

Fix #29996


![image](https://github.com/go-gitea/gitea/assets/2114189/11b7e458-88a4-480d-b4d7-72ee59406dd1)


![image](https://github.com/go-gitea/gitea/assets/2114189/0fe777d5-c95c-4288-a818-0427680805b6)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 .../administration/repo-indexer.en-us.md      |   6 +
 docs/content/installation/comparison.en-us.md |   3 +
 modules/git/command.go                        |   5 +-
 modules/git/git.go                            |   8 +-
 modules/git/grep.go                           | 112 ++++++++++++++++++
 modules/git/grep_test.go                      |  41 +++++++
 modules/indexer/code/search.go                |  35 +++---
 options/locale/locale_en-US.ini               |   1 +
 routers/web/repo/search.go                    |  67 +++++++----
 templates/repo/home.tmpl                      |  23 ++--
 templates/shared/search/code/search.tmpl      |  15 ++-
 templates/shared/searchbottom.tmpl            |   2 +
 12 files changed, 253 insertions(+), 65 deletions(-)
 create mode 100644 modules/git/grep.go
 create mode 100644 modules/git/grep_test.go

diff --git a/docs/content/administration/repo-indexer.en-us.md b/docs/content/administration/repo-indexer.en-us.md
index 6dec2d63fa..aa82222911 100644
--- a/docs/content/administration/repo-indexer.en-us.md
+++ b/docs/content/administration/repo-indexer.en-us.md
@@ -17,6 +17,12 @@ menu:
 
 # Repository indexer
 
+## Builtin repository code search without indexer
+
+Users could do repository-level code search without setting up a repository indexer.
+The builtin code search is based on the `git grep` command, which is fast and efficient for small repositories.
+Better code search support could be achieved by setting up the repository indexer.
+
 ## Setting up the repository indexer
 
 Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md):
diff --git a/docs/content/installation/comparison.en-us.md b/docs/content/installation/comparison.en-us.md
index 1ba4f7ecc2..3fb6561f31 100644
--- a/docs/content/installation/comparison.en-us.md
+++ b/docs/content/installation/comparison.en-us.md
@@ -87,6 +87,9 @@ _Symbols used in table:_
 | Git Blame                                   | ✓                                                   | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | ✓            |
 | Visual comparison of image changes          | ✓                                                   | ✘    | ✓         | ?         | ?         | ?         | ✘            | ✘            |
 
+- Gitea has builtin repository-level code search
+- Better code search support could be achieved by [using a repository indexer](administration/repo-indexer.md)
+
 ## Issue Tracker
 
 | Feature                       | Gitea                                               | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE |
diff --git a/modules/git/command.go b/modules/git/command.go
index 371109730a..22cb275ab2 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -367,7 +367,6 @@ type RunStdError interface {
 	error
 	Unwrap() error
 	Stderr() string
-	IsExitCode(code int) bool
 }
 
 type runStdError struct {
@@ -392,9 +391,9 @@ func (r *runStdError) Stderr() string {
 	return r.stderr
 }
 
-func (r *runStdError) IsExitCode(code int) bool {
+func IsErrorExitCode(err error, code int) bool {
 	var exitError *exec.ExitError
-	if errors.As(r.err, &exitError) {
+	if errors.As(err, &exitError) {
 		return exitError.ExitCode() == code
 	}
 	return false
diff --git a/modules/git/git.go b/modules/git/git.go
index f688ea7488..e411269f7c 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -341,7 +341,7 @@ func checkGitVersionCompatibility(gitVer *version.Version) error {
 
 func configSet(key, value string) error {
 	stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
-	if err != nil && !err.IsExitCode(1) {
+	if err != nil && !IsErrorExitCode(err, 1) {
 		return fmt.Errorf("failed to get git config %s, err: %w", key, err)
 	}
 
@@ -364,7 +364,7 @@ func configSetNonExist(key, value string) error {
 		// already exist
 		return nil
 	}
-	if err.IsExitCode(1) {
+	if IsErrorExitCode(err, 1) {
 		// not exist, set new config
 		_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
 		if err != nil {
@@ -382,7 +382,7 @@ func configAddNonExist(key, value string) error {
 		// already exist
 		return nil
 	}
-	if err.IsExitCode(1) {
+	if IsErrorExitCode(err, 1) {
 		// not exist, add new config
 		_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
 		if err != nil {
@@ -403,7 +403,7 @@ func configUnsetAll(key, value string) error {
 		}
 		return nil
 	}
-	if err.IsExitCode(1) {
+	if IsErrorExitCode(err, 1) {
 		// not exist
 		return nil
 	}
diff --git a/modules/git/grep.go b/modules/git/grep.go
new file mode 100644
index 0000000000..e533995984
--- /dev/null
+++ b/modules/git/grep.go
@@ -0,0 +1,112 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/util"
+)
+
+type GrepResult struct {
+	Filename    string
+	LineNumbers []int
+	LineCodes   []string
+}
+
+type GrepOptions struct {
+	RefName           string
+	ContextLineNumber int
+	IsFuzzy           bool
+}
+
+func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
+	stdoutReader, stdoutWriter, err := os.Pipe()
+	if err != nil {
+		return nil, fmt.Errorf("unable to create os pipe to grep: %w", err)
+	}
+	defer func() {
+		_ = stdoutReader.Close()
+		_ = stdoutWriter.Close()
+	}()
+
+	/*
+	 The output is like this ( "^@" means \x00):
+
+	 HEAD:.air.toml
+	 6^@bin = "gitea"
+
+	 HEAD:.changelog.yml
+	 2^@repo: go-gitea/gitea
+	*/
+	var results []*GrepResult
+	cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name")
+	cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber))
+	if opts.IsFuzzy {
+		words := strings.Fields(search)
+		for _, word := range words {
+			cmd.AddOptionValues("-e", strings.TrimLeft(word, "-"))
+		}
+	} else {
+		cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
+	}
+	cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
+	stderr := bytes.Buffer{}
+	err = cmd.Run(&RunOpts{
+		Dir:    repo.Path,
+		Stdout: stdoutWriter,
+		Stderr: &stderr,
+		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+			_ = stdoutWriter.Close()
+			defer stdoutReader.Close()
+
+			isInBlock := false
+			scanner := bufio.NewScanner(stdoutReader)
+			var res *GrepResult
+			for scanner.Scan() {
+				line := scanner.Text()
+				if !isInBlock {
+					if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok {
+						isInBlock = true
+						res = &GrepResult{Filename: filename}
+						results = append(results, res)
+					}
+					continue
+				}
+				if line == "" {
+					if len(results) >= 50 {
+						cancel()
+						break
+					}
+					isInBlock = false
+					continue
+				}
+				if line == "--" {
+					continue
+				}
+				if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok {
+					lineNumInt, _ := strconv.Atoi(lineNum)
+					res.LineNumbers = append(res.LineNumbers, lineNumInt)
+					res.LineCodes = append(res.LineCodes, lineCode)
+				}
+			}
+			return scanner.Err()
+		},
+	})
+	// git grep exits with 1 if no results are found
+	if IsErrorExitCode(err, 1) && stderr.Len() == 0 {
+		return nil, nil
+	}
+	if err != nil && !errors.Is(err, context.Canceled) {
+		return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String())
+	}
+	return results, nil
+}
diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go
new file mode 100644
index 0000000000..3993fa7ffc
--- /dev/null
+++ b/modules/git/grep_test.go
@@ -0,0 +1,41 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"context"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGrepSearch(t *testing.T) {
+	repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "language_stats_repo"))
+	assert.NoError(t, err)
+	defer repo.Close()
+
+	res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{})
+	assert.NoError(t, err)
+	assert.Equal(t, []*GrepResult{
+		{
+			Filename:    "java-hello/main.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] args)"},
+		},
+		{
+			Filename:    "main.vendor.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] args)"},
+		},
+	}, res)
+
+	res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
+	assert.NoError(t, err)
+	assert.Len(t, res, 0)
+
+	res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{})
+	assert.Error(t, err)
+	assert.Len(t, res, 0)
+}
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 51c7595cf8..5f35e8073b 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -70,13 +70,27 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
 	return nil
 }
 
+func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine {
+	// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
+	hl, _ := highlight.Code(filename, "", code)
+	highlightedLines := strings.Split(string(hl), "\n")
+
+	// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
+	lines := make([]ResultLine, min(len(highlightedLines), len(lineNums)))
+	for i := 0; i < len(lines); i++ {
+		lines[i].Num = lineNums[i]
+		lines[i].FormattedContent = template.HTML(highlightedLines[i])
+	}
+	return lines
+}
+
 func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) {
 	startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n")
 
 	var formattedLinesBuffer bytes.Buffer
 
 	contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n")
-	lines := make([]ResultLine, 0, len(contentLines))
+	lineNums := make([]int, 0, len(contentLines))
 	index := startIndex
 	for i, line := range contentLines {
 		var err error
@@ -91,29 +105,16 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 				line[closeActiveIndex:],
 			)
 		} else {
-			err = writeStrings(&formattedLinesBuffer,
-				line,
-			)
+			err = writeStrings(&formattedLinesBuffer, line)
 		}
 		if err != nil {
 			return nil, err
 		}
 
-		lines = append(lines, ResultLine{Num: startLineNum + i})
+		lineNums = append(lineNums, startLineNum+i)
 		index += len(line)
 	}
 
-	// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
-	hl, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
-	highlightedLines := strings.Split(string(hl), "\n")
-
-	// The lines outputted by highlight.Code might not match the original lines, because "highlight" removes the last `\n`
-	lines = lines[:min(len(highlightedLines), len(lines))]
-	highlightedLines = highlightedLines[:len(lines)]
-	for i := 0; i < len(lines); i++ {
-		lines[i].FormattedContent = template.HTML(highlightedLines[i])
-	}
-
 	return &Result{
 		RepoID:      result.RepoID,
 		Filename:    result.Filename,
@@ -121,7 +122,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 		UpdatedUnix: result.UpdatedUnix,
 		Language:    result.Language,
 		Color:       result.Color,
-		Lines:       lines,
+		Lines:       HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()),
 	}, nil
 }
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 4c52c4eeed..07082f99ae 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -172,6 +172,7 @@ org_kind = Search orgs...
 team_kind = Search teams...
 code_kind = Search code...
 code_search_unavailable = Code search is currently not available. Please contact the site administrator.
+code_search_by_git_grep = Current code search results are provided by "git grep". There might be better results if site administrator enables Repository Indexer.
 package_kind = Search packages...
 project_kind = Search projects...
 branch_kind = Search branches...
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 0f377a97bb..9d65427b8f 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -5,9 +5,11 @@ package repo
 
 import (
 	"net/http"
+	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/git"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/services/context"
@@ -17,11 +19,6 @@ const tplSearch base.TplName = "repo/search"
 
 // Search render repository search page
 func Search(ctx *context.Context) {
-	if !setting.Indexer.RepoIndexerEnabled {
-		ctx.Redirect(ctx.Repo.RepoLink)
-		return
-	}
-
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
@@ -42,26 +39,54 @@ func Search(ctx *context.Context) {
 		page = 1
 	}
 
-	total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
-		RepoIDs:        []int64{ctx.Repo.Repository.ID},
-		Keyword:        keyword,
-		IsKeywordFuzzy: isFuzzy,
-		Language:       language,
-		Paginator: &db.ListOptions{
-			Page:     page,
-			PageSize: setting.UI.RepoSearchPagingNum,
-		},
-	})
-	if err != nil {
-		if code_indexer.IsAvailable(ctx) {
-			ctx.ServerError("SearchResults", err)
+	var total int
+	var searchResults []*code_indexer.Result
+	var searchResultLanguages []*code_indexer.SearchResultLanguages
+	if setting.Indexer.RepoIndexerEnabled {
+		var err error
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+			RepoIDs:        []int64{ctx.Repo.Repository.ID},
+			Keyword:        keyword,
+			IsKeywordFuzzy: isFuzzy,
+			Language:       language,
+			Paginator: &db.ListOptions{
+				Page:     page,
+				PageSize: setting.UI.RepoSearchPagingNum,
+			},
+		})
+		if err != nil {
+			if code_indexer.IsAvailable(ctx) {
+				ctx.ServerError("SearchResults", err)
+				return
+			}
+			ctx.Data["CodeIndexerUnavailable"] = true
+		} else {
+			ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
+		}
+	} else {
+		res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy})
+		if err != nil {
+			ctx.ServerError("GrepSearch", err)
 			return
 		}
-		ctx.Data["CodeIndexerUnavailable"] = true
-	} else {
-		ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
+		total = len(res)
+		pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res))
+		pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res))
+		res = res[pageStart:pageEnd]
+		for _, r := range res {
+			searchResults = append(searchResults, &code_indexer.Result{
+				RepoID:   ctx.Repo.Repository.ID,
+				Filename: r.Filename,
+				CommitID: ctx.Repo.CommitID,
+				// UpdatedUnix: not supported yet
+				// Language:    not supported yet
+				// Color:       not supported yet
+				Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, strings.Join(r.LineCodes, "\n")),
+			})
+		}
 	}
 
+	ctx.Data["CodeIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 	ctx.Data["Repo"] = ctx.Repo.Repository
 	ctx.Data["SearchResults"] = searchResults
 	ctx.Data["SearchResultLanguages"] = searchResultLanguages
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 7b70f70bee..2463c768fd 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -5,27 +5,18 @@
 		{{template "base/alert" .}}
 		{{template "repo/code/recently_pushed_new_branches" .}}
 		{{if and (not .HideRepoInfo) (not .IsBlame)}}
-		<div class="ui repo-description gt-word-break">
-			<div id="repo-desc" class="tw-text-16">
+		<div class="repo-description">
+			<div id="repo-desc" class="gt-word-break tw-text-16">
 				{{$description := .Repository.DescriptionHTML $.Context}}
 				{{if $description}}<span class="description">{{$description | RenderCodeBlock}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{ctx.Locale.Tr "repo.no_desc"}}</span>{{end}}
 				<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
 			</div>
-			{{if .RepoSearchEnabled}}
-				<div class="ui repo-search">
-					<form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get">
-						<div class="field">
-							<div class="ui small action input{{if .CodeIndexerUnavailable}} disabled left icon{{end}}"{{if .CodeIndexerUnavailable}} data-tooltip-content="{{ctx.Locale.Tr "search.code_search_unavailable"}}"{{end}}>
-								<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
-								{{if .CodeIndexerUnavailable}}
-									<i class="icon">{{svg "octicon-alert"}}</i>
-								{{end}}
-								{{template "shared/search/button" dict "Disabled" .CodeIndexerUnavailable}}
-							</div>
-						</div>
-					</form>
+			<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
+				<div class="ui small action input">
+					<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
+					{{template "shared/search/button"}}
 				</div>
-			{{end}}
+			</form>
 		</div>
 		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics">
 			{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl
index 545ec1ea65..cb873f5a92 100644
--- a/templates/shared/search/code/search.tmpl
+++ b/templates/shared/search/code/search.tmpl
@@ -7,9 +7,16 @@
 		<div class="ui error message">
 			<p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p>
 		</div>
-	{{else if .SearchResults}}
-		{{template "shared/search/code/results" .}}
-	{{else if .Keyword}}
-		<div>{{ctx.Locale.Tr "search.no_results"}}</div>
+	{{else}}
+		{{if not .CodeIndexerEnabled}}
+			<div class="ui message">
+				<p>{{ctx.Locale.Tr "search.code_search_by_git_grep"}}</p>
+			</div>
+		{{end}}
+		{{if .SearchResults}}
+			{{template "shared/search/code/results" .}}
+		{{else if .Keyword}}
+			<div>{{ctx.Locale.Tr "search.no_results"}}</div>
+		{{end}}
 	{{end}}
 </div>
diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl
index 43d6092e8d..b22324585c 100644
--- a/templates/shared/searchbottom.tmpl
+++ b/templates/shared/searchbottom.tmpl
@@ -1,3 +1,4 @@
+{{if or .result.Language (not .result.UpdatedUnix.IsZero)}}
 <div class="ui bottom attached table segment tw-flex tw-items-center tw-justify-between">
 		<div class="tw-flex tw-items-center gt-ml-4">
 			{{if .result.Language}}
@@ -10,3 +11,4 @@
 			{{end}}
 		</div>
 </div>
+{{end}}

From 68ec9b48592fe88765bcc3a73093d43c98b315de Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 17:42:49 +0100
Subject: [PATCH 504/679] Migrate margin and padding helpers to tailwind
 (#30043)

This will conclude the refactor of 1:1 class replacements to tailwind,
except `gt-hidden`. Commands ran:

```bash
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-0#tw-$1$2-0#g'   {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-1#tw-$1$2-0.5#g' {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-2#tw-$1$2-1#g'   {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-3#tw-$1$2-2#g'   {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-4#tw-$1$2-4#g'   {web_src/js,templates,routers,services}/**/*
perl -p -i -e 's#gt-(p|m)([lrtbxy])?-5#tw-$1$2-8#g'   {web_src/js,templates,routers,services}/**/*
```
---
 routers/web/repo/blame.go                     |  2 +-
 routers/web/repo/issue_content_history.go     |  2 +-
 routers/web/repo/view.go                      |  4 +-
 services/auth/source/oauth2/providers.go      |  2 +-
 services/auth/source/oauth2/providers_base.go |  4 +-
 .../auth/source/oauth2/providers_openid.go    |  2 +-
 templates/admin/config.tmpl                   |  4 +-
 templates/admin/config_settings.tmpl          |  2 +-
 templates/admin/cron.tmpl                     |  2 +-
 templates/admin/dashboard.tmpl                |  2 +-
 templates/admin/emails/list.tmpl              |  4 +-
 templates/admin/org/list.tmpl                 |  4 +-
 templates/admin/queue_manage.tmpl             |  2 +-
 templates/admin/repo/unadopted.tmpl           |  4 +-
 templates/admin/self_check.tmpl               |  6 +-
 templates/admin/stacktrace-row.tmpl           | 10 +-
 templates/admin/user/edit.tmpl                |  4 +-
 templates/base/head_navbar.tmpl               | 28 +++---
 templates/base/paginate.tmpl                  |  8 +-
 templates/devtest/fetch-action.tmpl           |  2 +-
 templates/devtest/flex-list.tmpl              |  4 +-
 templates/devtest/gitea-ui.tmpl               | 12 +--
 templates/explore/repo_list.tmpl              |  2 +-
 templates/explore/search.tmpl                 |  4 +-
 templates/home.tmpl                           |  2 +-
 templates/install.tmpl                        | 16 +--
 templates/org/follow_unfollow.tmpl            |  2 +-
 templates/org/header.tmpl                     |  4 +-
 templates/org/projects/list.tmpl              |  2 +-
 templates/org/team/members.tmpl               |  2 +-
 templates/org/team/new.tmpl                   |  6 +-
 templates/org/team/repositories.tmpl          |  2 +-
 templates/package/metadata/alpine.tmpl        |  2 +-
 templates/package/metadata/cargo.tmpl         | 10 +-
 templates/package/metadata/chef.tmpl          |  6 +-
 templates/package/metadata/composer.tmpl      |  6 +-
 templates/package/metadata/conan.tmpl         |  8 +-
 templates/package/metadata/conda.tmpl         |  8 +-
 templates/package/metadata/container.tmpl     | 14 +--
 templates/package/metadata/helm.tmpl          |  4 +-
 templates/package/metadata/maven.tmpl         |  6 +-
 templates/package/metadata/npm.tmpl           |  8 +-
 templates/package/metadata/nuget.tmpl         |  6 +-
 templates/package/metadata/pub.tmpl           |  6 +-
 templates/package/metadata/pypi.tmpl          |  6 +-
 templates/package/metadata/rpm.tmpl           |  4 +-
 templates/package/metadata/rubygems.tmpl      |  6 +-
 templates/package/metadata/swift.tmpl         |  2 +-
 templates/package/metadata/vagrant.tmpl       |  6 +-
 templates/package/shared/list.tmpl            |  2 +-
 templates/package/shared/versionlist.tmpl     |  2 +-
 templates/package/view.tmpl                   | 14 +--
 templates/projects/list.tmpl                  |  8 +-
 templates/projects/view.tmpl                  |  8 +-
 templates/repo/actions/list.tmpl              |  2 +-
 templates/repo/actions/runs_list.tmpl         |  4 +-
 templates/repo/blame.tmpl                     |  4 +-
 templates/repo/branch/list.tmpl               | 40 ++++----
 templates/repo/branch_dropdown.tmpl           |  6 +-
 .../code/recently_pushed_new_branches.tmpl    |  2 +-
 .../repo/commit_load_branches_and_tags.tmpl   | 10 +-
 templates/repo/commit_page.tmpl               | 66 ++++++-------
 templates/repo/commits.tmpl                   |  2 +-
 templates/repo/commits_list.tmpl              | 10 +-
 templates/repo/commits_list_small.tmpl        |  2 +-
 templates/repo/commits_table.tmpl             |  4 +-
 templates/repo/diff/box.tmpl                  | 20 ++--
 templates/repo/diff/comment_form.tmpl         |  2 +-
 templates/repo/diff/comments.tmpl             |  2 +-
 templates/repo/diff/compare.tmpl              | 10 +-
 templates/repo/diff/conversation.tmpl         | 16 +--
 templates/repo/diff/new_review.tmpl           |  2 +-
 templates/repo/diff/stats.tmpl                |  2 +-
 templates/repo/diff/whitespace_dropdown.tmpl  |  8 +-
 templates/repo/editor/commit_form.tmpl        |  2 +-
 templates/repo/editor/patch.tmpl              |  2 +-
 templates/repo/empty.tmpl                     |  2 +-
 templates/repo/file_info.tmpl                 |  2 +-
 templates/repo/find/files.tmpl                |  4 +-
 templates/repo/forks.tmpl                     |  4 +-
 templates/repo/graph.tmpl                     | 14 +--
 templates/repo/graph/commits.tmpl             | 10 +-
 templates/repo/header.tmpl                    |  4 +-
 templates/repo/home.tmpl                      | 22 ++---
 .../repo/issue/branch_selector_field.tmpl     |  4 +-
 templates/repo/issue/card.tmpl                | 20 ++--
 templates/repo/issue/fields/checkboxes.tmpl   |  2 +-
 templates/repo/issue/fields/textarea.tmpl     |  2 +-
 templates/repo/issue/filter_actions.tmpl      |  4 +-
 templates/repo/issue/filter_list.tmpl         |  6 +-
 templates/repo/issue/filters.tmpl             |  2 +-
 templates/repo/issue/labels.tmpl              |  2 +-
 .../repo/issue/labels/edit_delete_label.tmpl  |  4 +-
 templates/repo/issue/labels/label_list.tmpl   |  2 +-
 .../issue/labels/labels_selector_field.tmpl   |  2 +-
 .../repo/issue/milestone/select_menu.tmpl     |  4 +-
 templates/repo/issue/milestone_issues.tmpl    |  6 +-
 templates/repo/issue/milestones.tmpl          |  4 +-
 templates/repo/issue/new_form.tmpl            | 22 ++---
 templates/repo/issue/openclose.tmpl           |  8 +-
 .../repo/issue/view_content/attachments.tmpl  |  4 +-
 .../repo/issue/view_content/comments.tmpl     |  2 +-
 .../repo/issue/view_content/conversation.tmpl | 22 ++---
 templates/repo/issue/view_content/pull.tmpl   |  4 +-
 .../view_content/pull_merge_instruction.tmpl  |  2 +-
 .../repo/issue/view_content/sidebar.tmpl      | 76 +++++++-------
 .../repo/issue/view_content/watching.tmpl     |  4 +-
 templates/repo/issue/view_title.tmpl          | 14 +--
 templates/repo/latest_commit.tmpl             |  4 +-
 templates/repo/migrate/migrate.tmpl           |  6 +-
 templates/repo/projects/view.tmpl             |  2 +-
 templates/repo/pulls/tab_menu.tmpl            |  2 +-
 templates/repo/pulse.tmpl                     | 12 +--
 templates/repo/release/list.tmpl              | 18 ++--
 templates/repo/release/new.tmpl               |  6 +-
 templates/repo/settings/branches.tmpl         |  2 +-
 templates/repo/settings/deploy_keys.tmpl      |  2 +-
 templates/repo/settings/githooks.tmpl         |  6 +-
 templates/repo/settings/options.tmpl          | 20 ++--
 templates/repo/settings/protected_branch.tmpl |  8 +-
 .../repo/settings/webhook/base_list.tmpl      |  8 +-
 templates/repo/sub_menu.tmpl                  |  2 +-
 templates/repo/tag/list.tmpl                  | 20 ++--
 templates/repo/view_file.tmpl                 | 12 +--
 templates/repo/view_list.tmpl                 |  2 +-
 templates/repo/wiki/new.tmpl                  |  2 +-
 templates/repo/wiki/revision.tmpl             |  2 +-
 templates/repo/wiki/start.tmpl                |  2 +-
 templates/shared/actions/runner_edit.tmpl     |  8 +-
 templates/shared/issuelist.tmpl               |  4 +-
 templates/shared/search/code/results.tmpl     |  4 +-
 templates/shared/searchbottom.tmpl            |  6 +-
 templates/shared/secrets/add_list.tmpl        |  2 +-
 templates/shared/user/authorlink.tmpl         |  2 +-
 templates/shared/user/blocked_users.tmpl      |  2 +-
 templates/shared/user/profile_big_avatar.tmpl |  6 +-
 templates/shared/variables/variable_list.tmpl |  4 +-
 templates/status/500.tmpl                     |  8 +-
 templates/user/auth/signin_inner.tmpl         |  4 +-
 templates/user/auth/signup_inner.tmpl         |  4 +-
 templates/user/auth/webauthn.tmpl             |  2 +-
 templates/user/auth/webauthn_error.tmpl       |  2 +-
 templates/user/dashboard/feeds.tmpl           |  2 +-
 templates/user/dashboard/issues.tmpl          |  4 +-
 templates/user/dashboard/milestones.tmpl      |  8 +-
 .../user/notification/notification_div.tmpl   | 28 +++---
 .../notification_subscriptions.tmpl           |  4 +-
 templates/user/overview/package_versions.tmpl |  2 +-
 templates/user/overview/packages.tmpl         |  2 +-
 templates/user/profile.tmpl                   |  2 +-
 templates/user/settings/account.tmpl          |  4 +-
 templates/user/settings/applications.tmpl     | 14 +--
 .../applications_oauth2_edit_form.tmpl        |  2 +-
 .../settings/applications_oauth2_list.tmpl    |  4 +-
 templates/user/settings/grants_oauth2.tmpl    |  2 +-
 templates/user/settings/keys_gpg.tmpl         |  2 +-
 templates/user/settings/keys_ssh.tmpl         |  2 +-
 templates/user/settings/profile.tmpl          |  4 +-
 templates/user/settings/repos.tmpl            | 20 ++--
 web_src/css/helpers.css                       | 98 -------------------
 web_src/js/components/DashboardRepoList.vue   | 38 +++----
 web_src/js/components/DiffCommitSelector.vue  |  2 +-
 web_src/js/components/DiffFileList.vue        |  8 +-
 web_src/js/components/DiffFileTree.vue        |  2 +-
 .../js/components/PullRequestMergeForm.vue    |  6 +-
 web_src/js/components/RepoActionView.vue      | 14 +--
 .../js/components/RepoBranchTagSelector.vue   | 10 +-
 web_src/js/components/RepoCodeFrequency.vue   |  2 +-
 web_src/js/components/RepoContributors.vue    |  4 +-
 web_src/js/components/RepoRecentCommits.vue   |  2 +-
 .../components/ScopedAccessTokenSelector.vue  |  2 +-
 web_src/js/features/repo-diff-commit.js       |  2 +-
 web_src/js/features/repo-findfile.js          |  2 +-
 web_src/js/features/repo-home.js              |  2 +-
 web_src/js/features/repo-issue-content.js     |  2 +-
 web_src/js/features/repo-issue-list.js        |  2 +-
 web_src/js/features/repo-legacy.js            |  6 +-
 web_src/js/features/tribute.js                |  2 +-
 178 files changed, 605 insertions(+), 703 deletions(-)

diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 549ccdeabe..935e6d78fc 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -280,7 +280,7 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames
 				if commit.User != nil {
 					avatar = string(avatarUtils.Avatar(commit.User, 18))
 				} else {
-					avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "gt-mr-3"))
+					avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "tw-mr-2"))
 				}
 
 				br.Avatar = gotemplate.HTML(avatar)
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 1ec497658f..bf3571c835 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -70,7 +70,7 @@ func GetContentHistoryList(ctx *context.Context) {
 		}
 
 		src := html.EscapeString(item.UserAvatarLink)
-		class := avatars.DefaultAvatarClass + " gt-mr-3"
+		class := avatars.DefaultAvatarClass + " tw-mr-2"
 		name := html.EscapeString(username)
 		avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
 		timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index b9e623919a..73a7be4e89 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -919,9 +919,9 @@ func prepareOpenWithEditorApps(ctx *context.Context) {
 		schema, _, _ := strings.Cut(app.OpenURL, ":")
 		var iconHTML template.HTML
 		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
-			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "gt-mr-3")
+			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2")
 		} else {
-			iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future
+			iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future
 		}
 		tmplApps = append(tmplApps, map[string]any{
 			"DisplayName": app.DisplayName,
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index c3edae4ab6..6ed6c184eb 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -59,7 +59,7 @@ func (p *AuthSourceProvider) DisplayName() string {
 
 func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
 	if p.iconURL != "" {
-		img := fmt.Sprintf(`<img class="tw-object-contain gt-mr-3" width="%d" height="%d" src="%s" alt="%s">`,
+		img := fmt.Sprintf(`<img class="tw-object-contain tw-mr-2" width="%d" height="%d" src="%s" alt="%s">`,
 			size,
 			size,
 			html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()),
diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go
index 5b6694487b..9d4ab106e5 100644
--- a/services/auth/source/oauth2/providers_base.go
+++ b/services/auth/source/oauth2/providers_base.go
@@ -35,10 +35,10 @@ func (b *BaseProvider) IconHTML(size int) template.HTML {
 	case "github":
 		svgName = "octicon-mark-github"
 	}
-	svgHTML := svg.RenderHTML(svgName, size, "gt-mr-3")
+	svgHTML := svg.RenderHTML(svgName, size, "tw-mr-2")
 	if svgHTML == "" {
 		log.Error("No SVG icon for oauth2 provider %q", b.name)
-		svgHTML = svg.RenderHTML("gitea-openid", size, "gt-mr-3")
+		svgHTML = svg.RenderHTML("gitea-openid", size, "tw-mr-2")
 	}
 	return svgHTML
 }
diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go
index a4dcfcafc7..285876d5ac 100644
--- a/services/auth/source/oauth2/providers_openid.go
+++ b/services/auth/source/oauth2/providers_openid.go
@@ -29,7 +29,7 @@ func (o *OpenIDProvider) DisplayName() string {
 
 // IconHTML returns icon HTML for this provider
 func (o *OpenIDProvider) IconHTML(size int) template.HTML {
-	return svg.RenderHTML("gitea-openid", size, "gt-mr-3")
+	return svg.RenderHTML("gitea-openid", size, "tw-mr-2")
 }
 
 // CreateGothProvider creates a GothProvider from this Provider
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 6bdda07e48..8c16429920 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -229,7 +229,7 @@
 					<dt>{{ctx.Locale.Tr "admin.config.mailer_user"}}</dt>
 					<dd>{{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}</dd>
 					<div class="divider"></div>
-					<dt class="gt-py-2">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
+					<dt class="tw-py-1">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
 					<dd>
 						<form class="ui form ignore-dirty" action="{{AppSubUrl}}/admin/config/test_mail" method="post">
 							{{.CsrfTokenHtml}}
@@ -332,7 +332,7 @@
 				{{range $loggerName, $loggerDetail := .Loggers}}
 					<dt>{{ctx.Locale.Tr "admin.config.logger_name_fmt" $loggerName}}</dt>
 					{{if $loggerDetail.IsEnabled}}
-						<dd><pre class="gt-m-0">{{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}</pre></dd>
+						<dd><pre class="tw-m-0">{{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}</pre></dd>
 					{{else}}
 						<dd>{{ctx.Locale.Tr "admin.config.disabled_logger"}}</dd>
 					{{end}}
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
index 22ad5c24ac..d7fb022274 100644
--- a/templates/admin/config_settings.tmpl
+++ b/templates/admin/config_settings.tmpl
@@ -28,7 +28,7 @@
 		<div class="field">
 			<details>
 				<summary>{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}}</summary>
-				<pre class="gt-px-4">{{.DefaultOpenWithEditorAppsString}}</pre>
+				<pre class="tw-px-4">{{.DefaultOpenWithEditorAppsString}}</pre>
 			</details>
 		</div>
 		<div class="field">
diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl
index af30cc06e1..3cb641488c 100644
--- a/templates/admin/cron.tmpl
+++ b/templates/admin/cron.tmpl
@@ -5,7 +5,7 @@
 	</h4>
 	<div class="ui attached table segment">
 		<form method="post" action="{{AppSubUrl}}/admin">
-			<table class="ui very basic striped table unstackable gt-mb-0">
+			<table class="ui very basic striped table unstackable tw-mb-0">
 				<thead>
 					<tr>
 						<th></th>
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl
index cc7d338589..bfd2ee6670 100644
--- a/templates/admin/dashboard.tmpl
+++ b/templates/admin/dashboard.tmpl
@@ -11,7 +11,7 @@
 		<div class="ui attached table segment">
 			<form method="post" action="{{AppSubUrl}}/admin">
 				{{.CsrfTokenHtml}}
-				<table class="ui very basic table gt-mt-0 gt-px-4">
+				<table class="ui very basic table tw-mt-0 tw-px-4">
 					<tbody>
 						<tr>
 							<td>{{ctx.Locale.Tr "admin.dashboard.delete_inactive_accounts"}}</td>
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index b72aef8f35..388863df9b 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -4,12 +4,12 @@
 			{{ctx.Locale.Tr "admin.emails.email_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
 		</h4>
 		<div class="ui attached segment">
-			<div class="ui secondary filter menu tw-items-center gt-mx-0">
+			<div class="ui secondary filter menu tw-items-center tw-mx-0">
 				<form class="ui form ignore-dirty tw-flex-1">
 					{{template "shared/search/combo" dict "Value" .Keyword}}
 				</form>
 				<!-- Sort -->
-				<div class="ui dropdown type jump item gt-mr-0">
+				<div class="ui dropdown type jump item tw-mr-0">
 					<span class="text">
 						{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 					</span>
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index ca0ee30092..987ceab1e0 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -7,12 +7,12 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			<div class="ui secondary filter menu tw-items-center gt-mx-0">
+			<div class="ui secondary filter menu tw-items-center tw-mx-0">
 				<form class="ui form ignore-dirty tw-flex-1">
 					{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}}
 				</form>
 				<!-- Sort -->
-				<div class="ui dropdown type jump item gt-mr-0">
+				<div class="ui dropdown type jump item tw-mr-0">
 					<span class="text">
 						{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 					</span>
diff --git a/templates/admin/queue_manage.tmpl b/templates/admin/queue_manage.tmpl
index dd1682a000..dc0196fc6a 100644
--- a/templates/admin/queue_manage.tmpl
+++ b/templates/admin/queue_manage.tmpl
@@ -30,7 +30,7 @@
 								-
 							{{else}}
 								{{$sum}}
-								<form action="{{$.Link}}/remove-all-items" method="post" class="tw-inline-block gt-ml-4">
+								<form action="{{$.Link}}/remove-all-items" method="post" class="tw-inline-block tw-ml-4">
 									{{$.CsrfTokenHtml}}
 									<button class="ui tiny basic red button">{{ctx.Locale.Tr "admin.monitor.queue.settings.remove_all_items"}}</button>
 								</form>
diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl
index 9166a844a7..6a8e203694 100644
--- a/templates/admin/repo/unadopted.tmpl
+++ b/templates/admin/repo/unadopted.tmpl
@@ -23,7 +23,7 @@
 							<div class="item tw-flex tw-items-center">
 								<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
 								<div>
-									<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
+									<button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
 									<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
 										<div class="header">
 											<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
@@ -40,7 +40,7 @@
 											{{template "base/modal_actions_confirm"}}
 										</form>
 									</div>
-									<button class="ui button red show-modal gt-p-3" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
+									<button class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
 									<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
 										<div class="header">
 											<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
diff --git a/templates/admin/self_check.tmpl b/templates/admin/self_check.tmpl
index fafaf9242d..94c4673a49 100644
--- a/templates/admin/self_check.tmpl
+++ b/templates/admin/self_check.tmpl
@@ -7,9 +7,9 @@
 	<div class="ui attached segment">
 		{{if .DatabaseCheckHasProblems}}
 			{{if .DatabaseType.IsMySQL}}
-				<div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
+				<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
 			{{else if .DatabaseType.IsMSSQL}}
-				<div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
+				<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
 			{{end}}
 			{{if .DatabaseCheckCollationMismatch}}
 				<div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div>
@@ -28,7 +28,7 @@
 				</div>
 			{{end}}
 		{{else}}
-			<div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}</div>
+			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}</div>
 		{{end}}
 	</div>
 </div>
diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl
index 3f639ba161..694bf56d96 100644
--- a/templates/admin/stacktrace-row.tmpl
+++ b/templates/admin/stacktrace-row.tmpl
@@ -1,6 +1,6 @@
 <div class="item">
 	<div class="tw-flex tw-items-center">
-		<div class="icon gt-ml-3 gt-mr-3">
+		<div class="icon tw-ml-2 tw-mr-2">
 			{{if eq .Process.Type "request"}}
 				{{svg "octicon-globe" 16}}
 			{{else if eq .Process.Type "system"}}
@@ -22,14 +22,14 @@
 		</div>
 	</div>
 	{{if .Process.Stacks}}
-		<div class="divided list gt-ml-3">
+		<div class="divided list tw-ml-2">
 			{{range .Process.Stacks}}
 				<div class="item">
 					<details>
 						<summary>
 							<div class="flex-text-inline">
-								<div class="header gt-ml-3">
-									<span class="icon gt-mr-3">{{svg "octicon-code" 16}}</span>{{.Description}}{{if gt .Count 1}} * {{.Count}}{{end}}
+								<div class="header tw-ml-2">
+									<span class="icon tw-mr-2">{{svg "octicon-code" 16}}</span>{{.Description}}{{if gt .Count 1}} * {{.Count}}{{end}}
 								</div>
 								<div class="description">
 									{{range .Labels}}
@@ -41,7 +41,7 @@
 						<div class="list">
 							{{range .Entry}}
 								<div class="item tw-flex tw-items-center">
-									<span class="icon gt-mr-4">{{svg "octicon-dot-fill" 16}}</span>
+									<span class="icon tw-mr-4">{{svg "octicon-dot-fill" 16}}</span>
 									<div class="content tw-flex-1">
 										<div class="header"><code>{{.Function}}</code></div>
 										<div class="description"><code>{{.File}}:{{.Line}}</code></div>
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index 159c821099..751eb6d83f 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -181,7 +181,7 @@
 						<label>{{ctx.Locale.Tr "settings.lookup_avatar_by_mail"}}</label>
 					</div>
 				</div>
-				<div class="field gt-pl-4 {{if .Err_Gravatar}}error{{end}}">
+				<div class="field tw-pl-4 {{if .Err_Gravatar}}error{{end}}">
 					<label for="gravatar">Avatar {{ctx.Locale.Tr "email"}}</label>
 					<input id="gravatar" name="gravatar" value="{{.User.AvatarEmail}}">
 				</div>
@@ -194,7 +194,7 @@
 					</div>
 				</div>
 
-				<div class="inline field gt-pl-4">
+				<div class="inline field tw-pl-4">
 					<label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
 					<input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
 				</div>
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 50ca744457..490fddcf05 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -13,14 +13,14 @@
 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
 		<div class="ui secondary menu item navbar-mobile-right only-mobile">
 			{{if .IsSigned}}
-			<a id="mobile-notifications-icon" class="item tw-w-auto gt-p-3" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
+			<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 				<div class="tw-relative">
 					{{svg "octicon-bell"}}
 					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
 			</a>
 			{{end}}
-			<button class="item tw-w-auto ui icon mini button gt-p-3 gt-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button>
+			<button class="item tw-w-auto ui icon mini button tw-p-2 tw-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button>
 		</div>
 
 		<!-- navbar links non-mobile -->
@@ -57,8 +57,8 @@
 		{{if and .IsSigned .MustChangePassword}}
 			<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
 				<span class="text tw-flex tw-items-center">
-					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
-					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
+					{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
+					<span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
@@ -75,19 +75,19 @@
 			</div><!-- end dropdown avatar menu -->
 		{{else if .IsSigned}}
 			{{if EnableTimetracking}}
-			<a class="active-stopwatch-trigger item gt-mx-0{{if not .ActiveStopwatch}} gt-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
+			<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} gt-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
 				<div class="tw-relative">
 					{{svg "octicon-stopwatch"}}
 					<span class="header-stopwatch-dot"></span>
 				</div>
-				<span class="only-mobile gt-ml-3">{{ctx.Locale.Tr "active_stopwatch"}}</span>
+				<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
 			</a>
-			<div class="active-stopwatch-popup item tippy-target gt-p-3">
+			<div class="active-stopwatch-popup item tippy-target tw-p-2">
 				<div class="tw-flex tw-items-center">
 					<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
-						{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+						{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
-						<span class="ui primary label stopwatch-time gt-my-0 gt-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
+						<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
 							{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
 						</span>
 					</a>
@@ -111,14 +111,14 @@
 			</div>
 			{{end}}
 
-			<a class="item not-mobile gt-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
+			<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 				<div class="tw-relative">
 					{{svg "octicon-bell"}}
 					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
 			</a>
 
-			<div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}">
+			<div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}">
 				<span class="text">
 					{{svg "octicon-plus"}}
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
@@ -141,10 +141,10 @@
 				</div><!-- end content create new menu -->
 			</div><!-- end dropdown menu create new -->
 
-			<div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
+			<div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
 				<span class="text tw-flex tw-items-center">
-					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
-					<span class="only-mobile gt-ml-3">{{.SignedUser.Name}}</span>
+					{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
+					<span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl
index 8c2adc1f94..9a7a6322f7 100644
--- a/templates/base/paginate.tmpl
+++ b/templates/base/paginate.tmpl
@@ -6,11 +6,11 @@
 		<div class="center page buttons">
 			<div class="ui borderless pagination menu">
 				<a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$paginationLink}}{{if $paginationParams}}?{{$paginationParams}}{{end}}"{{end}}>
-					{{svg "gitea-double-chevron-left" 16 "gt-mr-2"}}
+					{{svg "gitea-double-chevron-left" 16 "tw-mr-1"}}
 					<span class="navigation_label">{{ctx.Locale.Tr "admin.first_page"}}</span>
 				</a>
 				<a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$paginationLink}}?page={{.Previous}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
-					{{svg "octicon-chevron-left" 16 "gt-mr-2"}}
+					{{svg "octicon-chevron-left" 16 "tw-mr-1"}}
 					<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.previous"}}</span>
 				</a>
 				{{range .Pages}}
@@ -22,11 +22,11 @@
 				{{end}}
 				<a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$paginationLink}}?page={{.Next}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
 					<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.next"}}</span>
-					{{svg "octicon-chevron-right" 16 "gt-ml-2"}}
+					{{svg "octicon-chevron-right" 16 "tw-ml-1"}}
 				</a>
 				<a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$paginationLink}}?page={{.TotalPages}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
 					<span class="navigation_label">{{ctx.Locale.Tr "admin.last_page"}}</span>
-					{{svg "gitea-double-chevron-right" 16 "gt-ml-2"}}
+					{{svg "gitea-double-chevron-right" 16 "tw-ml-1"}}
 				</a>
 			</div>
 		</div>
diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl
index 70844a8751..7b0bbba554 100644
--- a/templates/devtest/fetch-action.tmpl
+++ b/templates/devtest/fetch-action.tmpl
@@ -26,7 +26,7 @@
 				<div><button name="btn">submit post</button></div>
 			</form>
 			<form method="post" action="/no-such-uri" class="form-fetch-action">
-				<div class="gt-py-5">bad action url</div>
+				<div class="tw-py-8">bad action url</div>
 				<div><button name="btn">submit test</button></div>
 			</form>
 		</div>
diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl
index fdb9eb6b39..d5678566d8 100644
--- a/templates/devtest/flex-list.tmpl
+++ b/templates/devtest/flex-list.tmpl
@@ -73,7 +73,7 @@
 						</div>
 						<div class="flex-item-trailing">
 							<a class="muted" href="{{$.Link}}">
-								<span class="flex-text-inline"><i class="color-icon gt-mr-3 tw-bg-blue"></i>Go</span>
+								<span class="flex-text-inline"><i class="color-icon tw-mr-2 tw-bg-blue"></i>Go</span>
 							</a>
 							<a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-star" 16}}45000</a>
 							<a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-git-branch" 16}}1234</a>
@@ -104,7 +104,7 @@
 		</div>
 
 		<h1>If parent provides the padding/margin space:</h1>
-		<div class="tw-border tw-border-secondary gt-py-4">
+		<div class="tw-border tw-border-secondary tw-py-4">
 			<div class="flex-list flex-space-fitted">
 				<div class="flex-item">item 1 (no padding top)</div>
 				<div class="flex-item">item 2 (no padding bottom)</div>
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 6341076323..76de4a93d7 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -67,10 +67,10 @@
 				</li>
 				<li class="sample-group">
 					<h2>Inline / Plain:</h2>
-					<div class="gt-my-2">
-						<button class="btn gt-p-3">Plain button</button>
-						<button class="btn interact-fg gt-p-3">Plain button with interact fg</button>
-						<button class="btn interact-bg gt-p-3">Plain button with interact bg</button>
+					<div class="tw-my-1">
+						<button class="btn tw-p-2">Plain button</button>
+						<button class="btn interact-fg tw-p-2">Plain button with interact fg</button>
+						<button class="btn interact-bg tw-p-2">Plain button with interact bg</button>
 					</div>
 				</li>
 			</ul>
@@ -102,8 +102,8 @@
 
 	<div>
 		<h1>Loading</h1>
-		<div class="is-loading small-loading-icon tw-border tw-border-secondary gt-py-2"><span>loading ...</span></div>
-		<div class="is-loading tw-border tw-border-secondary gt-py-4">
+		<div class="is-loading small-loading-icon tw-border tw-border-secondary tw-py-1"><span>loading ...</span></div>
+		<div class="is-loading tw-border tw-border-secondary tw-py-4">
 			<p>loading ...</p>
 			<p>loading ...</p>
 			<p>loading ...</p>
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index 4c0f93ed2e..d00773a963 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -33,7 +33,7 @@
 					<div class="flex-item-trailing muted-links">
 						{{if .PrimaryLanguage}}
 							<a class="flex-text-inline" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
-								<i class="color-icon gt-mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>
+								<i class="color-icon tw-mr-2" style="background-color: {{.PrimaryLanguage.Color}}"></i>
 								{{.PrimaryLanguage.Language}}
 							</a>
 						{{end}}
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index c12ff325f9..1d984a2e37 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -1,4 +1,4 @@
-<div class="ui small secondary filter menu tw-items-center gt-mx-0">
+<div class="ui small secondary filter menu tw-items-center tw-mx-0">
 	<form class="ui form ignore-dirty tw-flex-1">
 		{{if .PageIsExploreUsers}}
 			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
@@ -7,7 +7,7 @@
 		{{end}}
 	</form>
 	<!-- Sort -->
-	<div class="ui small dropdown type jump item gt-mr-0">
+	<div class="ui small dropdown type jump item tw-mr-0">
 		<span class="text">
 			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 		</span>
diff --git a/templates/home.tmpl b/templates/home.tmpl
index 1e5369e7ee..e6fd4ef020 100644
--- a/templates/home.tmpl
+++ b/templates/home.tmpl
@@ -1,6 +1,6 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}" class="page-content home">
-	<div class="gt-mb-5 gt-px-5">
+	<div class="tw-mb-8 tw-px-8">
 		<div class="center">
 			<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}">
 			<div class="hero">
diff --git a/templates/install.tmpl b/templates/install.tmpl
index 05a74cc788..b2f449618c 100644
--- a/templates/install.tmpl
+++ b/templates/install.tmpl
@@ -28,7 +28,7 @@
 						</div>
 					</div>
 
-					<div class="gt-mt-4 gt-hidden" data-db-setting-for="common-host">
+					<div class="tw-mt-4 gt-hidden" data-db-setting-for="common-host">
 						<div class="inline required field {{if .Err_DbSetting}}error{{end}}">
 							<label for="db_host">{{ctx.Locale.Tr "install.host"}}</label>
 							<input id="db_host" name="db_host" value="{{.db_host}}">
@@ -47,7 +47,7 @@
 						</div>
 					</div>
 
-					<div class="gt-mt-4 gt-hidden" data-db-setting-for="postgres">
+					<div class="tw-mt-4 gt-hidden" data-db-setting-for="postgres">
 						<div class="inline required field">
 							<label>{{ctx.Locale.Tr "install.ssl_mode"}}</label>
 							<div class="ui selection database type dropdown">
@@ -68,7 +68,7 @@
 						</div>
 					</div>
 
-					<div class="gt-mt-4 gt-hidden" data-db-setting-for="sqlite3">
+					<div class="tw-mt-4 gt-hidden" data-db-setting-for="sqlite3">
 						<div class="inline required field {{if or .Err_DbPath .Err_DbSetting}}error{{end}}">
 							<label for="db_path">{{ctx.Locale.Tr "install.path"}}</label>
 							<input id="db_path" name="db_path" value="{{.db_path}}">
@@ -160,7 +160,7 @@
 
 					<!-- Email -->
 					<details class="optional field">
-						<summary class="right-content gt-py-3{{if .Err_SMTP}} text red{{end}}">
+						<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}">
 							{{ctx.Locale.Tr "install.email_title"}}
 						</summary>
 						<div class="inline field">
@@ -200,7 +200,7 @@
 
 					<!-- Server and other services -->
 					<details class="optional field">
-						<summary class="right-content gt-py-3{{if .Err_Services}} text red{{end}}">
+						<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}">
 							{{ctx.Locale.Tr "install.server_service_title"}}
 						</summary>
 						<div class="inline field">
@@ -298,7 +298,7 @@
 
 					<!-- Admin -->
 					<details class="optional field">
-						<summary class="right-content gt-py-3{{if .Err_Admin}} text red{{end}}">
+						<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}">
 							{{ctx.Locale.Tr "install.admin_title"}}
 						</summary>
 						<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
@@ -327,7 +327,7 @@
 						<div class="right-content">
 							{{ctx.Locale.Tr "install.env_config_keys_prompt"}}
 						</div>
-						<div class="right-content gt-mt-3">
+						<div class="right-content tw-mt-2">
 							{{range .EnvConfigKeys}}<span class="ui label">{{.}}</span>{{end}}
 						</div>
 					</div>
@@ -338,7 +338,7 @@
 						<div class="right-content">
 							These configuration options will be written into: {{.CustomConfFile}}
 						</div>
-						<div class="right-content gt-mt-3">
+						<div class="right-content tw-mt-2">
 							<button class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button>
 						</div>
 					</div>
diff --git a/templates/org/follow_unfollow.tmpl b/templates/org/follow_unfollow.tmpl
index b9a3bb77fe..ba0bd01efe 100644
--- a/templates/org/follow_unfollow.tmpl
+++ b/templates/org/follow_unfollow.tmpl
@@ -1,4 +1,4 @@
-<button class="ui basic button gt-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
+<button class="ui basic button tw-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
 	{{if $.IsFollowing}}
 		{{ctx.Locale.Tr "user.unfollow"}}
 	{{else}}
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 6eb7feb33f..7361df99ea 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -9,7 +9,7 @@
 			</span>
 			<span class="tw-flex tw-items-center tw-gap-1 tw-ml-auto tw-text-16 tw-whitespace-nowrap">
 				{{if .EnableFeed}}
-					<a class="ui basic label button gt-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
+					<a class="ui basic label button tw-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
 						{{svg "octicon-rss" 24}}
 					</a>
 				{{end}}
@@ -19,7 +19,7 @@
 			</span>
 		</div>
 		{{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription}}</div>{{end}}
-		<div class="text light meta gt-mt-2">
+		<div class="text light meta tw-mt-1">
 			{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
 			{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
 			{{if .IsSigned}}
diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl
index 97cc6cf66c..ec9cfece9a 100644
--- a/templates/org/projects/list.tmpl
+++ b/templates/org/projects/list.tmpl
@@ -14,7 +14,7 @@
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
 				<div class="ui twelve wide column">
-				<div class="gt-mb-4">
+				<div class="tw-mb-4">
 					{{template "user/overview/header" .}}
 				</div>
 				{{template "projects/list" .}}
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index aa358841da..5719328a27 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -12,7 +12,7 @@
 						<form class="ui form ignore-dirty tw-flex tw-flex-wrap tw-gap-2" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post">
 							{{.CsrfTokenHtml}}
 							<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
-							<div id="search-user-box" class="ui search gt-mr-3"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
+							<div id="search-user-box" class="ui search tw-mr-2"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
 								<div class="ui input">
 									<input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
 								</div>
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl
index d1e0dbe382..0de70296fd 100644
--- a/templates/org/team/new.tmpl
+++ b/templates/org/team/new.tmpl
@@ -78,11 +78,11 @@
 										<tr>
 											<th>{{ctx.Locale.Tr "units.unit"}}</th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.none_access"}}
-											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.read_access"}}
-											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.write_access"}}
-											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
 										</tr>
 									</thead>
 									<tbody>
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl
index 202279240b..98b4854eb8 100644
--- a/templates/org/team/repositories.tmpl
+++ b/templates/org/team/repositories.tmpl
@@ -17,7 +17,7 @@
 									<input class="prompt" name="repo_name" placeholder="{{ctx.Locale.Tr "search.repo_kind"}}" autocomplete="off" required>
 								</div>
 							</div>
-							<button class="ui primary button gt-ml-3">{{ctx.Locale.Tr "add"}}</button>
+							<button class="ui primary button tw-ml-2">{{ctx.Locale.Tr "add"}}</button>
 						</form>
 						<div class="tw-inline-block">
 							<button class="ui primary button link-action" data-modal-confirm="{{ctx.Locale.Tr "org.teams.add_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/addall">{{ctx.Locale.Tr "add_all"}}</button>
diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl
index 73cbc06aac..3e7f10f66a 100644
--- a/templates/package/metadata/alpine.tmpl
+++ b/templates/package/metadata/alpine.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "alpine"}}
 	{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}}
 	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/cargo.tmpl b/templates/package/metadata/cargo.tmpl
index c8471a71ef..5ad3c20a93 100644
--- a/templates/package/metadata/cargo.tmpl
+++ b/templates/package/metadata/cargo.tmpl
@@ -1,7 +1,7 @@
 {{if eq .PackageDescriptor.Package.Type "cargo"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/chef.tmpl b/templates/package/metadata/chef.tmpl
index fa6e068d23..23a9ce3ec0 100644
--- a/templates/package/metadata/chef.tmpl
+++ b/templates/package/metadata/chef.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "chef"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/composer.tmpl b/templates/package/metadata/composer.tmpl
index fbdc33f73d..0f6ff9d6f2 100644
--- a/templates/package/metadata/composer.tmpl
+++ b/templates/package/metadata/composer.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "composer"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/conan.tmpl b/templates/package/metadata/conan.tmpl
index 40bda555bb..4e05ec2587 100644
--- a/templates/package/metadata/conan.tmpl
+++ b/templates/package/metadata/conan.tmpl
@@ -1,6 +1,6 @@
 {{if eq .PackageDescriptor.Package.Type "conan"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl
index f70e2b2a1c..3628686e13 100644
--- a/templates/package/metadata/conda.tmpl
+++ b/templates/package/metadata/conda.tmpl
@@ -1,6 +1,6 @@
 {{if eq .PackageDescriptor.Package.Type "conda"}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/container.tmpl b/templates/package/metadata/container.tmpl
index b05ef0b846..f5abb7ef6e 100644
--- a/templates/package/metadata/container.tmpl
+++ b/templates/package/metadata/container.tmpl
@@ -1,9 +1,9 @@
 {{if eq .PackageDescriptor.Package.Type "container"}}
-	<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Type.Name}}</div>
-	{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}</div>
+	{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl
index 499f77e80d..50ea484999 100644
--- a/templates/package/metadata/helm.tmpl
+++ b/templates/package/metadata/helm.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "helm"}}
-	{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl
index 36f5eca840..548be61790 100644
--- a/templates/package/metadata/maven.tmpl
+++ b/templates/package/metadata/maven.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "maven"}}
-	{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/npm.tmpl b/templates/package/metadata/npm.tmpl
index 9794d851af..df37504e37 100644
--- a/templates/package/metadata/npm.tmpl
+++ b/templates/package/metadata/npm.tmpl
@@ -1,8 +1,8 @@
 {{if eq .PackageDescriptor.Package.Type "npm"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 	{{range .PackageDescriptor.VersionProperties}}
-		{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "gt-mr-3"}} {{.Value}}</div>{{end}}
+		{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}</div>{{end}}
 	{{end}}
 {{end}}
diff --git a/templates/package/metadata/nuget.tmpl b/templates/package/metadata/nuget.tmpl
index f25e1c3b63..5534577bd2 100644
--- a/templates/package/metadata/nuget.tmpl
+++ b/templates/package/metadata/nuget.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "nuget"}}
-	{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/pub.tmpl b/templates/package/metadata/pub.tmpl
index 1e4a90e78c..16f7cec370 100644
--- a/templates/package/metadata/pub.tmpl
+++ b/templates/package/metadata/pub.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "pub"}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/pypi.tmpl b/templates/package/metadata/pypi.tmpl
index f447cb7f4f..3d9b213907 100644
--- a/templates/package/metadata/pypi.tmpl
+++ b/templates/package/metadata/pypi.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "pypi"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl
index 026f129590..eda8a489f3 100644
--- a/templates/package/metadata/rpm.tmpl
+++ b/templates/package/metadata/rpm.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "rpm"}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/rubygems.tmpl b/templates/package/metadata/rubygems.tmpl
index 62150b1a43..9b11287691 100644
--- a/templates/package/metadata/rubygems.tmpl
+++ b/templates/package/metadata/rubygems.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "rubygems"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>	{{end}}
-	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>	{{end}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl
index 326ebe1a94..fdffb6dede 100644
--- a/templates/package/metadata/swift.tmpl
+++ b/templates/package/metadata/swift.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "swift"}}
 	{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl
index a92398a275..4628a2dcbb 100644
--- a/templates/package/metadata/vagrant.tmpl
+++ b/templates/package/metadata/vagrant.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "vagrant"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index a9ee023061..e4e8eca91e 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -50,7 +50,7 @@
 				<p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}</p>
 			</div>
 		{{else}}
-			<p class="gt-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
+			<p class="tw-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
 		{{end}}
 	{{end}}
 	{{template "base/paginate" .}}
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl
index fc34ccc938..e5c568e059 100644
--- a/templates/package/shared/versionlist.tmpl
+++ b/templates/package/shared/versionlist.tmpl
@@ -31,7 +31,7 @@
 		</div>
 	</div>
 	{{else}}
-		<p class="gt-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
+		<p class="tw-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
 	{{end}}
 	{{template "base/paginate" .}}
 </div>
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index e81a714895..6beb249a7f 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -43,12 +43,12 @@
 			<div class="issue-content-right ui segment">
 				<strong>{{ctx.Locale.Tr "packages.details"}}</strong>
 				<div class="ui relaxed list">
-					<div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "gt-mr-3"}} {{.PackageDescriptor.Package.Type.Name}}</div>
+					<div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "tw-mr-2"}} {{.PackageDescriptor.Package.Type.Name}}</div>
 					{{if .HasRepositoryAccess}}
-					<div class="item">{{svg "octicon-repo" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
+					<div class="item">{{svg "octicon-repo" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
 					{{end}}
-					<div class="item">{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div>
-					<div class="item">{{svg "octicon-download" 16 "gt-mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
+					<div class="item">{{svg "octicon-calendar" 16 "tw-mr-2"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div>
+					<div class="item">{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
 					{{template "package/metadata/alpine" .}}
 					{{template "package/metadata/cargo" .}}
 					{{template "package/metadata/chef" .}}
@@ -70,7 +70,7 @@
 					{{template "package/metadata/swift" .}}
 					{{template "package/metadata/vagrant" .}}
 					{{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}}
-					<div class="item">{{svg "octicon-database" 16 "gt-mr-3"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
+					<div class="item">{{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
 					{{end}}
 				</div>
 				{{if not (eq .PackageDescriptor.Package.Type "container")}}
@@ -100,10 +100,10 @@
 					<div class="divider"></div>
 					<div class="ui relaxed list">
 						{{if .HasRepositoryAccess}}
-						<div class="item">{{svg "octicon-issue-opened" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
+						<div class="item">{{svg "octicon-issue-opened" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
 						{{end}}
 						{{if .CanWritePackages}}
-						<div class="item">{{svg "octicon-tools" 16 "gt-mr-3"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
+						<div class="item">{{svg "octicon-tools" 16 "tw-mr-2"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
 						{{end}}
 					</div>
 				{{end}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index f33f9180bb..ec02e9a6fc 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -1,12 +1,12 @@
 {{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
-	<div class="tw-flex tw-justify-between gt-mb-4">
+	<div class="tw-flex tw-justify-between tw-mb-4">
 		<div class="small-menu-items ui compact tiny menu list-header-toggle">
 			<a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}">
-				{{svg "octicon-project-symlink" 16 "gt-mr-3"}}
+				{{svg "octicon-project-symlink" 16 "tw-mr-2"}}
 				{{ctx.Locale.PrettyNumber .OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 			</a>
 			<a class="item{{if .IsShowClosed}} active{{end}}" href="?state=closed&q={{$.Keyword}}">
-				{{svg "octicon-check" 16 "gt-mr-3"}}
+				{{svg "octicon-check" 16 "tw-mr-2"}}
 				{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 			</a>
 		</div>
@@ -41,7 +41,7 @@
 <div class="milestone-list">
 	{{range .Projects}}
 		<li class="milestone-card">
-			<h3 class="flex-text-block gt-m-0">
+			<h3 class="flex-text-block tw-m-0">
 				{{svg .IconName 16}}
 				<a class="muted" href="{{.Link ctx}}">{{.Title}}</a>
 			</h3>
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 93c2cdbb57..ba5cbc3b45 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -1,8 +1,8 @@
 {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
 
 <div class="ui container">
-	<div class="tw-flex tw-justify-between tw-items-center gt-mb-4">
-		<h2 class="gt-mb-0">{{.Project.Title}}</h2>
+	<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
+		<h2 class="tw-mb-0">{{.Project.Title}}</h2>
 		{{if $canWriteProject}}
 			<div class="ui compact mini menu">
 				<a class="item" href="{{.Link}}/edit?redirect=project">
@@ -68,7 +68,7 @@
 		{{range .Columns}}
 			<div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
 				<div class="project-column-header">
-					<div class="ui large label project-column-title gt-py-2">
+					<div class="ui large label project-column-title tw-py-1">
 						<div class="ui small circular grey label project-column-issue-count">
 							{{.NumIssues ctx}}
 						</div>
@@ -76,7 +76,7 @@
 					</div>
 					{{if and $canWriteProject (ne .ID 0)}}
 						<div class="ui dropdown jump item">
-							<div class="gt-px-3">
+							<div class="tw-px-2">
 								{{svg "octicon-kebab-horizontal"}}
 							</div>
 							<div class="menu user-menu">
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index 916949d4f9..b66d0e360a 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -66,7 +66,7 @@
 					</div>
 
 					{{if .AllowDisableOrEnableWorkflow}}
-						<button class="ui jump dropdown btn interact-bg gt-p-3">
+						<button class="ui jump dropdown btn interact-bg tw-p-2">
 							{{svg "octicon-kebab-horizontal"}}
 							<div class="menu">
 								<a class="item link-action" data-url="{{$.Link}}/{{if .CurWorkflowDisabled}}enable{{else}}disable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl
index d393df6539..ac5049cf56 100644
--- a/templates/repo/actions/runs_list.tmpl
+++ b/templates/repo/actions/runs_list.tmpl
@@ -28,9 +28,9 @@
 			</div>
 			<div class="flex-item-trailing">
 				{{if .RefLink}}
-					<a class="ui label gt-px-2 gt-mx-0" href="{{.RefLink}}">{{.PrettyRef}}</a>
+					<a class="ui label tw-px-1 tw-mx-0" href="{{.RefLink}}">{{.PrettyRef}}</a>
 				{{else}}
-					<span class="ui label gt-px-2 gt-mx-0">{{.PrettyRef}}</span>
+					<span class="ui label tw-px-1 tw-mx-0">{{.PrettyRef}}</span>
 				{{end}}
 			</div>
 			<div class="run-list-item-right">
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index fc6b65f142..31b5b99829 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -12,7 +12,7 @@
 {{end}}
 <div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
 	<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
-		<div class="file-header-left tw-flex tw-items-center gt-py-3 gt-pr-4">
+		<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4">
 			{{template "repo/file_info" .}}
 		</div>
 		<div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap">
@@ -69,7 +69,7 @@
 								</td>
 							{{end}}
 							<td rel="L{{$row.RowNumber}}" class="lines-code blame-code chroma">
-								<code class="code-inner gt-pl-3">{{$row.Code}}</code>
+								<code class="code-inner tw-pl-2">{{$row.Code}}</code>
 							</td>
 						</tr>
 					{{end}}
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 7e061696e4..77cccd65b7 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -22,14 +22,14 @@
 								<div class="flex-text-block">
 									{{if .DefaultBranchBranch.IsProtected}}{{svg "octicon-shield-lock"}}{{end}}
 									<a class="gt-ellipsis" href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a>
-									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
 								</div>
-								<p class="info tw-flex tw-items-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
+								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
 							</td>
 							<td class="right aligned middle aligned overflow-visible">
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
-									<button class="btn interact-bg show-create-branch-modal gt-p-3"
+									<button class="btn interact-bg show-create-branch-modal tw-p-2"
 										data-modal="#create-branch-modal"
 										data-branch-from="{{$.DefaultBranchBranch.DBBranch.Name}}"
 										data-branch-from-urlcomponent="{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}"
@@ -39,10 +39,10 @@
 									</button>
 								{{end}}
 								{{if .EnableFeed}}
-									<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-rss"}}</a>
+									<a role="button" class="btn interact-bg tw-p-2" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-rss"}}</a>
 								{{end}}
 								{{if not $.DisableDownloadSourceArchives}}
-									<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" ($.DefaultBranchBranch.DBBranch.Name)}}">
+									<div class="ui dropdown btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" ($.DefaultBranchBranch.DBBranch.Name)}}">
 										{{svg "octicon-download"}}
 										<div class="menu">
 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
@@ -51,7 +51,7 @@
 									</div>
 								{{end}}
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted) (not $.IsMirror)}}
-									<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
+									<button class="btn interact-bg tw-p-2 show-modal show-rename-branch-modal"
 										data-is-default-branch="true"
 										data-modal="#rename-branch-modal"
 										data-old-branch-name="{{$.DefaultBranchBranch.DBBranch.Name}}"
@@ -88,17 +88,17 @@
 							{{if .DBBranch.IsDeleted}}
 								<div class="flex-text-block">
 									<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
-									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 								</div>
 								<p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix ctx.Locale}}</p>
 							{{else}}
 								<div class="flex-text-block">
 									{{if .IsProtected}}{{svg "octicon-shield-lock"}}{{end}}
 									<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
-									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
 								</div>
-								<p class="info tw-flex tw-items-center gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
+								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
 							{{end}}
 							</td>
 							<td class="two wide ui">
@@ -124,29 +124,29 @@
 										</span>
 									{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
 									<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
-										<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
+										<button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
 									</a>
 									{{end}}
 								{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}}
 									{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
 									<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
-										<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
+										<button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
 									</a>
 									{{end}}
 								{{else}}
 									<a href="{{.LatestPullRequest.Issue.Link}}" class="tw-align-middle ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
 									{{if .LatestPullRequest.HasMerged}}
-										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
 									{{else if .LatestPullRequest.Issue.IsClosed}}
-										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
 									{{else}}
-										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
 									{{end}}
 								{{end}}
 							</td>
 							<td class="three wide right aligned overflow-visible">
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
-									<button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal"
+									<button class="btn interact-bg tw-p-2 show-modal show-create-branch-modal"
 										data-branch-from="{{.DBBranch.Name}}"
 										data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}"
 										data-tooltip-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}"
@@ -156,10 +156,10 @@
 									</button>
 								{{end}}
 								{{if $.EnableFeed}}
-									<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a>
+									<a role="button" class="btn interact-bg tw-p-2" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a>
 								{{end}}
 								{{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}}
-									<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}">
+									<div class="ui dropdown btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}">
 										{{svg "octicon-download"}}
 										<div class="menu">
 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
@@ -168,7 +168,7 @@
 									</div>
 								{{end}}
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}}
-									<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
+									<button class="btn interact-bg tw-p-2 show-modal show-rename-branch-modal"
 										data-is-default-branch="false"
 										data-old-branch-name="{{.DBBranch.Name}}"
 										data-modal="#rename-branch-modal"
@@ -179,13 +179,13 @@
 								{{end}}
 								{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}}
 									{{if .DBBranch.IsDeleted}}
-										<button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}">
+										<button class="btn interact-bg tw-p-2 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}">
 											<span class="text blue">
 												{{svg "octicon-reply"}}
 											</span>
 										</button>
 									{{else}}
-										<button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}">
+										<button class="btn interact-bg tw-p-2 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}">
 											{{svg "octicon-trash"}}
 										</button>
 									{{end}}
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index 418006a0f6..6c2e08a985 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -70,8 +70,8 @@
 <div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}">
 	{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
 	<div class="ui dropdown custom">
-		<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex gt-m-0">
-			<span class="text tw-flex tw-items-center gt-mr-2">
+		<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0">
+			<span class="text tw-flex tw-items-center tw-mr-1">
 				{{if .release}}
 					{{ctx.Locale.Tr "repo.release.compare"}}
 				{{else}}
@@ -80,7 +80,7 @@
 					{{else}}
 						{{svg "octicon-git-branch"}}
 					{{end}}
-					<strong ref="dropdownRefName" class="gt-ml-3">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
+					<strong ref="dropdownRefName" class="tw-ml-2">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
 				{{end}}
 			</span>
 			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index 840a0e32ec..17ae7d119d 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -5,7 +5,7 @@
 			{{$branchLink := HTMLFormat `<a href="%s/src/branch/%s">%s</a>` $.RepoLink (PathEscapeSegments .Name) .Name}}
 			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
 		</div>
-		<a role="button" class="ui compact positive button gt-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
+		<a role="button" class="ui compact positive button tw-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
 			{{ctx.Locale.Tr "repo.pulls.compare_changes"}}
 		</a>
 	</div>
diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl
index 49f7323845..9ab1e2fe05 100644
--- a/templates/repo/commit_load_branches_and_tags.tmpl
+++ b/templates/repo/commit_load_branches_and_tags.tmpl
@@ -1,18 +1,18 @@
 {{if not .PageIsWiki}}
 <div class="branch-and-tag-area" data-text-default-branch-tooltip="{{ctx.Locale.Tr "repo.commit.contained_in_default_branch"}}">
-	<button class="ui button ellipsis-button load-branches-and-tags gt-mt-3" aria-expanded="false"
+	<button class="ui button ellipsis-button load-branches-and-tags tw-mt-2" aria-expanded="false"
 		data-fetch-url="{{.RepoLink}}/commit/{{.CommitID}}/load-branches-and-tags"
 		data-tooltip-content="{{ctx.Locale.Tr "repo.commit.load_referencing_branches_and_tags"}}"
 	>...</button>
 	<div class="branch-and-tag-detail gt-hidden">
 		<div class="divider"></div>
 		<div>{{ctx.Locale.Tr "repo.commit.contained_in"}}</div>
-		<div class="tw-flex gt-mt-3">
-			<div class="gt-p-2">{{svg "octicon-git-branch"}}</div>
+		<div class="tw-flex tw-mt-2">
+			<div class="tw-p-1">{{svg "octicon-git-branch"}}</div>
 			<div class="branch-area flex-text-block tw-flex-wrap tw-flex-1"></div>
 		</div>
-		<div class="tw-flex gt-mt-3">
-			<div class="gt-p-2">{{svg "octicon-tag"}}</div>
+		<div class="tw-flex tw-mt-2">
+			<div class="tw-p-1">{{svg "octicon-tag"}}</div>
 			<div class="tag-area flex-text-block tw-flex-wrap tw-flex-1"></div>
 		</div>
 	</div>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 345c28f475..3ae7fffa1c 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -18,8 +18,8 @@
 			{{end}}
 		{{end}}
 		<div class="ui top attached header clearing segment tw-relative commit-header {{$class}}">
-			<div class="tw-flex gt-mb-4 tw-flex-wrap">
-				<h3 class="gt-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
+			<div class="tw-flex tw-mb-4 tw-flex-wrap">
+				<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
 				{{if not $.PageIsWiki}}
 					<div>
 						<a class="ui primary tiny button" href="{{.SourcePath}}">
@@ -139,27 +139,27 @@
 			{{end}}
 			{{template "repo/commit_load_branches_and_tags" .}}
 		</div>
-		<div class="ui attached segment tw-flex tw-items-center tw-justify-between gt-py-2 commit-header-row tw-flex-wrap {{$class}}">
+		<div class="ui attached segment tw-flex tw-items-center tw-justify-between tw-py-1 commit-header-row tw-flex-wrap {{$class}}">
 				<div class="tw-flex tw-items-center author">
 					{{if .Author}}
-						{{ctx.AvatarUtils.Avatar .Author 28 "gt-mr-3"}}
+						{{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}}
 						{{if .Author.FullName}}
 							<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a>
 						{{else}}
 							<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a>
 						{{end}}
 					{{else}}
-						{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "gt-mr-3"}}
+						{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}}
 						<strong>{{.Commit.Author.Name}}</strong>
 					{{end}}
-					<span class="text grey gt-ml-3" id="authored-time">{{TimeSince .Commit.Author.When ctx.Locale}}</span>
+					<span class="text grey tw-ml-2" id="authored-time">{{TimeSince .Commit.Author.When ctx.Locale}}</span>
 					{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
-						<span class="text grey gt-mx-3">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
+						<span class="text grey tw-mx-2">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
 						{{if ne .Verification.CommittingUser.ID 0}}
-							{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "gt-mx-3"}}
+							{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}}
 							<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
 						{{else}}
-							{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "gt-mr-3"}}
+							{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}}
 							<strong>{{.Commit.Committer.Name}}</strong>
 						{{end}}
 					{{end}}
@@ -184,73 +184,73 @@
 				</div>
 		</div>
 		{{if .Commit.Signature}}
-			<div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap gt-mb-0 {{$class}}">
+			<div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap tw-mb-0 {{$class}}">
 				<div class="tw-flex tw-items-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
-							{{svg "gitea-lock" 16 "gt-mr-3"}}
+							{{svg "gitea-lock" 16 "tw-mr-2"}}
 							{{if eq .Verification.TrustStatus "trusted"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
 							{{else if eq .Verification.TrustStatus "untrusted"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
 							{{else}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
 							{{end}}
-							{{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "gt-mr-3"}}
+							{{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}}
 							<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.GetDisplayName}}</strong></a>
 						{{else}}
-							<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "gt-mr-3"}}</span>
-							<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
-							{{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "gt-mr-3"}}
+							<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "tw-mr-2"}}</span>
+							<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
+							{{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}}
 							<strong>{{.Verification.SigningUser.GetDisplayName}}</strong>
 						{{end}}
 					{{else}}
-						{{svg "gitea-unlock" 16 "gt-mr-3"}}
+						{{svg "gitea-unlock" 16 "tw-mr-2"}}
 						<span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span>
 					{{end}}
 				</div>
 				<div class="tw-flex tw-items-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
-							{{svg "octicon-verified" 16 "gt-mr-3"}}
+							{{svg "octicon-verified" 16 "tw-mr-2"}}
 							{{if .Verification.SigningSSHKey}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 								{{.Verification.SigningSSHKey.Fingerprint}}
 							{{else}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 								{{.Verification.SigningKey.PaddedKeyID}}
 							{{end}}
 						{{else}}
-							{{svg "octicon-unverified" 16 "gt-mr-3"}}
+							{{svg "octicon-unverified" 16 "tw-mr-2"}}
 							{{if .Verification.SigningSSHKey}}
-								<span class="ui text gt-mr-3" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+								<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 								{{.Verification.SigningSSHKey.Fingerprint}}
 							{{else}}
-								<span class="ui text gt-mr-3" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+								<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 								{{.Verification.SigningKey.PaddedKeyID}}
 							{{end}}
 						{{end}}
 					{{else if .Verification.Warning}}
-						{{svg "octicon-unverified" 16 "gt-mr-3"}}
+						{{svg "octicon-unverified" 16 "tw-mr-2"}}
 						{{if .Verification.SigningSSHKey}}
-							<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+							<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 							{{.Verification.SigningSSHKey.Fingerprint}}
 						{{else}}
-							<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+							<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 							{{.Verification.SigningKey.PaddedKeyID}}
 						{{end}}
 					{{else}}
 						{{if .Verification.SigningKey}}
 							{{if ne .Verification.SigningKey.KeyID ""}}
-								{{svg "octicon-verified" 16 "gt-mr-3"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+								{{svg "octicon-verified" 16 "tw-mr-2"}}
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 								{{.Verification.SigningKey.PaddedKeyID}}
 							{{end}}
 						{{end}}
 						{{if .Verification.SigningSSHKey}}
 							{{if ne .Verification.SigningSSHKey.Fingerprint ""}}
-								{{svg "octicon-verified" 16 "gt-mr-3"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+								{{svg "octicon-verified" 16 "tw-mr-2"}}
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 								{{.Verification.SigningSSHKey.Fingerprint}}
 							{{end}}
 						{{end}}
@@ -260,7 +260,7 @@
 		{{end}}
 		{{if .NoteRendered}}
 			<div class="ui top attached header segment git-notes">
-				{{svg "octicon-note" 16 "gt-mr-3"}}
+				{{svg "octicon-note" 16 "tw-mr-2"}}
 				{{ctx.Locale.Tr "repo.diff.git-notes"}}:
 				{{if .NoteAuthor}}
 					<a href="{{.NoteAuthor.HomeLink}}">
diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl
index ea173da7a5..e6efe1ff54 100644
--- a/templates/repo/commits.tmpl
+++ b/templates/repo/commits.tmpl
@@ -5,7 +5,7 @@
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
 			<div class="tw-flex tw-items-center">
-				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
+				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
 				<a href="{{.RepoLink}}/graph" class="ui basic small compact button">
 					{{svg "octicon-git-branch"}}
 					{{ctx.Locale.Tr "repo.commit_graph"}}
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index 4eb31e0e8e..99787f715f 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -19,9 +19,9 @@
 								{{if .User.FullName}}
 									{{$userName = .User.FullName}}
 								{{end}}
-								{{ctx.AvatarUtils.Avatar .User 28 "gt-mr-2"}}<a href="{{.User.HomeLink}}">{{$userName}}</a>
+								{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-1"}}<a href="{{.User.HomeLink}}">{{$userName}}</a>
 							{{else}}
-								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "gt-mr-2"}}
+								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-1"}}
 								{{$userName}}
 							{{end}}
 						</td>
@@ -76,10 +76,10 @@
 						{{else}}
 							<td class="text right aligned">{{TimeSince .Author.When ctx.Locale}}</td>
 						{{end}}
-						<td class="text right aligned gt-py-0">
-							<button class="btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
+						<td class="text right aligned tw-py-0">
+							<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
 							<a
-								class="btn interact-bg gt-p-3"
+								class="btn interact-bg tw-p-2"
 								data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}"
 								href="{{if $.FileName}}{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}{{else}}{{printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}{{end}}">
 								{{svg "octicon-file-code"}}
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index b195f06483..cb867df65a 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -30,7 +30,7 @@
 					{{$class = (print $class " isWarning")}}
 				{{end}}
 			{{end}}
-			<a href="{{$commitLink}}" rel="nofollow" class="gt-ml-3 {{$class}}">
+			<a href="{{$commitLink}}" rel="nofollow" class="tw-ml-2 {{$class}}">
 				<span class="shortsha">{{ShortSha .ID.String}}</span>
 				{{if .Signature}}
 					{{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}}
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index 48e9368c65..91fc1c2fae 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -10,9 +10,9 @@
 	</div>
 	{{if .IsDiffCompare}}
 		<div class="commits-table-right tw-whitespace-nowrap">
-			<a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a>
+			<a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a>
 			...
-			<a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a>
+			<a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a>
 		</div>
 	{{end}}
 </h4>
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 9fd8593c53..a6ca314b3a 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -1,7 +1,7 @@
 {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
 <div>
 	<div class="diff-detail-box diff-box">
-		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 gt-ml-1">
+		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-ml-0.5">
 			{{if $showFileTree}}
 				<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
 					{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
@@ -19,13 +19,13 @@
 			{{end}}
 			{{if not .DiffNotAvailable}}
 				<div class="diff-detail-stats tw-flex tw-items-center tw-flex-wrap">
-					{{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}}
+					{{svg "octicon-diff" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}}
 				</div>
 			{{end}}
 		</div>
 		<div class="diff-detail-actions">
 			{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}}
-				<div class="not-mobile tw-flex tw-items-center tw-flex-col tw-whitespace-nowrap gt-mr-2">
+				<div class="not-mobile tw-flex tw-items-center tw-flex-col tw-whitespace-nowrap tw-mr-1">
 					<label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{ctx.Locale.Tr "repo.pulls.viewed_files_label"}}">
 						{{ctx.Locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
 					</label>
@@ -109,10 +109,10 @@
 					{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
 					{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
 					{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
-					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
+					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
 						<h4 class="diff-file-header sticky-2nd-row ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between tw-flex-wrap">
 							<div class="diff-file-name tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
-								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} tw-invisible{{end}}">
+								<button class="fold-file btn interact-bg tw-p-1{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
 										{{svg "octicon-chevron-right" 18}}
 									{{else}}
@@ -121,7 +121,7 @@
 								</button>
 								<div class="tw-font-semibold tw-flex tw-items-center gt-mono">
 									{{if $file.IsBin}}
-										<span class="gt-ml-1 gt-mr-3">
+										<span class="tw-ml-0.5 tw-mr-2">
 											{{ctx.Locale.Tr "repo.diff.bin"}}
 										</span>
 									{{else}}
@@ -129,7 +129,7 @@
 									{{end}}
 								</div>
 								<span class="file gt-mono"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}</span>
-								<button class="btn interact-fg gt-p-3" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
+								<button class="btn interact-fg tw-p-2" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
 								{{if $file.IsGenerated}}
 									<span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span>
 								{{end}}
@@ -139,9 +139,9 @@
 								{{if and $file.Mode $file.OldMode}}
 									{{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}}
 									{{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}
-									<span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
+									<span class="tw-ml-4 gt-mono">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
 								{{else if $file.Mode}}
-									<span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
+									<span class="tw-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
 								{{end}}
 							</div>
 							<div class="diff-file-header-actions tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
@@ -217,7 +217,7 @@
 				{{end}}
 
 				{{if .Diff.IsIncomplete}}
-					<div class="diff-file-box diff-box file-content gt-mt-3" id="diff-incomplete">
+					<div class="diff-file-box diff-box file-content tw-mt-2" id="diff-incomplete">
 						<h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between">
 							{{ctx.Locale.Tr "repo.diff.too_many_files"}}
 							<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl
index 6005ea28ef..d797e89444 100644
--- a/templates/repo/diff/comment_form.tmpl
+++ b/templates/repo/diff/comment_form.tmpl
@@ -25,7 +25,7 @@
 			</div>
 		{{end}}
 
-		<div class="field footer gt-mx-3">
+		<div class="field footer tw-mx-2">
 			<span class="markup-info">{{svg "octicon-markup"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
 			<div class="tw-text-right">
 				{{if $.reply}}
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 070fe92317..b03a9291c5 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -11,7 +11,7 @@
 		<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between">
 			<div class="comment-header-left tw-flex tw-items-center">
 				{{if .OriginalAuthor}}
-					<span class="text black tw-font-semibold gt-mr-2">
+					<span class="text black tw-font-semibold tw-mr-1">
 						{{svg (MigrationIcon $.root.Repository.GetOriginalURLHostname)}}
 						{{.OriginalAuthor}}
 					</span>
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index 773db40e18..ea9c0d471a 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -28,7 +28,7 @@
 		{{- end -}}
 	{{- end -}}
 	<div class="ui segment choose branch">
-		<a class="gt-mr-3" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
+		<a class="tw-mr-2" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
 		<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 			<div class="ui basic small button">
 				<span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_base"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_base"}}{{end}}: {{$BaseCompareName}}:{{$.BaseBranch}}</span>
@@ -44,12 +44,12 @@
 						<div class="two column row">
 							<a class="reference column" href="#" data-target=".base-branch-list">
 								<span class="text black">
-									{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}}
+									{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
 								</span>
 							</a>
 							<a class="reference column" href="#" data-target=".base-tag-list">
 								<span class="text black">
-									{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}}
+									{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
 								</span>
 							</a>
 						</div>
@@ -113,12 +113,12 @@
 						<div class="two column row">
 							<a class="reference column" href="#" data-target=".head-branch-list">
 								<span class="text black">
-									{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}}
+									{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
 								</span>
 							</a>
 							<a class="reference column" href="#" data-target=".head-tag-list">
 								<span class="text black">
-									{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}}
+									{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
 								</span>
 							</a>
 						</div>
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index e689deb1bf..872cbee78b 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -10,25 +10,25 @@
 		{{if $resolved}}
 			<div class="ui attached header resolved-placeholder tw-flex tw-items-center tw-justify-between">
 				<div class="ui grey text tw-flex tw-items-center tw-flex-wrap tw-gap-1">
-					{{svg "octicon-check" 16 "icon gt-mr-2"}}
+					{{svg "octicon-check" 16 "icon tw-mr-1"}}
 					<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
 					{{if $invalid}}
 						<!--
 						We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above).
 						The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl
 						-->
-						<a href="{{$referenceUrl}}" class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+						<a href="{{$referenceUrl}}" class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
 							{{ctx.Locale.Tr "repo.issues.review.outdated"}}
 						</a>
 					{{end}}
 				</div>
 				<div class="tw-flex tw-items-center tw-gap-2">
 					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-items-center">
-						{{svg "octicon-unfold" 16 "gt-mr-3"}}
+						{{svg "octicon-unfold" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
 					</button>
 					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-items-center gt-hidden">
-						{{svg "octicon-fold" 16 "gt-mr-3"}}
+						{{svg "octicon-fold" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
 					</button>
 				</div>
@@ -40,8 +40,8 @@
 					{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
 				</ui>
 			</div>
-			<div class="tw-flex tw-justify-end tw-items-center tw-flex-wrap gt-mt-3">
-				<div class="ui buttons gt-mr-2">
+			<div class="tw-flex tw-justify-end tw-items-center tw-flex-wrap tw-mt-2">
+				<div class="ui buttons tw-mr-1">
 					<button class="ui icon tiny basic button previous-conversation">
 						{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
 					</button>
@@ -59,8 +59,8 @@
 					</button>
 				{{end}}
 				{{if and $.SignedUserID (not $.Repository.IsArchived)}}
-					<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
-						{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+					<button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0">
+						{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
 					</button>
 				{{end}}
 			</div>
diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl
index 9c824db0ad..a2eae007a5 100644
--- a/templates/repo/diff/new_review.tmpl
+++ b/templates/repo/diff/new_review.tmpl
@@ -1,5 +1,5 @@
 <div id="review-box">
-	<button class="ui tiny primary button gt-pr-2 tw-flex js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}>
+	<button class="ui tiny primary button tw-pr-1 tw-flex js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}>
 		{{ctx.Locale.Tr "repo.diff.review"}}
 		<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl
index b7acb3d49b..d0dff1bd09 100644
--- a/templates/repo/diff/stats.tmpl
+++ b/templates/repo/diff/stats.tmpl
@@ -1,5 +1,5 @@
 {{Eval .file.Addition "+" .file.Deletion}}
-<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion}}">
+<span class="diff-stats-bar tw-mx-2" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion}}">
 	{{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
 	<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .file.Addition "/" "(" .file.Addition "+" .file.Deletion "+" 0.0 ")"}}%"></div>
 </span>
diff --git a/templates/repo/diff/whitespace_dropdown.tmpl b/templates/repo/diff/whitespace_dropdown.tmpl
index cfabf836d6..c54de165a4 100644
--- a/templates/repo/diff/whitespace_dropdown.tmpl
+++ b/templates/repo/diff/whitespace_dropdown.tmpl
@@ -3,25 +3,25 @@
 	<div class="menu">
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=show-all&show-outdated={{$.ShowOutdatedComments}}">
 			<label class="tw-pointer-events-none">
-				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}>
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_show_everything"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-all&show-outdated={{$.ShowOutdatedComments}}">
 			<label class="tw-pointer-events-none">
-				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}>
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_all_whitespace"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-change&show-outdated={{$.ShowOutdatedComments}}">
 			<label class="tw-pointer-events-none">
-				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}>
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_amount_changes"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-eol&show-outdated={{$.ShowOutdatedComments}}">
 			<label class="tw-pointer-events-none">
-				<input class="gt-mr-3 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}>
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_at_eol"}}
 			</label>
 		</a>
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index 94429452dd..56d96e0f37 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -60,7 +60,7 @@
 			<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}gt-hidden{{end}}">
 				<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
 					{{svg "octicon-git-branch"}}
-					<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast gt-mr-2 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
+					<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
 					<span class="text-muted js-quick-pull-normalization-info"></span>
 				</div>
 			</div>
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
index 8df8758988..1d919814c9 100644
--- a/templates/repo/editor/patch.tmpl
+++ b/templates/repo/editor/patch.tmpl
@@ -20,7 +20,7 @@
 			</div>
 			<div class="field">
 				<div class="ui top attached tabular menu" data-write="write">
-					<a class="active item" data-tab="write">{{svg "octicon-code" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
+					<a class="active item" data-tab="write">{{svg "octicon-code" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
 				</div>
 				<div class="ui bottom attached active tab segment" data-tab="write">
 					<textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-patch"
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index d3665a9f8b..cb2a5ba1e9 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -44,7 +44,7 @@
 						</div>
 
 						{{if not .Repository.IsArchived}}
-							<div class="divider gt-my-0"></div>
+							<div class="divider tw-my-0"></div>
 
 							<div class="item">
 								<h3>{{ctx.Locale.Tr "repo.create_new_repo_command"}}</h3>
diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl
index 33f0f87d61..0fb56a9a19 100644
--- a/templates/repo/file_info.tmpl
+++ b/templates/repo/file_info.tmpl
@@ -16,7 +16,7 @@
 	{{end}}
 	{{if .LFSLock}}
 		<div class="file-info-entry ui" data-tooltip-content="{{.LFSLockHint}}">
-			{{svg "octicon-lock" 16 "gt-mr-2"}}
+			{{svg "octicon-lock" 16 "tw-mr-1"}}
 			<a href="{{.LFSLockOwnerHomeLink}}">{{.LFSLockOwner}}</a>
 		</div>
 	{{end}}
diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl
index eebdcb2b1b..703f2eee2f 100644
--- a/templates/repo/find/files.tmpl
+++ b/templates/repo/find/files.tmpl
@@ -4,7 +4,7 @@
 	<div class="ui container">
 		<div class="tw-flex tw-items-center">
 			<a href="{{$.RepoLink}}">{{.RepoName}}</a>
-			<span class="gt-mx-3">/</span>
+			<span class="tw-mx-2">/</span>
 			<div class="ui input tw-flex-1">
 				<input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}">
 			</div>
@@ -13,7 +13,7 @@
 			<tbody>
 			</tbody>
 		</table>
-		<div id="repo-find-file-no-result" class="ui row center gt-mt-5 gt-hidden">
+		<div id="repo-find-file-no-result" class="ui row center tw-mt-8 gt-hidden">
 			<h3>{{ctx.Locale.Tr "repo.find_file.no_matching"}}</h3>
 		</div>
 	</div>
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
index 6acb89f367..412c59b60e 100644
--- a/templates/repo/forks.tmpl
+++ b/templates/repo/forks.tmpl
@@ -6,8 +6,8 @@
 			{{ctx.Locale.Tr "repo.forks"}}
 		</h2>
 		{{range .Forks}}
-			<div class="tw-flex tw-items-center gt-py-3">
-				<span class="gt-mr-2">{{ctx.AvatarUtils.Avatar .Owner}}</span>
+			<div class="tw-flex tw-items-center tw-py-2">
+				<span class="tw-mr-1">{{ctx.AvatarUtils.Avatar .Owner}}</span>
 				<a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a>
 			</div>
 		{{end}}
diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl
index 67804f117d..4b40233ac9 100644
--- a/templates/repo/graph.tmpl
+++ b/templates/repo/graph.tmpl
@@ -12,7 +12,7 @@
 						<div class="menu">
 							<div class="item" data-value="...flow-hide-pr-refs">
 								<span class="truncate">
-									{{svg "octicon-eye-closed" 16 "gt-mr-2"}}<span title="{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}">{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}</span>
+									{{svg "octicon-eye-closed" 16 "tw-mr-1"}}<span title="{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}">{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}</span>
 								</span>
 							</div>
 							{{range .AllRefs}}
@@ -20,33 +20,33 @@
 								{{if eq $refGroup "pull"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}<span title="{{.ShortName}}">#{{.ShortName}}</span>
+											{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}<span title="{{.ShortName}}">#{{.ShortName}}</span>
 										</span>
 									</div>
 								{{else if eq $refGroup "tags"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-tag" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
+											{{svg "octicon-tag" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
 										</span>
 									</div>
 								{{else if eq $refGroup "remotes"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-cross-reference" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
+											{{svg "octicon-cross-reference" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
 										</span>
 									</div>
 								{{else if eq $refGroup "heads"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-git-branch" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
+											{{svg "octicon-git-branch" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
 										</span>
 									</div>
 								{{end}}
 							{{end}}
 						</div>
 					</div>
-					<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button>
-					<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button>
+					<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button>
+					<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button>
 				</div>
 			</h2>
 			<div class="ui dividing"></div>
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index b22527c8ef..96d09072da 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -28,10 +28,10 @@
 							{{- end -}}
 						</a>
 					</span>
-					<span class="message tw-inline-block gt-ellipsis gt-mr-3">
+					<span class="message tw-inline-block gt-ellipsis tw-mr-2">
 						<span>{{RenderCommitMessage $.Context $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
 					</span>
-					<span class="commit-refs tw-flex tw-items-center gt-mr-2">
+					<span class="commit-refs tw-flex tw-items-center tw-mr-1">
 						{{range $commit.Refs}}
 							{{$refGroup := .RefGroup}}
 							{{if eq $refGroup "pull"}}
@@ -58,16 +58,16 @@
 							{{end}}
 						{{end}}
 					</span>
-					<span class="author tw-flex tw-items-center gt-mr-3">
+					<span class="author tw-flex tw-items-center tw-mr-2">
 						{{$userName := $commit.Commit.Author.Name}}
 						{{if $commit.User}}
 							{{if $commit.User.FullName}}
 								{{$userName = $commit.User.FullName}}
 							{{end}}
-							<span class="gt-mr-2">{{ctx.AvatarUtils.Avatar $commit.User}}</span>
+							<span class="tw-mr-1">{{ctx.AvatarUtils.Avatar $commit.User}}</span>
 							<a href="{{$commit.User.HomeLink}}">{{$userName}}</a>
 						{{else}}
-							<span class="gt-mr-2">{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName}}</span>
+							<span class="tw-mr-1">{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName}}</span>
 							{{$userName}}
 						{{end}}
 					</span>
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 002d06c23a..5e2774dfa1 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -98,8 +98,8 @@
 								<div class="content tw-text-left">
 									<div class="ui list">
 										{{range $.UserAndOrgForks}}
-											<div class="ui item gt-py-3">
-												<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
+											<div class="ui item tw-py-2">
+												<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "tw-mr-2"}}{{.FullName}}</a>
 											</div>
 										{{end}}
 									</div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 2463c768fd..2418b21b69 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -19,13 +19,13 @@
 			</form>
 		</div>
 		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics">
-			{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
+			{{range .Topics}}<a class="ui repo-topic large label topic tw-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
 			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
 		</div>
 		{{end}}
 		{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-		<div class="ui form gt-hidden tw-flex tw-flex-col gt-mt-4" id="topic_edit">
-			<div class="field tw-flex-1 gt-mb-2">
+		<div class="ui form gt-hidden tw-flex tw-flex-col tw-mt-4" id="topic_edit">
+			<div class="field tw-flex-1 tw-mb-1">
 				<div class="ui fluid multiple search selection dropdown tw-flex-wrap" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
 					<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
 					{{range .Topics}}
@@ -53,7 +53,7 @@
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
 			<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-y-2">
-				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
+				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
 				{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 					{{$cmpBranch := ""}}
 					{{if ne .Repository.ID .BaseRepo.ID}}
@@ -74,7 +74,7 @@
 				{{end}}
 
 				{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
-					<button class="ui dropdown basic compact jump button gt-mr-2"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
+					<button class="ui dropdown basic compact jump button tw-mr-1"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
 						{{ctx.Locale.Tr "repo.editor.add_file"}}
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						<div class="menu">
@@ -99,7 +99,7 @@
 					</a>
 				{{end}}
 				{{if ne $n 0}}
-					<span class="breadcrumb repo-path gt-ml-2">
+					<span class="breadcrumb repo-path tw-ml-1">
 						<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
 						{{- range $i, $v := .TreeNames -}}
 							<span class="breadcrumb-divider">/</span>
@@ -121,12 +121,12 @@
 							{{svg "octicon-kebab-horizontal"}}
 							<div class="menu">
 								{{if not $.DisableDownloadSourceArchives}}
-									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
-									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
-									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
 								{{end}}
 								{{if .CitiationExist}}
-									<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
+									<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
 								{{end}}
 								{{range .OpenWithEditorApps}}
 									<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
@@ -139,7 +139,7 @@
 				{{end}}
 				{{if and (ne $n 0) (not .IsViewFile) (not .IsBlame)}}
 					<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
-						{{svg "octicon-history" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.file_history"}}
+						{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
 					</a>
 				{{end}}
 			</div>
diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl
index 4160e47465..f182b909d6 100644
--- a/templates/repo/issue/branch_selector_field.tmpl
+++ b/templates/repo/issue/branch_selector_field.tmpl
@@ -20,12 +20,12 @@
 				<div class="two column row">
 					<a class="reference column muted" href="#" data-target="#branch-list">
 						<span class="text black">
-							{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}}
+							{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
 						</span>
 					</a>
 					<a class="reference column muted" href="#" data-target="#tag-list">
 						<span class="text">
-							{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}}
+							{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
 						</span>
 					</a>
 				</div>
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index d25a36c456..bb9340bb2e 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -6,7 +6,7 @@
 			{{end}}
 		</div>
 	{{end}}
-	<div class="content gt-p-0 tw-w-full">
+	<div class="content tw-p-0 tw-w-full">
 		<div class="tw-flex tw-items-start">
 			<div class="issue-card-icon">
 				{{template "shared/issueicon" .}}
@@ -18,7 +18,7 @@
 				</a>
 			{{end}}
 		</div>
-		<div class="meta gt-my-2">
+		<div class="meta tw-my-1">
 			<span class="text light grey muted-links">
 				{{if not $.Page.Repository}}{{.Repo.FullName}}{{end}}#{{.Index}}
 				{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
@@ -32,18 +32,18 @@
 			</span>
 		</div>
 		{{if .MilestoneID}}
-		<div class="meta gt-my-2">
+		<div class="meta tw-my-1">
 			<a class="milestone" href="{{.Repo.Link}}/milestone/{{.MilestoneID}}">
-				{{svg "octicon-milestone" 16 "gt-mr-2 tw-align-middle"}}
+				{{svg "octicon-milestone" 16 "tw-mr-1 tw-align-middle"}}
 				<span class="tw-align-middle">{{.Milestone.Name}}</span>
 			</a>
 		</div>
 		{{end}}
 		{{if $.Page.LinkedPRs}}
 		{{range index $.Page.LinkedPRs .ID}}
-		<div class="meta gt-my-2">
+		<div class="meta tw-my-1">
 			<a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}">
-				<span class="gt-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "gt-mr-2 tw-align-middle"}}</span>
+				<span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span>
 				<span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
 			</a>
 		</div>
@@ -51,21 +51,21 @@
 		{{end}}
 		{{$tasks := .GetTasks}}
 		{{if gt $tasks 0}}
-			<div class="meta gt-my-2">
-				{{svg "octicon-checklist" 16 "gt-mr-2 tw-align-middle"}}
+			<div class="meta tw-my-1">
+				{{svg "octicon-checklist" 16 "tw-mr-1 tw-align-middle"}}
 				<span class="tw-align-middle">{{.GetTasksDone}} / {{$tasks}}</span>
 			</div>
 		{{end}}
 	</div>
 
 	{{if or .Labels .Assignees}}
-	<div class="extra content labels-list gt-p-0 gt-pt-2">
+	<div class="extra content labels-list tw-p-0 tw-pt-1">
 		{{range .Labels}}
 			<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a>
 		{{end}}
 		<div class="right floated">
 			{{range .Assignees}}
-				<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28 "mini gt-mr-3"}}</a>
+				<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28 "mini tw-mr-2"}}</a>
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/repo/issue/fields/checkboxes.tmpl b/templates/repo/issue/fields/checkboxes.tmpl
index b928b2be58..5d98605983 100644
--- a/templates/repo/issue/fields/checkboxes.tmpl
+++ b/templates/repo/issue/fields/checkboxes.tmpl
@@ -2,7 +2,7 @@
 	{{template "repo/issue/fields/header" .}}
 	{{range $i, $opt := .item.Attributes.options}}
 		<div class="field inline">
-			<div class="ui checkbox gt-mr-0 {{if and ($opt.visible) (not (SliceUtils.Contains $opt.visible "form"))}}gt-hidden{{end}}">
+			<div class="ui checkbox tw-mr-0 {{if and ($opt.visible) (not (SliceUtils.Contains $opt.visible "form"))}}gt-hidden{{end}}">
 				<input type="checkbox" name="form-field-{{$.item.ID}}-{{$i}}" {{if $opt.required}}required{{end}}>
 				<label>{{RenderMarkdownToHtml $.context $opt.label}}</label>
 			</div>
diff --git a/templates/repo/issue/fields/textarea.tmpl b/templates/repo/issue/fields/textarea.tmpl
index 4f68b4038b..831cea01d5 100644
--- a/templates/repo/issue/fields/textarea.tmpl
+++ b/templates/repo/issue/fields/textarea.tmpl
@@ -16,7 +16,7 @@
 		)}}
 
 		{{if .root.IsAttachmentEnabled}}
-		<div class="gt-mt-4 form-field-dropzone gt-hidden">
+		<div class="tw-mt-4 form-field-dropzone gt-hidden">
 			{{template "repo/upload" .root}}
 		</div>
 		{{end}}
diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl
index 9c259fe0e1..f23ca36d78 100644
--- a/templates/repo/issue/filter_actions.tmpl
+++ b/templates/repo/issue/filter_actions.tmpl
@@ -85,7 +85,7 @@
 					</div>
 					{{range .OpenProjects}}
 						<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/projects">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</div>
 					{{end}}
 				{{end}}
@@ -96,7 +96,7 @@
 					</div>
 					{{range .ClosedProjects}}
 						<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/projects">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</div>
 					{{end}}
 				{{end}}
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index f1fc61bccb..c6de4977dc 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -16,7 +16,7 @@
 			>
 			<label for="archived-filter-checkbox">
 				{{ctx.Locale.Tr "repo.issues.label_archived_filter"}}
-				<i class="gt-ml-2" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
+				<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
 					{{svg "octicon-info"}}
 				</i>
 			</label>
@@ -108,7 +108,7 @@
 			</div>
 			{{range .OpenProjects}}
 				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
-					{{svg .IconName 18 "gt-mr-3 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
+					{{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
 				</a>
 			{{end}}
 		{{end}}
@@ -119,7 +119,7 @@
 			</div>
 			{{range .ClosedProjects}}
 				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
-					{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+					{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 				</a>
 			{{end}}
 		{{end}}
diff --git a/templates/repo/issue/filters.tmpl b/templates/repo/issue/filters.tmpl
index 56c65e2401..06e7c1aa6c 100644
--- a/templates/repo/issue/filters.tmpl
+++ b/templates/repo/issue/filters.tmpl
@@ -1,7 +1,7 @@
 <div id="issue-filters" class="issue-list-toolbar">
 	<div class="issue-list-toolbar-left">
 		{{if and $.CanWriteIssuesOrPulls .Issues}}
-			<input type="checkbox" autocomplete="off" class="issue-checkbox-all gt-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
+			<input type="checkbox" autocomplete="off" class="issue-checkbox-all tw-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
 		{{end}}
 		{{template "repo/issue/openclose" .}}
 		<!-- Total Tracked Time -->
diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl
index 86e4bae0f7..6dc7e4ef64 100644
--- a/templates/repo/issue/labels.tmpl
+++ b/templates/repo/issue/labels.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository labels">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="navbar gt-mb-4">
+		<div class="navbar tw-mb-4">
 			{{template "repo/issue/navbar" .}}
 			{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
 				<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index 7ddc38a387..526bc760a2 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -30,7 +30,7 @@
 				</div>
 				<br>
 				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
-				<div class="desc gt-ml-2 gt-mt-3 gt-hidden label-exclusive-warning">
+				<div class="desc tw-ml-1 tw-mt-2 gt-hidden label-exclusive-warning">
 					{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}
 				</div>
 				<br>
@@ -40,7 +40,7 @@
 					<input class="label-is-archived-input" name="is_archived" type="checkbox">
 					<label>{{ctx.Locale.Tr "repo.issues.label_archive"}}</label>
 				</div>
-				<i class="gt-ml-2" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
+				<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
 					{{svg "octicon-info"}}
 				</i>
 			</div>
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index 86d08e5f75..d84f14242a 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -3,7 +3,7 @@
 	<div class="ui right">
 		<div class="ui secondary menu">
 			<!-- Sort -->
-			<div class="item ui jump dropdown gt-py-3">
+			<div class="item ui jump dropdown tw-py-2">
 				<span class="text">
 					{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 				</span>
diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl
index a9c33bc4eb..e5f15caca5 100644
--- a/templates/repo/issue/labels/labels_selector_field.tmpl
+++ b/templates/repo/issue/labels/labels_selector_field.tmpl
@@ -2,7 +2,7 @@
 	<span class="text muted flex-text-block">
 		<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong>
 		{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-			{{svg "octicon-gear" 16 "gt-ml-2"}}
+			{{svg "octicon-gear" 16 "tw-ml-1"}}
 		{{end}}
 	</span>
 	<div class="filter menu" {{if .Issue}}data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels"{{else}}data-id="#label_ids"{{end}}>
diff --git a/templates/repo/issue/milestone/select_menu.tmpl b/templates/repo/issue/milestone/select_menu.tmpl
index 6f8c6c85c2..9b0492ce52 100644
--- a/templates/repo/issue/milestone/select_menu.tmpl
+++ b/templates/repo/issue/milestone/select_menu.tmpl
@@ -18,7 +18,7 @@
 		</div>
 		{{range .OpenMilestones}}
 			<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-				{{svg "octicon-milestone" 16 "gt-mr-2"}}
+				{{svg "octicon-milestone" 16 "tw-mr-1"}}
 				{{.Name}}
 			</a>
 		{{end}}
@@ -30,7 +30,7 @@
 		</div>
 		{{range .ClosedMilestones}}
 			<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-				{{svg "octicon-milestone" 16 "gt-mr-2"}}
+				{{svg "octicon-milestone" 16 "tw-mr-1"}}
 				{{.Name}}
 			</a>
 		{{end}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index 8ba7eecf4d..5bae6fc6d5 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -4,7 +4,7 @@
 	<div class="ui container">
 		{{template "base/alert" .}}
 		<div class="tw-flex">
-			<h1 class="gt-mb-3">{{.Milestone.Name}}</h1>
+			<h1 class="tw-mb-2">{{.Milestone.Name}}</h1>
 			{{if not .Repository.IsArchived}}
 				<div class="text right tw-flex-1">
 					{{if or .CanWriteIssues .CanWritePulls}}
@@ -22,7 +22,7 @@
 			{{end}}
 		</div>
 		{{if .Milestone.RenderedContent}}
-		<div class="markup content gt-mb-4">
+		<div class="markup content tw-mb-4">
 				{{.Milestone.RenderedContent}}
 		</div>
 		{{end}}
@@ -46,7 +46,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}</div>
+				<div class="tw-mr-2">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}</div>
 				{{if .TotalTrackedTime}}
 					<div data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
 						{{svg "octicon-clock"}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 57b697d8fd..bce7ad8717 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -19,12 +19,12 @@
 			{{range .Milestones}}
 				<li class="milestone-card">
 					<div class="milestone-header">
-						<h3 class="flex-text-block gt-m-0">
+						<h3 class="flex-text-block tw-m-0">
 							{{svg "octicon-milestone" 16}}
 							<a class="muted" href="{{$.RepoLink}}/milestone/{{.ID}}">{{.Name}}</a>
 						</h3>
 						<div class="tw-flex tw-items-center">
-							<span class="gt-mr-3">{{.Completeness}}%</span>
+							<span class="tw-mr-2">{{.Completeness}}%</span>
 							<progress value="{{.Completeness}}" max="100"></progress>
 						</div>
 					</div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index ba1e19bf07..7c73bd182b 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -7,7 +7,7 @@
 		<div class="ui comments">
 			<div class="comment">
 				{{ctx.AvatarUtils.Avatar .SignedUser 40}}
-				<div class="ui segment content gt-my-0">
+				<div class="ui segment content tw-my-0">
 					<div class="field">
 						<input name="title" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" autofocus required maxlength="255" autocomplete="off">
 						{{if .PageIsComparePull}}
@@ -60,7 +60,7 @@
 			<span class="text flex-text-block">
 				<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
 				{{if .HasIssuesOrPullsWritePermission}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</span>
 			<div class="menu">
@@ -72,7 +72,7 @@
 			<div class="selected">
 				{{if .Milestone}}
 					<a class="item muted sidebar-item-link" href="{{.RepoLink}}/issues?milestone={{.Milestone.ID}}">
-						{{svg "octicon-milestone" 18 "gt-mr-3"}}
+						{{svg "octicon-milestone" 18 "tw-mr-2"}}
 						{{.Milestone.Name}}
 					</a>
 				{{end}}
@@ -87,7 +87,7 @@
 			<span class="text flex-text-block">
 				<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
 				{{if .HasIssuesOrPullsWritePermission}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</span>
 			<div class="menu">
@@ -110,7 +110,7 @@
 						</div>
 						{{range .OpenProjects}}
 							<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-								{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+								{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 							</a>
 						{{end}}
 					{{end}}
@@ -121,7 +121,7 @@
 						</div>
 						{{range .ClosedProjects}}
 							<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-								{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+								{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 							</a>
 						{{end}}
 					{{end}}
@@ -133,7 +133,7 @@
 			<div class="selected">
 				{{if .Project}}
 					<a class="item muted sidebar-item-link" href="{{.Project.Link ctx}}">
-						{{svg .Project.IconName 18 "gt-mr-3"}}{{.Project.Title}}
+						{{svg .Project.IconName 18 "tw-mr-2"}}{{.Project.Title}}
 					</a>
 				{{end}}
 			</div>
@@ -145,7 +145,7 @@
 				<span class="text flex-text-block">
 					<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
 					{{if .HasIssuesOrPullsWritePermission}}
-						{{svg "octicon-gear" 16 "gt-ml-2"}}
+						{{svg "octicon-gear" 16 "tw-ml-1"}}
 					{{end}}
 				</span>
 				<div class="filter menu" data-id="#assignee_ids">
@@ -158,7 +158,7 @@
 						<a class="item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
 							<span class="octicon-check tw-invisible">{{svg "octicon-check"}}</span>
 							<span class="text">
-								{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3"}}{{template "repo/search_name" .}}
+								{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{template "repo/search_name" .}}
 							</span>
 						</a>
 					{{end}}
@@ -170,8 +170,8 @@
 				</span>
 				<div class="selected">
 				{{range .Assignees}}
-					<a class="item gt-p-2 muted gt-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
-						{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3 tw-align-middle"}}{{.GetDisplayName}}
+					<a class="item tw-p-1 muted gt-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
+						{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}}
 					</a>
 				{{end}}
 				</div>
diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl
index 38848c51ac..eb2d6e09ee 100644
--- a/templates/repo/issue/openclose.tmpl
+++ b/templates/repo/issue/openclose.tmpl
@@ -1,16 +1,16 @@
 <div class="small-menu-items ui compact tiny menu">
 	<a class="{{if eq .State "open"}}active {{end}}item" href="{{if eq .State "open"}}{{.AllStatesLink}}{{else}}{{.OpenLink}}{{end}}">
 		{{if .PageIsMilestones}}
-			{{svg "octicon-milestone" 16 "gt-mr-3"}}
+			{{svg "octicon-milestone" 16 "tw-mr-2"}}
 		{{else if .PageIsPullList}}
-			{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
+			{{svg "octicon-git-pull-request" 16 "tw-mr-2"}}
 		{{else}}
-			{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+			{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 		{{end}}
 		{{ctx.Locale.PrettyNumber .OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 	</a>
 	<a class="{{if eq .State "closed"}}active {{end}}item" href="{{if eq .State "closed"}}{{.AllStatesLink}}{{else}}{{.ClosedLink}}{{end}}">
-		{{svg "octicon-check" 16 "gt-mr-3"}}
+		{{svg "octicon-check" 16 "tw-mr-2"}}
 		{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 	</a>
 </div>
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
index 0635a201be..2155f78656 100644
--- a/templates/repo/issue/view_content/attachments.tmpl
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -5,7 +5,7 @@
 	{{$hasThumbnails := false}}
 	{{- range .Attachments -}}
 		<div class="tw-flex">
-			<div class="tw-flex-1 gt-p-3">
+			<div class="tw-flex-1 tw-p-2">
 				<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					{{if FilenameIsImage .Name}}
 						{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
@@ -18,7 +18,7 @@
 					<span><strong>{{.Name}}</strong></span>
 				</a>
 			</div>
-			<div class="gt-p-3 tw-flex tw-items-center">
+			<div class="tw-p-2 tw-flex tw-items-center">
 				<span class="ui text grey">{{.Size | FileSize}}</span>
 			</div>
 		</div>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index b137dd0a9c..dbfe016e89 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -28,7 +28,7 @@
 					<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3">
 						<div class="comment-header-left tw-flex tw-items-center">
 							{{if .OriginalAuthor}}
-								<span class="text black tw-font-semibold gt-mr-2">
+								<span class="text black tw-font-semibold tw-mr-1">
 									{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 									{{.OriginalAuthor}}
 								</span>
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index c0ffec36c3..1a282968d7 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -6,11 +6,11 @@
 	{{$hasReview := and $comment.Review}}
 	{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
 	<div class="ui segments conversation-holder">
-		<div class="ui segment collapsible-comment-box gt-py-3 tw-flex tw-items-center tw-justify-between">
+		<div class="ui segment collapsible-comment-box tw-py-2 tw-flex tw-items-center tw-justify-between">
 			<div class="tw-flex tw-items-center">
-				<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment gt-ml-3 gt-word-break">{{$comment.TreePath}}</a>
+				<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment tw-ml-2 gt-word-break">{{$comment.TreePath}}</a>
 				{{if $invalid}}
-					<span class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+					<span class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
 						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
 					</span>
 				{{end}}
@@ -18,7 +18,7 @@
 			<div>
 				{{if or $invalid $resolved}}
 					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-items-center">
-						{{svg "octicon-unfold" 16 "gt-mr-3"}}
+						{{svg "octicon-unfold" 16 "tw-mr-2"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
 						{{else}}
@@ -26,7 +26,7 @@
 						{{end}}
 					</button>
 					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-items-center">
-						{{svg "octicon-fold" 16 "gt-mr-3"}}
+						{{svg "octicon-fold" 16 "tw-mr-2"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
 						{{else}}
@@ -52,10 +52,10 @@
 			</div>
 		{{end}}
 		<div id="code-comments-{{$comment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} gt-hidden{{end}}">
-			<div class="ui comments gt-mb-0">
+			<div class="ui comments tw-mb-0">
 				{{range .comments}}
 					{{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}}
-					<div class="comment code-comment gt-pb-4" id="{{.HashTag}}">
+					<div class="comment code-comment tw-pb-4" id="{{.HashTag}}">
 						<div class="content">
 							<div class="header comment-header">
 								<div class="comment-header-left tw-flex tw-items-center">
@@ -109,11 +109,11 @@
 					</div>
 				{{end}}
 			</div>
-			<div class="code-comment-buttons tw-flex tw-items-center tw-flex-wrap gt-mt-3 gt-mb-2 gt-mx-3">
+			<div class="code-comment-buttons tw-flex tw-items-center tw-flex-wrap tw-mt-2 tw-mb-1 tw-mx-2">
 				<div class="tw-flex-1">
 					{{if $resolved}}
 						<div class="ui grey text">
-							{{svg "octicon-check" 16 "gt-mr-2"}}
+							{{svg "octicon-check" 16 "tw-mr-1"}}
 							<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
 						</div>
 					{{end}}
@@ -129,8 +129,8 @@
 						</button>
 					{{end}}
 					{{if and $.SignedUserID (not $.Repository.IsArchived)}}
-						<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
-							{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+						<button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0">
+							{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
 						</button>
 					{{end}}
 				</div>
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 9fafeb5ee3..25e9bbcfc2 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -35,7 +35,7 @@
 				{{if .IsPullBranchDeletable}}
 					<div class="item item-section text tw-flex-1">
 						<div class="item-section-left">
-							<h3 class="gt-mb-3">
+							<h3 class="tw-mb-2">
 								{{ctx.Locale.Tr "repo.pulls.merged_success"}}
 							</h3>
 							<div class="merge-section-info">
@@ -50,7 +50,7 @@
 			{{else if .Issue.IsClosed}}
 				<div class="item item-section text tw-flex-1">
 					<div class="item-section-left">
-						<h3 class="gt-mb-3">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3>
+						<h3 class="tw-mb-2">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3>
 						<div class="merge-section-info">
 							{{if .IsPullRequestBroken}}
 								{{ctx.Locale.Tr "repo.pulls.cant_reopen_deleted_branch"}}
diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
index 12b0c4b4e0..4ff38950cd 100644
--- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl
+++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
@@ -1,6 +1,6 @@
 <div class="divider"></div>
 <div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}} </div>
-<div class="instruct-content gt-mt-3 gt-hidden">
+<div class="instruct-content tw-mt-2 gt-hidden">
 	<div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}</div>
 	{{$localBranch := .PullRequest.HeadBranch}}
 	{{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 99ea699f4a..324175e6cf 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -6,7 +6,7 @@
 			<a class="text tw-flex tw-items-center muted">
 				<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
 				{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</a>
 			<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/request_review">
@@ -22,7 +22,7 @@
 							<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_{{.ItemID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
 								<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
 								<span class="text">
-									{{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}}
+									{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{template "repo/search_name" .User}}
 								</span>
 							</a>
 						{{end}}
@@ -37,7 +37,7 @@
 							<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
 								<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span>
 								<span class="text">
-									{{svg "octicon-people" 16 "gt-ml-4 gt-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
+									{{svg "octicon-people" 16 "tw-ml-4 tw-mr-1"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
 								</span>
 							</a>
 						{{end}}
@@ -50,12 +50,12 @@
 			<span class="no-select item {{if or .OriginalReviews .PullReviewers}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
 			<div class="selected">
 				{{range .PullReviewers}}
-					<div class="item tw-flex tw-items-center gt-py-3">
+					<div class="item tw-flex tw-items-center tw-py-2">
 						<div class="tw-flex tw-items-center tw-flex-1">
 							{{if .User}}
-								<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "gt-mr-3"}}{{.User.GetDisplayName}}</a>
+								<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "tw-mr-2"}}{{.User.GetDisplayName}}</a>
 							{{else if .Team}}
-								<span class="text">{{svg "octicon-people" 20 "gt-mr-3"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
+								<span class="text">{{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
 							{{end}}
 						</div>
 						<div class="tw-flex tw-items-center tw-gap-2">
@@ -99,10 +99,10 @@
 					</div>
 				{{end}}
 				{{range .OriginalReviews}}
-					<div class="item tw-flex tw-items-center gt-py-3">
+					<div class="item tw-flex tw-items-center tw-py-2">
 						<div class="tw-flex tw-items-center tw-flex-1">
 							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
-								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "gt-mr-3"}}
+								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "tw-mr-2"}}
 								{{.OriginalAuthor}}
 							</a>
 						</div>
@@ -132,7 +132,7 @@
 		<a class="text muted flex-text-block">
 			<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
 			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-				{{svg "octicon-gear" 16 "gt-ml-2"}}
+				{{svg "octicon-gear" 16 "tw-ml-1"}}
 			{{end}}
 		</a>
 		<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
@@ -144,7 +144,7 @@
 		<div class="selected">
 			{{if .Issue.Milestone}}
 				<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
-					{{svg "octicon-milestone" 18 "gt-mr-3"}}
+					{{svg "octicon-milestone" 18 "tw-mr-2"}}
 					{{.Issue.Milestone.Name}}
 				</a>
 			{{end}}
@@ -158,7 +158,7 @@
 			<a class="text muted flex-text-block">
 				<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
 				{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</a>
 			<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/projects">
@@ -176,7 +176,7 @@
 					</div>
 					{{range .OpenProjects}}
 						<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</a>
 					{{end}}
 				{{end}}
@@ -187,7 +187,7 @@
 					</div>
 					{{range .ClosedProjects}}
 						<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</a>
 					{{end}}
 				{{end}}
@@ -198,7 +198,7 @@
 			<div class="selected">
 				{{if .Issue.Project}}
 					<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
-						{{svg .Issue.Project.IconName 18 "gt-mr-3"}}{{.Issue.Project.Title}}
+						{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
 					</a>
 				{{end}}
 			</div>
@@ -212,7 +212,7 @@
 		<a class="text muted flex-text-block">
 			<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
 			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-				{{svg "octicon-gear" 16 "gt-ml-2"}}
+				{{svg "octicon-gear" 16 "tw-ml-1"}}
 			{{end}}
 		</a>
 		<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
@@ -233,7 +233,7 @@
 					{{end}}
 					<span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
 					<span class="text">
-						{{ctx.AvatarUtils.Avatar . 20 "gt-mr-3"}}{{template "repo/search_name" .}}
+						{{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}}
 					</span>
 				</a>
 			{{end}}
@@ -245,7 +245,7 @@
 			{{range .Issue.Assignees}}
 				<div class="item">
 					<a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
-						{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3"}}
+						{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
 						{{.GetDisplayName}}
 					</a>
 				</div>
@@ -260,7 +260,7 @@
 		<div class="ui list tw-flex tw-flex-wrap">
 			{{range .Participants}}
 				<a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}">
-					{{ctx.AvatarUtils.Avatar . 28 "gt-my-1 gt-mr-2"}}
+					{{ctx.AvatarUtils.Avatar . 28 "tw-my-0.5 tw-mr-1"}}
 				</a>
 			{{end}}
 		</div>
@@ -271,7 +271,7 @@
 
 		<div class="ui watching">
 			<span class="text"><strong>{{ctx.Locale.Tr "notification.notifications"}}</strong></span>
-			<div class="gt-mt-3">
+			<div class="tw-mt-2">
 				{{template "repo/issue/view_content/watching" .}}
 			</div>
 		</div>
@@ -281,7 +281,7 @@
 			<div class="divider"></div>
 			<div class="ui timetrack">
 				<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span>
-				<div class="gt-mt-3">
+				<div class="tw-mt-2">
 					<form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form">
 						{{$.CsrfTokenHtml}}
 					</form>
@@ -290,11 +290,11 @@
 					</form>
 					{{if $.IsStopwatchRunning}}
 						<button class="ui fluid button issue-stop-time">
-							{{svg "octicon-stopwatch" 16 "gt-mr-3"}}
+							{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.stop_tracking"}}
 						</button>
-						<button class="ui fluid button issue-cancel-time gt-mt-3">
-							{{svg "octicon-trash" 16 "gt-mr-3"}}
+						<button class="ui fluid button issue-cancel-time tw-mt-2">
+							{{svg "octicon-trash" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}
 						</button>
 					{{else}}
@@ -304,7 +304,7 @@
 							</div>
 						{{end}}
 						<button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'>
-							{{svg "octicon-stopwatch" 16 "gt-mr-3"}}
+							{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.start_tracking_short"}}
 						</button>
 						<div class="ui mini modal issue-start-time-modal">
@@ -321,8 +321,8 @@
 								<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.add_time_cancel"}}</button>
 							</div>
 						</div>
-						<button class="ui fluid button issue-add-time gt-mt-3" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'>
-							{{svg "octicon-plus" 16 "gt-mr-3"}}
+						<button class="ui fluid button issue-add-time tw-mt-2" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'>
+							{{svg "octicon-plus" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.add_time_short"}}
 						</button>
 					{{end}}
@@ -335,7 +335,7 @@
 				<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
 				<div>
 					{{range $user, $trackedtime := .WorkingUsers}}
-						<div class="comment gt-mt-3">
+						<div class="comment tw-mt-2">
 							<a class="avatar">
 								{{ctx.AvatarUtils.Avatar $user}}
 							</a>
@@ -363,12 +363,12 @@
 			<p>
 				<div class="tw-flex tw-justify-between tw-items-center">
 					<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
-						{{svg "octicon-calendar" 16 "gt-mr-3"}}
+						{{svg "octicon-calendar" 16 "tw-mr-2"}}
 						{{DateTime "long" .Issue.DeadlineUnix.FormatDate}}
 					</div>
 					<div>
 						{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-							<a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil" 16 "gt-mr-2"}}</a>
+							<a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil" 16 "tw-mr-1"}}</a>
 							<a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a>
 						{{end}}
 					</div>
@@ -426,7 +426,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right tw-flex tw-items-center gt-m-2">
+							<div class="item-right tw-flex tw-items-center tw-m-1">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -458,7 +458,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right tw-flex tw-items-center gt-m-2">
+							<div class="item-right tw-flex tw-items-center tw-m-1">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -481,7 +481,7 @@
 										{{.Repository.OwnerName}}/{{.Repository.Name}}
 									</div>
 								</div>
-								<div class="item-right tw-flex tw-items-center gt-m-2">
+								<div class="item-right tw-flex tw-items-center tw-m-1">
 									{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 										<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 											{{svg "octicon-trash" 16}}
@@ -550,7 +550,7 @@
 		{{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
 		<div class="row tw-items-center" data-tooltip-content="{{$issueReferenceLink}}">
 			<span class="text column truncate">{{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}</span>
-			<button class="ui two wide button column gt-p-3" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
+			<button class="ui two wide button column tw-p-2" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
 		</div>
 	</div>
 
@@ -558,21 +558,21 @@
 		<div class="divider"></div>
 
 		{{if or .PinEnabled .Issue.IsPinned}}
-			<form class="gt-mt-2 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}>
+			<form class="tw-mt-1 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}>
 				{{$.CsrfTokenHtml}}
 				<button class="fluid ui button {{if not $.NewPinAllowed}}disabled{{end}}">
 					{{if not .Issue.IsPinned}}
-						{{svg "octicon-pin" 16 "gt-mr-3"}}
+						{{svg "octicon-pin" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "pin"}}
 					{{else}}
-						{{svg "octicon-pin-slash" 16 "gt-mr-3"}}
+						{{svg "octicon-pin-slash" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "unpin"}}
 					{{end}}
 				</button>
 			</form>
 		{{end}}
 
-		<button class="gt-mt-2 fluid ui show-modal button {{if .Issue.IsLocked}} negative {{end}}" data-modal="#lock">
+		<button class="tw-mt-1 fluid ui show-modal button {{if .Issue.IsLocked}} negative {{end}}" data-modal="#lock">
 			{{if .Issue.IsLocked}}
 				{{svg "octicon-key"}}
 				{{ctx.Locale.Tr "repo.issues.unlock"}}
@@ -645,7 +645,7 @@
 				</form>
 			</div>
 		</div>
-		<button class="gt-mt-2 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
+		<button class="tw-mt-1 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
 			{{svg "octicon-trash"}}
 			{{ctx.Locale.Tr "repo.issues.delete"}}
 		</button>
diff --git a/templates/repo/issue/view_content/watching.tmpl b/templates/repo/issue/view_content/watching.tmpl
index 0e8562fed2..05936d090b 100644
--- a/templates/repo/issue/view_content/watching.tmpl
+++ b/templates/repo/issue/view_content/watching.tmpl
@@ -2,10 +2,10 @@
 	<input type="hidden" name="watch" value="{{if $.IssueWatch.IsWatching}}0{{else}}1{{end}}">
 	<button class="fluid ui button">
 		{{if $.IssueWatch.IsWatching}}
-			{{svg "octicon-mute" 16 "gt-mr-3"}}
+			{{svg "octicon-mute" 16 "tw-mr-2"}}
 			{{ctx.Locale.Tr "repo.issues.unsubscribe"}}
 		{{else}}
-			{{svg "octicon-unmute" 16 "gt-mr-3"}}
+			{{svg "octicon-unmute" 16 "tw-mr-2"}}
 			{{ctx.Locale.Tr "repo.issues.subscribe"}}
 		{{end}}
 	</button>
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 4b5bf2ec0a..5b846f6b21 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -1,5 +1,5 @@
 {{if .Flash}}
-	<div class="sixteen wide column gt-mb-3">
+	<div class="sixteen wide column tw-mb-2">
 		{{template "base/alert" .}}
 	</div>
 {{end}}
@@ -14,22 +14,22 @@
 		</h1>
 		<div class="issue-title-buttons">
 			{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
-				<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} gt-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
+				<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
 			{{end}}
 			{{if not .Issue.IsPull}}
-				<a role="button" class="ui small primary button new-issue-button gt-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
+				<a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 			{{end}}
 		</div>
 		{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
 			<div class="edit-buttons">
 				<button id="cancel-edit-title" class="ui small basic button in-edit gt-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
-				<button id="save-edit-title" class="ui small primary button in-edit gt-hidden gt-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
+				<button id="save-edit-title" class="ui small primary button in-edit gt-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
 			</div>
 		{{end}}
 	</div>
 	<div class="issue-title-meta">
 		{{if .HasMerged}}
-			<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "gt-mr-2"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
+			<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
 		{{else if .Issue.IsClosed}}
 			<div class="ui red label issue-state-label">{{if .Issue.IsPull}}{{svg "octicon-git-pull-request"}}{{else}}{{svg "octicon-issue-closed"}}{{end}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
 		{{else if .Issue.IsPull}}
@@ -41,7 +41,7 @@
 		{{else}}
 			<div class="ui green label issue-state-label">{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.open_title"}}</div>
 		{{end}}
-		<div class="gt-ml-3">
+		<div class="tw-ml-2">
 			{{if .Issue.IsPull}}
 				{{$headHref := .HeadTarget}}
 				{{if .HeadBranchLink}}
@@ -72,7 +72,7 @@
 					{{end}}
 					<span id="pull-desc-edit" class="gt-hidden flex-text-block">
 						<div class="ui floating filter dropdown">
-							<div class="ui basic small button gt-mr-0">
+							<div class="ui basic small button tw-mr-0">
 								<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>
 							</div>
 						</div>
diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl
index b2f0798917..ad31c95ad4 100644
--- a/templates/repo/latest_commit.tmpl
+++ b/templates/repo/latest_commit.tmpl
@@ -2,7 +2,7 @@
 	<div class="ui active tiny slow centered inline">…</div>
 {{else}}
 	{{if .LatestCommitUser}}
-		{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "gt-mr-2"}}
+		{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}}
 		{{if .LatestCommitUser.FullName}}
 			<a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
 		{{else}}
@@ -10,7 +10,7 @@
 		{{end}}
 	{{else}}
 		{{if .LatestCommit.Author}}
-			{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "gt-mr-2"}}
+			{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "tw-mr-1"}}
 			<span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span>
 		{{end}}
 	{{end}}
diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl
index 32465bc394..c5c697edff 100644
--- a/templates/repo/migrate/migrate.tmpl
+++ b/templates/repo/migrate/migrate.tmpl
@@ -7,11 +7,11 @@
 				{{range .Services}}
 					<a class="ui card migrate-entry tw-flex tw-items-center" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
 						{{if eq .Name "github"}}
-							{{svg "octicon-mark-github" 184 "gt-p-4"}}
+							{{svg "octicon-mark-github" 184 "tw-p-4"}}
 						{{else if eq .Name "gitlab"}}
-							{{svg "gitea-gitlab" 184 "gt-p-4"}}
+							{{svg "gitea-gitlab" 184 "tw-p-4"}}
 						{{else if eq .Name "gitbucket"}}
-							{{svg "gitea-gitbucket" 184 "gt-p-4"}}
+							{{svg "gitea-gitbucket" 184 "tw-p-4"}}
 						{{else}}
 							{{svg (printf "gitea-%s" .Name) 184}}
 						{{end}}
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl
index eea1057a50..05ad7264bf 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 	{{template "repo/header" .}}
 	<div class="ui container padded">
-		<div class="tw-flex tw-justify-between tw-items-center gt-mb-4">
+		<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
 			{{template "repo/issue/navbar" .}}
 			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 		</div>
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index 0ddb17a934..c0e48928f9 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -15,7 +15,7 @@
 			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
-		<span class="item tw-ml-auto gt-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2">
+		<span class="item tw-ml-auto tw-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2">
 			<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
 			<span class="diff-stats-bar">
 				<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>
diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl
index 5943ae0434..cfb3ec1d3d 100644
--- a/templates/repo/pulse.tmpl
+++ b/templates/repo/pulse.tmpl
@@ -109,7 +109,7 @@
 
 {{if gt .Activity.PublishedReleaseCount 0}}
 	<h4 class="divider divider-text tw-normal-case" id="published-releases">
-		{{svg "octicon-tag" 16 "gt-mr-3"}}
+		{{svg "octicon-tag" 16 "tw-mr-2"}}
 		{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
 			(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
 			(ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
@@ -131,7 +131,7 @@
 
 {{if gt .Activity.MergedPRCount 0}}
 	<h4 class="divider divider-text tw-normal-case" id="merged-pull-requests">
-		{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
+		{{svg "octicon-git-pull-request" 16 "tw-mr-2"}}
 		{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
 			(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
 			(ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
@@ -150,7 +150,7 @@
 
 {{if gt .Activity.OpenedPRCount 0}}
 	<h4 class="divider divider-text tw-normal-case" id="proposed-pull-requests">
-		{{svg "octicon-git-branch" 16 "gt-mr-3"}}
+		{{svg "octicon-git-branch" 16 "tw-mr-2"}}
 		{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
 			(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
 			(ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
@@ -169,7 +169,7 @@
 
 {{if gt .Activity.ClosedIssueCount 0}}
 	<h4 class="divider divider-text tw-normal-case" id="closed-issues">
-		{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
+		{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
 		{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
 			(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
 			(ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
@@ -188,7 +188,7 @@
 
 {{if gt .Activity.OpenedIssueCount 0}}
 	<h4 class="divider divider-text tw-normal-case" id="new-issues">
-		{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+		{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 		{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
 			(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
 			(ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
@@ -207,7 +207,7 @@
 
 {{if gt .Activity.UnresolvedIssueCount 0}}
 	<h4 class="divider divider-text tw-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
-		{{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
+		{{svg "octicon-comment-discussion" 16 "tw-mr-2"}}
 		{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
 	</h4>
 	<div class="list">
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 6c77ee12f7..9da6c48c8e 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -9,14 +9,14 @@
 				{{$release := $info.Release}}
 				<li class="ui grid">
 					<div class="ui four wide column meta">
-						<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>
+						<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 "tw-mr-1"}}{{$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>
+							<a class="muted gt-mono" href="{{$.RepoLink}}/src/commit/{{$release.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha $release.Sha1}}</a>
 							{{template "repo/branch_dropdown" dict "root" $ "release" $release}}
 						{{end}}
 					</div>
 					<div class="ui twelve wide column detail">
-						<div class="tw-flex tw-items-center tw-justify-between tw-flex-wrap gt-mb-3">
+						<div class="tw-flex tw-items-center tw-justify-between tw-flex-wrap tw-mb-2">
 							<h4 class="release-list-title gt-word-break">
 								{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>{{end}}
 								{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}}
@@ -39,9 +39,9 @@
 						<p class="text grey">
 							<span class="author">
 							{{if $release.OriginalAuthor}}
-								{{svg (MigrationIcon $release.Repo.GetOriginalURLHostname) 20 "gt-mr-2"}}{{$release.OriginalAuthor}}
+								{{svg (MigrationIcon $release.Repo.GetOriginalURLHostname) 20 "tw-mr-1"}}{{$release.OriginalAuthor}}
 							{{else if $release.Publisher}}
-								{{ctx.AvatarUtils.Avatar $release.Publisher 20 "gt-mr-2"}}
+								{{ctx.AvatarUtils.Avatar $release.Publisher 20 "tw-mr-1"}}
 								<a href="{{$release.Publisher.HomeLink}}">{{$release.Publisher.GetDisplayName}}</a>
 							{{else}}
 								Ghost
@@ -62,22 +62,22 @@
 						</div>
 						<div class="divider"></div>
 						<details class="download" {{if eq $idx 0}}open{{end}}>
-							<summary class="gt-my-4">
+							<summary class="tw-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>
+										<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{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>
+										<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{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>
+											<strong>{{svg "octicon-package" 16 "tw-mr-1"}}{{.Name}}</strong>
 										</a>
 										<div>
 											<span class="text grey">{{.Size | FileSize}}</span>
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index fd6338a701..c01f9a421b 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -39,12 +39,12 @@
 							</div>
 						</div>
 						<div>
-							<span id="tag-helper" class="help gt-mt-3 gt-pb-0">{{ctx.Locale.Tr "repo.release.tag_helper"}}</span>
+							<span id="tag-helper" class="help tw-mt-2 tw-pb-0">{{ctx.Locale.Tr "repo.release.tag_helper"}}</span>
 						</div>
 					{{end}}
 				</div>
 			</div>
-			<div class="eleven wide gt-pt-0">
+			<div class="eleven wide tw-pt-0">
 				<div class="field {{if .Err_Title}}error{{end}}">
 					<input name="title" aria-label="{{ctx.Locale.Tr "repo.release.title"}}" placeholder="{{ctx.Locale.Tr "repo.release.title"}}" value="{{.title}}" autofocus maxlength="255">
 				</div>
@@ -100,7 +100,7 @@
 						</div>
 					</div>
 					<span class="help">{{ctx.Locale.Tr "repo.release.prerelease_helper"}}</span>
-					<div class="divider gt-mt-0"></div>
+					<div class="divider tw-mt-0"></div>
 					<div class="tw-flex tw-justify-end">
 						{{if .PageIsEditRelease}}
 							<a class="ui small button" href="{{.RepoLink}}/releases">
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index ee6bdfbf2f..52c0c2c800 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -16,7 +16,7 @@
 					{{.CsrfTokenHtml}}
 					<input type="hidden" name="action" value="default_branch">
 					{{if not .Repository.IsEmpty}}
-						<div class="ui dropdown selection search tw-flex-1 gt-mr-3 tw-max-w-96">
+						<div class="ui dropdown selection search tw-flex-1 tw-mr-2 tw-max-w-96">
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 							<input type="hidden" name="branch" value="{{.Repository.DefaultBranch}}">
 							<div class="default text">{{.Repository.DefaultBranch}}</div>
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index f66b94c332..cc77e79e8c 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -11,7 +11,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			<div class="{{if not .HasError}}gt-hidden{{end}} gt-mb-4" id="add-deploy-key-panel">
+			<div class="{{if not .HasError}}gt-hidden{{end}} tw-mb-4" id="add-deploy-key-panel">
 				<form class="ui form" action="{{.Link}}" method="post">
 					{{.CsrfTokenHtml}}
 					<div class="field">
diff --git a/templates/repo/settings/githooks.tmpl b/templates/repo/settings/githooks.tmpl
index 3d15d097cc..1a603f9fe8 100644
--- a/templates/repo/settings/githooks.tmpl
+++ b/templates/repo/settings/githooks.tmpl
@@ -10,9 +10,9 @@
 				</div>
 				{{range .Hooks}}
 					<div class="item truncated-item-container">
-						<span class="text {{if .IsActive}}green{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span>
-						<span class="text truncate tw-flex-1 gt-mr-3">{{.Name}}</span>
-						<a class="muted tw-float-right gt-p-3" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}">
+						<span class="text {{if .IsActive}}green{{else}}grey{{end}} tw-mr-2">{{svg "octicon-dot-fill" 22}}</span>
+						<span class="text truncate tw-flex-1 tw-mr-2">{{.Name}}</span>
+						<a class="muted tw-float-right tw-p-2" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}">
 							{{svg "octicon-pencil"}}
 						</a>
 					</div>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 1e32853d81..b8fa4759b1 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -121,7 +121,7 @@
 							<tbody>
 								<tr>
 									<td colspan="4">
-										<div class="text red gt-py-4">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div>
+										<div class="text red tw-py-4">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div>
 									</td>
 								</tr>
 							</tbody>
@@ -163,10 +163,10 @@
 											<p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p>
 										</div>
 										<details class="ui optional field" {{if or .Err_Auth $address.Username}}open{{end}}>
-											<summary class="gt-p-2">
+											<summary class="tw-p-1">
 												{{ctx.Locale.Tr "repo.need_auth"}}
 											</summary>
-											<div class="gt-p-2">
+											<div class="tw-p-1">
 												<div class="inline field {{if .Err_Auth}}error{{end}}">
 													<label for="mirror_username">{{ctx.Locale.Tr "username"}}</label>
 													<input id="mirror_username" name="mirror_username" value="{{$address.Username}}" {{if not .mirror_username}}data-need-clear="true"{{end}}>
@@ -262,10 +262,10 @@
 												<p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p>
 											</div>
 											<details class="ui optional field" {{if or .Err_PushMirrorAuth .push_mirror_username}}open{{end}}>
-												<summary class="gt-p-2">
+												<summary class="tw-p-1">
 													{{ctx.Locale.Tr "repo.need_auth"}}
 												</summary>
-												<div class="gt-p-2">
+												<div class="tw-p-1">
 													<div class="inline field {{if .Err_PushMirrorAuth}}error{{end}}">
 														<label for="push_mirror_username">{{ctx.Locale.Tr "username"}}</label>
 														<input id="push_mirror_username" name="push_mirror_username" value="{{.push_mirror_username}}">
@@ -335,7 +335,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
 						</div>
 					</div>
-					<div class="inline field gt-pl-4">
+					<div class="inline field tw-pl-4">
 						<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
 						<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
 					</div>
@@ -345,7 +345,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label>
 						</div>
 					</div>
-					<div class="field gt-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box">
+					<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box">
 						<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label>
 						<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}">
 						<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p>
@@ -372,7 +372,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label>
 						</div>
 					</div>
-					<div class="field gt-pl-4 {{if (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
+					<div class="field tw-pl-4 {{if (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
 						{{if .Repository.CanEnableTimetracker}}
 							<div class="field">
 								<div class="ui checkbox">
@@ -404,7 +404,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label>
 						</div>
 					</div>
-					<div class="field gt-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
+					<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
 						<div class="field">
 							<label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label>
 							<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}">
@@ -458,7 +458,7 @@
 						<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
 					</div>
 				</div>
-				<div class="field {{if not $isProjectsEnabled}} disabled{{end}} gt-pl-4" id="projects_box">
+				<div class="field {{if not $isProjectsEnabled}} disabled{{end}} tw-pl-4" id="projects_box">
 					<p>
 						{{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
 					</p>
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index e95dd831c9..6d8578a2f7 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -10,17 +10,17 @@
 					<label>{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern"}}</label>
 					<input name="rule_name" type="text" value="{{.Rule.RuleName}}">
 					<input name="rule_id" type="hidden" value="{{.Rule.ID}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc"}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc"}}</p>
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns"}}</label>
 					<input name="protected_file_patterns" type="text" value="{{.Rule.ProtectedFilePatterns}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc"}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc"}}</p>
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns"}}</label>
 					<input name="unprotected_file_patterns" type="text" value="{{.Rule.UnprotectedFilePatterns}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc"}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc"}}</p>
 				</div>
 
 				{{.CsrfTokenHtml}}
@@ -98,7 +98,7 @@
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_required_approvals"}}</label>
 					<input name="required_approvals" type="number" value="{{.Rule.RequiredApprovals}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_required_approvals_desc"}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_required_approvals_desc"}}</p>
 				</div>
 				<div class="grouped fields">
 					<div class="field">
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index 9abc03e40e..36e75a7eb5 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -14,12 +14,12 @@
 		</div>
 		{{range .Webhooks}}
 			<div class="item truncated-item-container">
-				<span class="text {{if eq .LastStatus 1}}green{{else if eq .LastStatus 2}}red{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span>
-				<div class="text truncate tw-flex-1 gt-mr-3">
+				<span class="text {{if eq .LastStatus 1}}green{{else if eq .LastStatus 2}}red{{else}}grey{{end}} tw-mr-2">{{svg "octicon-dot-fill" 22}}</span>
+				<div class="text truncate tw-flex-1 tw-mr-2">
 					<a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a>
 				</div>
-				<a class="muted gt-p-3" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
-				<a class="delete-button gt-p-3" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}}</a>
+				<a class="muted tw-p-2" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
+				<a class="delete-button tw-p-2" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}}</a>
 			</div>
 		{{end}}
 	</div>
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index 654a65fa5c..a0dbe7e10c 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -1,5 +1,5 @@
 {{if and (not .HideRepoInfo) (not .IsBlame)}}
-<div class="ui segments repository-summary gt-mt-2 gt-mb-0">
+<div class="ui segments repository-summary tw-mt-1 tw-mb-0">
 	<div class="ui segment sub-menu repository-menu">
 		{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
 			<a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}">
diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl
index 06c02c5f75..a107bd1ad3 100644
--- a/templates/repo/tag/list.tmpl
+++ b/templates/repo/tag/list.tmpl
@@ -6,7 +6,7 @@
 		{{template "repo/release_tag_header" .}}
 		<h4 class="ui top attached header">
 			<div class="five wide column tw-flex tw-items-center">
-				{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.tags"}}
+				{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.tags"}}
 			</div>
 		</h4>
 		{{$canReadReleases := $.Permission.CanRead $.UnitTypeReleases}}
@@ -16,7 +16,7 @@
 					{{range $idx, $release := .Releases}}
 						<tr>
 							<td class="tag">
-								<h3 class="release-tag-name gt-mb-3">
+								<h3 class="release-tag-name tw-mb-2">
 									{{if $canReadReleases}}
 										<a class="tw-flex tw-items-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
 									{{else}}
@@ -26,28 +26,28 @@
 								<div class="download tw-flex tw-items-center">
 									{{if $.Permission.CanRead $.UnitTypeCode}}
 										{{if .CreatedUnix}}
-											<span class="gt-mr-3">{{svg "octicon-clock" 16 "gt-mr-2"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
+											<span class="tw-mr-2">{{svg "octicon-clock" 16 "tw-mr-1"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
 										{{end}}
 
-										<a class="gt-mr-3 gt-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a>
+										<a class="tw-mr-2 gt-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .Sha1}}</a>
 
 										{{if not $.DisableDownloadSourceArchives}}
-											<a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}ZIP</a>
-											<a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}TAR.GZ</a>
+											<a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}ZIP</a>
+											<a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}TAR.GZ</a>
 										{{end}}
 
 										{{if (and $canReadReleases $.CanCreateRelease $release.IsTag)}}
-											<a class="gt-mr-3 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a>
+											<a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a>
 										{{end}}
 
 										{{if (and ($.Permission.CanWrite $.UnitTypeCode) $release.IsTag)}}
-											<a class="ui delete-button gt-mr-3 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}">
-												{{svg "octicon-trash" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.delete_tag"}}
+											<a class="ui delete-button tw-mr-2 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}">
+												{{svg "octicon-trash" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.delete_tag"}}
 											</a>
 										{{end}}
 
 										{{if and $canReadReleases (not $release.IsTag)}}
-											<a class="gt-mr-3 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.detail"}}</a>
+											<a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.detail"}}</a>
 										{{end}}
 									{{end}}
 								</div>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index ebe82ff161..a5b6fa7a52 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -11,7 +11,7 @@
 	{{end}}
 
 	{{if not .ReadmeInList}}
-		<div id="repo-file-commit-box" class="ui top attached header list-header gt-mb-4">
+		<div id="repo-file-commit-box" class="ui top attached header list-header tw-mb-4">
 			<div>
 				{{template "repo/latest_commit" .}}
 			</div>
@@ -26,9 +26,9 @@
 	{{end}}
 
 	<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
-		<div class="file-header-left tw-flex tw-items-center gt-py-3 gt-pr-4">
+		<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4">
 			{{if .ReadmeInList}}
-				{{svg "octicon-book" 16 "gt-mr-3"}}
+				{{svg "octicon-book" 16 "tw-mr-2"}}
 				<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong>
 			{{else}}
 				{{template "repo/file_info" .}}
@@ -42,7 +42,7 @@
 				</div>
 			{{end}}
 			{{if not .ReadmeInList}}
-				<div class="ui buttons gt-mr-2">
+				<div class="ui buttons tw-mr-1">
 					<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
 					{{if not .IsViewCommit}}
 						<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
@@ -76,8 +76,8 @@
 					{{end}}
 				{{end}}
 			{{else if .EscapeStatus.Escaped}}
-				<button class="ui mini basic button unescape-button gt-mr-2 gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
-				<button class="ui mini basic button escape-button gt-mr-2">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
+				<button class="ui mini basic button unescape-button tw-mr-1 gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
+				<button class="ui mini basic button escape-button tw-mr-1">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 			{{end}}
 			{{if and .ReadmeInList .CanEditReadmeFile}}
 				<a class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.editor.edit_this_file"}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}/{{PathEscapeSegments .FileName}}">{{svg "octicon-pencil"}}</a>
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index 988a5ddd50..7c463c50a6 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -1,4 +1,4 @@
-<table id="repo-files-table" class="ui single line table gt-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
+<table id="repo-files-table" class="ui single line table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
 	<thead>
 		<tr class="commit-list">
 			<th colspan="2">
diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl
index d1abd27342..0f10e60c4f 100644
--- a/templates/repo/wiki/new.tmpl
+++ b/templates/repo/wiki/new.tmpl
@@ -31,7 +31,7 @@
 				"TextareaContent" $content
 			)}}
 
-			<div class="field gt-mt-4">
+			<div class="field tw-mt-4">
 				<input name="message" aria-label="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}">
 			</div>
 			<div class="divider"></div>
diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
index 182635e011..8e0060d4b3 100644
--- a/templates/repo/wiki/revision.tmpl
+++ b/templates/repo/wiki/revision.tmpl
@@ -22,7 +22,7 @@
 			</div>
 		</div>
 		<h2 class="ui top header">{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}</h2>
-		<div class="gt-mt-4">
+		<div class="tw-mt-4">
 			<h4 class="ui top attached header">
 				<div class="ui stackable grid">
 					<div class="sixteen wide column">
diff --git a/templates/repo/wiki/start.tmpl b/templates/repo/wiki/start.tmpl
index dca7a074aa..1b3c3d538a 100644
--- a/templates/repo/wiki/start.tmpl
+++ b/templates/repo/wiki/start.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository wiki start">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="ui center segment gt-py-5">
+		<div class="ui center segment tw-py-8">
 			{{svg "octicon-book" 48}}
 			<h2>{{ctx.Locale.Tr "repo.wiki.welcome"}}</h2>
 			<p>{{ctx.Locale.Tr "repo.wiki.welcome_desc"}}</p>
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index f8bbf23b62..d60f10b71f 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -7,15 +7,15 @@
 			{{template "base/disable_form_autofill"}}
 			{{.CsrfTokenHtml}}
 			<div class="runner-basic-info">
-				<div class="field tw-inline-block gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.status"}}</label>
 					<span class="ui {{if .Runner.IsOnline}}green{{else}}basic{{end}} label">{{.Runner.StatusLocaleName ctx.Locale}}</span>
 				</div>
-				<div class="field tw-inline-block gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.last_online"}}</label>
 					<span>{{if .Runner.LastOnline}}{{TimeSinceUnix .Runner.LastOnline ctx.Locale}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</span>
 				</div>
-				<div class="field tw-inline-block gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.labels"}}</label>
 					<span>
 						{{range .Runner.AgentLabels}}
@@ -23,7 +23,7 @@
 						{{end}}
 					</span>
 				</div>
-				<div class="field tw-inline-block gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.owner_type"}}</label>
 					<span data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</span>
 				</div>
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index adb2f61c54..1c0dfcc551 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -5,7 +5,7 @@
 
 			<div class="flex-item-icon">
 				{{if $.CanWriteIssuesOrPulls}}
-				<input type="checkbox" autocomplete="off" class="issue-checkbox gt-mr-4" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} &quot;{{.Title}}&quot;">
+				<input type="checkbox" autocomplete="off" class="issue-checkbox tw-mr-4" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} &quot;{{.Title}}&quot;">
 				{{end}}
 				{{template "shared/issueicon" .}}
 			</div>
@@ -19,7 +19,7 @@
 								{{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}}
 							{{end}}
 						{{end}}
-						<span class="labels-list gt-ml-2">
+						<span class="labels-list tw-ml-1">
 							{{range .Labels}}
 								<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context ctx.Locale .}}</a>
 							{{end}}
diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl
index 42e029da82..a98a662654 100644
--- a/templates/shared/search/code/results.tmpl
+++ b/templates/shared/search/code/results.tmpl
@@ -1,8 +1,8 @@
 <div class="flex-text-block tw-flex-wrap">
 	{{range $term := .SearchResultLanguages}}
-	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0"
+	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label tw-m-0"
 		href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&fuzzy={{$.IsFuzzy}}">
-		<i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i>
+		<i class="color-icon tw-mr-2" style="background-color: {{$term.Color}}"></i>
 		{{$term.Language}}
 		<div class="detail">{{$term.Count}}</div>
 	</a>
diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl
index b22324585c..bee0397259 100644
--- a/templates/shared/searchbottom.tmpl
+++ b/templates/shared/searchbottom.tmpl
@@ -1,11 +1,11 @@
 {{if or .result.Language (not .result.UpdatedUnix.IsZero)}}
 <div class="ui bottom attached table segment tw-flex tw-items-center tw-justify-between">
-		<div class="tw-flex tw-items-center gt-ml-4">
+		<div class="tw-flex tw-items-center tw-ml-4">
 			{{if .result.Language}}
-					<i class="color-icon gt-mr-3" style="background-color: {{.result.Color}}"></i>{{.result.Language}}
+					<i class="color-icon tw-mr-2" style="background-color: {{.result.Color}}"></i>{{.result.Language}}
 			{{end}}
 		</div>
-		<div class="gt-mr-4">
+		<div class="tw-mr-4">
 			{{if not .result.UpdatedUnix.IsZero}}
 					<span class="ui grey text">{{ctx.Locale.Tr "explore.code_last_indexed_at" (TimeSinceUnix .result.UpdatedUnix ctx.Locale)}}</span>
 			{{end}}
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index c943a1944d..ea59459083 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -30,7 +30,7 @@
 				<span class="color-text-light-2">
 					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}
 				</span>
-				<button class="ui btn interact-bg link-action gt-p-3"
+				<button class="ui btn interact-bg link-action tw-p-2"
 					data-url="{{$.Link}}/delete?id={{.ID}}"
 					data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
 					data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}"
diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl
index 4d8ad736be..d57a635b4b 100644
--- a/templates/shared/user/authorlink.tmpl
+++ b/templates/shared/user/authorlink.tmpl
@@ -1 +1 @@
-<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label gt-p-2">bot</span>{{end}}
+<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1">bot</span>{{end}}
diff --git a/templates/shared/user/blocked_users.tmpl b/templates/shared/user/blocked_users.tmpl
index 071c7da11c..e83a039ef5 100644
--- a/templates/shared/user/blocked_users.tmpl
+++ b/templates/shared/user/blocked_users.tmpl
@@ -17,7 +17,7 @@
 		{{.CsrfTokenHtml}}
 		<input type="hidden" name="action" value="block" />
 		<div id="search-user-box" class="field ui fluid search input">
-			<input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
+			<input class="prompt tw-mr-2" name="blockee" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
 			<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
 		</div>
 		<div class="field">
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 3e1cacd9ba..868f8d5a13 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -18,10 +18,10 @@
 						{{svg "octicon-gear" 18}}
 					</a>
 				{{end}}</span>
-		<div class="gt-mt-3">
-			<a class="muted" href="{{.ContextUser.HomeLink}}?tab=followers">{{svg "octicon-person" 18 "gt-mr-2"}}{{.NumFollowers}} {{ctx.Locale.Tr "user.followers"}}</a> · <a class="muted" href="{{.ContextUser.HomeLink}}?tab=following">{{.NumFollowing}} {{ctx.Locale.Tr "user.following"}}</a>
+		<div class="tw-mt-2">
+			<a class="muted" href="{{.ContextUser.HomeLink}}?tab=followers">{{svg "octicon-person" 18 "tw-mr-1"}}{{.NumFollowers}} {{ctx.Locale.Tr "user.followers"}}</a> · <a class="muted" href="{{.ContextUser.HomeLink}}?tab=following">{{.NumFollowing}} {{ctx.Locale.Tr "user.following"}}</a>
 			{{if .EnableFeed}}
-				<a href="{{.ContextUser.HomeLink}}.rss"><i class="ui text grey gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">{{svg "octicon-rss" 18}}</i></a>
+				<a href="{{.ContextUser.HomeLink}}.rss"><i class="ui text grey tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">{{svg "octicon-rss" 18}}</i></a>
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index fc2ac98e29..06c71c0610 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -32,7 +32,7 @@
 				<span class="color-text-light-2">
 					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}
 				</span>
-				<button class="btn interact-bg gt-p-3 show-modal"
+				<button class="btn interact-bg tw-p-2 show-modal"
 					data-tooltip-content="{{ctx.Locale.Tr "actions.variables.edit"}}"
 					data-modal="#edit-variable-modal"
 					data-modal-form.action="{{$.Link}}/{{.ID}}/edit"
@@ -42,7 +42,7 @@
 				>
 					{{svg "octicon-pencil"}}
 				</button>
-				<button class="btn interact-bg gt-p-3 link-action"
+				<button class="btn interact-bg tw-p-2 link-action"
 					data-tooltip-content="{{ctx.Locale.Tr "actions.variables.deletion"}}"
 					data-url="{{$.Link}}/{{.ID}}/delete"
 					data-modal-confirm="{{ctx.Locale.Tr "actions.variables.deletion.description"}}"
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index 106529ca72..576b6eebbb 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -28,20 +28,20 @@
 				</div>
 			</div>
 		</nav>
-		<div class="divider gt-my-0"></div>
+		<div class="divider tw-my-0"></div>
 		<div role="main" class="page-content status-page-500">
 			<div class="ui container" >
 				<style> .ui.message.flash-message { text-align: left; } </style>
 				{{template "base/alert" .}}
 			</div>
-			<p class="gt-mt-5 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p>
+			<p class="tw-mt-8 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p>
 			<div class="divider"></div>
-			<div class="ui container gt-my-5">
+			<div class="ui container tw-my-8">
 				{{if .ErrorMsg}}
 					<p>{{ctx.Locale.Tr "error.occurred"}}:</p>
 					<pre class="tw-whitespace-pre-wrap tw-break-all">{{.ErrorMsg}}</pre>
 				{{end}}
-				<div class="center gt-mt-5">
+				<div class="center tw-mt-8">
 					{{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
 					{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message"}}</p>{{end}}
 				</div>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index ad86b0b881..9872096fbc 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -53,11 +53,11 @@
 	<div class="divider divider-text">
 		{{ctx.Locale.Tr "sign_in_or"}}
 	</div>
-	<div id="oauth2-login-navigator" class="gt-py-2">
+	<div id="oauth2-login-navigator" class="tw-py-1">
 		<div class="tw-flex tw-flex-col tw-justify-center">
 			<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
 				{{range $provider := .OAuth2Providers}}
-					<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+					<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center tw-py-2 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
 						{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 					</a>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index 26d9091b68..bdb691d833 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -58,11 +58,11 @@
 			<div class="divider divider-text">
 				{{ctx.Locale.Tr "sign_in_or"}}
 			</div>
-			<div id="oauth2-login-navigator" class="gt-py-2">
+			<div id="oauth2-login-navigator" class="tw-py-1">
 				<div class="tw-flex tw-flex-col tw-justify-center">
 					<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
 						{{range $provider := .OAuth2Providers}}
-							<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center gt-py-3 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+							<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center tw-py-2 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
 								{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 							</a>
diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl
index 24dd75eed5..1b84765323 100644
--- a/templates/user/auth/webauthn.tmpl
+++ b/templates/user/auth/webauthn.tmpl
@@ -10,7 +10,7 @@
 				{{template "base/alert" .}}
 				<p>{{ctx.Locale.Tr "webauthn_sign_in"}}</p>
 			</div>
-			<div class="ui attached segment tw-flex tw-items-center tw-justify-center tw-gap-1 gt-py-3">
+			<div class="ui attached segment tw-flex tw-items-center tw-justify-center tw-gap-1 tw-py-2">
 				<div class="is-loading tw-w-[40px] tw-h-[40px]"></div>
 				{{ctx.Locale.Tr "webauthn_press_button"}}
 			</div>
diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl
index fc6064db76..13659affd5 100644
--- a/templates/user/auth/webauthn_error.tmpl
+++ b/templates/user/auth/webauthn_error.tmpl
@@ -1,6 +1,6 @@
 <div id="webauthn-error" class="ui negative message gt-hidden">
 	<div class="header">{{ctx.Locale.Tr "webauthn_error"}}</div>
-	<div id="webauthn-error-msg" class="gt-pt-3"></div>
+	<div id="webauthn-error-msg" class="tw-pt-2"></div>
 	<div class="gt-hidden">
 		<div data-webauthn-error-msg="browser">{{ctx.Locale.Tr "webauthn_unsupported_browser"}}</div>
 		<div data-webauthn-error-msg="unknown">{{ctx.Locale.Tr "webauthn_error_unknown"}}</div>
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index fbe151607c..60aa194534 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -119,7 +119,7 @@
 				{{end}}
 			</div>
 			<div class="flex-item-trailing">
-				{{svg (printf "octicon-%s" (ActionIcon .GetOpType)) 32 "text grey gt-mr-2"}}
+				{{svg (printf "octicon-%s" (ActionIcon .GetOpType)) 32 "text grey tw-mr-1"}}
 			</div>
 		</div>
 	{{end}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 5080821dd1..ea75267de1 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -38,11 +38,11 @@
 				<div class="list-header">
 					<div class="small-menu-items ui compact tiny menu list-header-toggle">
 						<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
-							{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+							{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 						</a>
 						<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
-							{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
+							{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 						</a>
 					</div>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 3a260c3d10..0f1e866a21 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -37,11 +37,11 @@
 				<div class="list-header">
 					<div class="small-menu-items ui compact tiny menu list-header-toggle">
 						<a class="item{{if not .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
-							{{svg "octicon-milestone" 16 "gt-mr-3"}}
+							{{svg "octicon-milestone" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .MilestoneStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 						</a>
 						<a class="item{{if .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
-							{{svg "octicon-check" 16 "gt-mr-3"}}
+							{{svg "octicon-check" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .MilestoneStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 						</a>
 					</div>
@@ -72,7 +72,7 @@
 					{{range .Milestones}}
 						<li class="milestone-card">
 							<div class="milestone-header">
-								<h3 class="flex-text-block gt-m-0">
+								<h3 class="flex-text-block tw-m-0">
 									<span class="ui large label">
 										{{.Repo.FullName}}
 									</span>
@@ -80,7 +80,7 @@
 									<a class="muted" href="{{.Repo.Link}}/milestone/{{.ID}}">{{.Name}}</a>
 								</h3>
 								<div class="tw-flex tw-items-center">
-									<span class="gt-mr-3">{{.Completeness}}%</span>
+									<span class="tw-mr-2">{{.Completeness}}%</span>
 									<progress value="{{.Completeness}}" max="100"></progress>
 								</div>
 							</div>
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index f0fe6ac6f4..44edd6e107 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -1,7 +1,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
 	<div class="ui container">
 		{{$notificationUnreadCount := call .NotificationUnreadCount}}
-		<div class="tw-flex tw-items-center tw-justify-between gt-mb-4">
+		<div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
 			<div class="small-menu-items ui compact tiny menu">
 				<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
 					{{ctx.Locale.Tr "notification.unread"}}
@@ -15,18 +15,18 @@
 				<form action="{{AppSubUrl}}/notifications/purge" method="post">
 					{{$.CsrfTokenHtml}}
 					<div class="{{if not $notificationUnreadCount}}gt-hidden{{end}}">
-						<button class="ui mini button primary gt-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}">
+						<button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}">
 							{{svg "octicon-checklist"}}
 						</button>
 					</div>
 				</form>
 			{{end}}
 		</div>
-		<div class="gt-p-0">
+		<div class="tw-p-0">
 			<div id="notification_table">
 				{{if not .Notifications}}
-					<div class="tw-flex tw-items-center tw-flex-col gt-p-4">
-						{{svg "octicon-inbox" 56 "gt-mb-4"}}
+					<div class="tw-flex tw-items-center tw-flex-col tw-p-4">
+						{{svg "octicon-inbox" 56 "tw-mb-4"}}
 						{{if eq .Status 1}}
 							{{ctx.Locale.Tr "notification.no_unread"}}
 						{{else}}
@@ -35,8 +35,8 @@
 					</div>
 				{{else}}
 					{{range $notification := .Notifications}}
-						<div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
-							<div class="notifications-icon gt-ml-3 gt-mr-2 tw-self-start gt-mt-2">
+						<div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-p-2" id="notification_{{.ID}}" data-status="{{.Status}}">
+							<div class="notifications-icon tw-ml-2 tw-mr-1 tw-self-start tw-mt-1">
 								{{if .Issue}}
 									{{template "shared/issueicon" .Issue}}
 								{{else}}
@@ -47,10 +47,10 @@
 								<div class="notifications-top-row tw-text-13">
 									{{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}}
 									{{if eq .Status 3}}
-										{{svg "octicon-pin" 13 "text blue gt-mt-1 gt-ml-2"}}
+										{{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}}
 									{{end}}
 								</div>
-								<div class="notifications-bottom-row tw-text-16 gt-py-1">
+								<div class="notifications-bottom-row tw-text-16 tw-py-0.5">
 									<span class="issue-title">
 										{{if .Issue}}
 											{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}
@@ -60,20 +60,20 @@
 									</span>
 								</div>
 							</a>
-							<div class="notifications-updated tw-items-center gt-mr-3">
+							<div class="notifications-updated tw-items-center tw-mr-2">
 								{{if .Issue}}
 									{{TimeSinceUnix .Issue.UpdatedUnix ctx.Locale}}
 								{{else}}
 									{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
 								{{end}}
 							</div>
-							<div class="notifications-buttons tw-items-center tw-justify-end tw-gap-1 gt-px-2">
+							<div class="notifications-buttons tw-items-center tw-justify-end tw-gap-1 tw-px-1">
 								{{if ne .Status 3}}
 									<form action="{{AppSubUrl}}/notifications/status" method="post">
 										{{$.CsrfTokenHtml}}
 										<input type="hidden" name="notification_id" value="{{.ID}}">
 										<input type="hidden" name="status" value="pinned">
-										<button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.pin"}}"
+										<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.pin"}}"
 											data-url="{{AppSubUrl}}/notifications/status"
 											data-status="pinned"
 											data-page="{{$.Page.Paginater.Current}}"
@@ -89,7 +89,7 @@
 										<input type="hidden" name="notification_id" value="{{.ID}}">
 										<input type="hidden" name="status" value="read">
 										<input type="hidden" name="page" value="{{$.Page.Paginater.Current}}">
-										<button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.mark_as_read"}}"
+										<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_read"}}"
 											data-url="{{AppSubUrl}}/notifications/status"
 											data-status="read"
 											data-page="{{$.Page.Paginater.Current}}"
@@ -104,7 +104,7 @@
 										<input type="hidden" name="notification_id" value="{{.ID}}">
 										<input type="hidden" name="status" value="unread">
 										<input type="hidden" name="page" value="{{$.Page.Paginater.Current}}">
-										<button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}"
+										<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}"
 											data-url="{{AppSubUrl}}/notifications/status"
 											data-status="unread"
 											data-page="{{$.Page.Paginater.Current}}"
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
index eb71621d92..a5a965ca52 100644
--- a/templates/user/notification/notification_subscriptions.tmpl
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -18,11 +18,11 @@
 								{{ctx.Locale.Tr "all"}}
 							</a>
 							<a class="{{if eq .State "open"}}active {{end}}item" href="?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}">
-								{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+								{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 								{{ctx.Locale.Tr "repo.issues.open_title"}}
 							</a>
 							<a class="{{if eq .State "closed"}}active {{end}}item" href="?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}">
-								{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
+								{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
 								{{ctx.Locale.Tr "repo.issues.closed_title"}}
 							</a>
 						</div>
diff --git a/templates/user/overview/package_versions.tmpl b/templates/user/overview/package_versions.tmpl
index f6f963aecb..b2cc814e13 100644
--- a/templates/user/overview/package_versions.tmpl
+++ b/templates/user/overview/package_versions.tmpl
@@ -14,7 +14,7 @@
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
 				<div class="ui twelve wide column">
-					<div class="gt-mb-4">
+					<div class="tw-mb-4">
 						{{template "user/overview/header" .}}
 					</div>
 					{{template "package/shared/versionlist" .}}
diff --git a/templates/user/overview/packages.tmpl b/templates/user/overview/packages.tmpl
index 30ff871cb2..95cb506e57 100644
--- a/templates/user/overview/packages.tmpl
+++ b/templates/user/overview/packages.tmpl
@@ -14,7 +14,7 @@
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
 				<div class="ui twelve wide column">
-					<div class="gt-mb-4">
+					<div class="tw-mb-4">
 						{{template "user/overview/header" .}}
 					</div>
 					{{template "package/shared/list" .}}
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index 1495d58dd3..e68f79fae6 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -6,7 +6,7 @@
 				{{template "shared/user/profile_big_avatar" .}}
 			</div>
 			<div class="ui twelve wide column">
-				<div class="gt-mb-4">
+				<div class="tw-mb-4">
 					{{template "user/overview/header" .}}
 				</div>
 
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index 6645963984..040f46e48b 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -42,7 +42,7 @@
 			<div class="ui list">
 				{{if $.EnableNotifyMail}}
 				<div class="item">
-					<div class="gt-mb-3">{{ctx.Locale.Tr "settings.email_desc"}}</div>
+					<div class="tw-mb-2">{{ctx.Locale.Tr "settings.email_desc"}}</div>
 					<form action="{{AppSubUrl}}/user/settings/account/email" class="ui form" method="post">
 						{{$.CsrfTokenHtml}}
 						<input name="_method" type="hidden" value="NOTIFICATION">
@@ -96,7 +96,7 @@
 								</form>
 							</div>
 						{{end}}
-						<div class="content gt-py-3">
+						<div class="content tw-py-2">
 							<strong>{{.Email}}</strong>
 							{{if .IsPrimary}}
 								<div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div>
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index e43cf2ebbe..57f4c36161 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -18,7 +18,7 @@
 						<div class="flex-item-main">
 							<details>
 								<summary><span class="flex-item-title">{{.Name}}</span></summary>
-								<p class="gt-my-2">
+								<p class="tw-my-1">
 									{{ctx.Locale.Tr "settings.repo_and_org_access"}}:
 									{{if .DisplayPublicOnly}}
 										{{ctx.Locale.Tr "settings.permissions_public_only"}}
@@ -26,8 +26,8 @@
 										{{ctx.Locale.Tr "settings.permissions_access_all"}}
 									{{end}}
 								</p>
-								<p class="gt-my-2">{{ctx.Locale.Tr "settings.permissions_list"}}</p>
-								<ul class="gt-my-2">
+								<p class="tw-my-1">{{ctx.Locale.Tr "settings.permissions_list"}}</p>
+								<ul class="tw-my-1">
 								{{range .Scope.StringSlice}}
 									{{if (ne . $.AccessTokenScopePublicOnly)}}
 										<li>{{.}}</li>
@@ -41,7 +41,7 @@
 						</div>
 						<div class="flex-item-trailing">
 								<button class="ui red tiny button delete-button" data-modal-id="delete-token" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
-									{{svg "octicon-trash" 16 "gt-mr-2"}}
+									{{svg "octicon-trash" 16 "tw-mr-1"}}
 									{{ctx.Locale.Tr "settings.delete_token"}}
 								</button>
 						</div>
@@ -62,16 +62,16 @@
 				<div class="field">
 					<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
 					<label class="tw-cursor-pointer">
-						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
+						<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
 						{{ctx.Locale.Tr "settings.permissions_public_only"}}
 					</label>
 					<label class="tw-cursor-pointer">
-						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="" checked>
+						<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="" checked>
 						{{ctx.Locale.Tr "settings.permissions_access_all"}}
 					</label>
 				</div>
 				<details class="ui optional field">
-					<summary class="gt-pb-4 gt-pl-2">
+					<summary class="tw-pb-4 tw-pl-1">
 						{{ctx.Locale.Tr "settings.select_permissions"}}
 					</summary>
 					<p class="activity meta">
diff --git a/templates/user/settings/applications_oauth2_edit_form.tmpl b/templates/user/settings/applications_oauth2_edit_form.tmpl
index c0bddd55b3..f7ef115693 100644
--- a/templates/user/settings/applications_oauth2_edit_form.tmpl
+++ b/templates/user/settings/applications_oauth2_edit_form.tmpl
@@ -26,7 +26,7 @@
 		<form class="ui form ignore-dirty" action="{{.FormActionPath}}/regenerate_secret" method="post">
 			{{.CsrfTokenHtml}}
 			{{ctx.Locale.Tr "settings.oauth2_regenerate_secret_hint"}}
-			<button class="ui mini button gt-ml-3" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button>
+			<button class="ui mini button tw-ml-2" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button>
 		</form>
 	</div>
 </div>
diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl
index bfbebb104d..cfcb6d053d 100644
--- a/templates/user/settings/applications_oauth2_list.tmpl
+++ b/templates/user/settings/applications_oauth2_list.tmpl
@@ -21,12 +21,12 @@
 						<span class="ui basic label" data-tooltip-content="{{ctx.Locale.Tr "settings.oauth2_application_locked"}}">{{ctx.Locale.Tr "locked"}}</span>
 					{{else}}
 						<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button">
-							{{svg "octicon-pencil" 16 "gt-mr-2"}}
+							{{svg "octicon-pencil" 16 "tw-mr-1"}}
 							{{ctx.Locale.Tr "settings.oauth2_application_edit"}}
 						</a>
 						<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application"
 								data-url="{{$.Link}}/oauth2/{{.ID}}/delete">
-							{{svg "octicon-trash" 16 "gt-mr-2"}}
+							{{svg "octicon-trash" 16 "tw-mr-1"}}
 							{{ctx.Locale.Tr "settings.delete_key"}}
 						</button>
 					{{end}}
diff --git a/templates/user/settings/grants_oauth2.tmpl b/templates/user/settings/grants_oauth2.tmpl
index 92fea1306f..b5ae3e0337 100644
--- a/templates/user/settings/grants_oauth2.tmpl
+++ b/templates/user/settings/grants_oauth2.tmpl
@@ -29,7 +29,7 @@
 
 	<div class="ui g-modal-confirm delete modal" id="revoke-gitea-oauth2-grant">
 		<div class="header">
-			{{svg "octicon-shield" 16 "gt-mr-2"}}
+			{{svg "octicon-shield" 16 "tw-mr-1"}}
 			{{ctx.Locale.Tr "settings.revoke_oauth2_grant"}}
 		</div>
 		<div class="content">
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index 2f90d0bdad..8ee8ff0ee0 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -5,7 +5,7 @@
 	</div>
 </h4>
 <div class="ui attached segment">
-	<div class="{{if not .HasGPGError}}gt-hidden{{end}} gt-mb-4" id="add-gpg-key-panel">
+	<div class="{{if not .HasGPGError}}gt-hidden{{end}} tw-mb-4" id="add-gpg-key-panel">
 		<form class="ui form{{if .HasGPGError}} error{{end}}" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<input type="hidden" name="title" value="none">
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index 5577cd0ffd..f075a51983 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -7,7 +7,7 @@
 	</div>
 </h4>
 <div class="ui attached segment">
-	<div class="{{if not .HasSSHError}}gt-hidden{{end}} gt-mb-4" id="add-ssh-key-panel">
+	<div class="{{if not .HasSSHError}}gt-hidden{{end}} tw-mb-4" id="add-ssh-key-panel">
 		<form class="ui form" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="field {{if .Err_Title}}error{{end}}">
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index d1c68656b6..1beb8a3dfd 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -106,7 +106,7 @@
 						<label>{{ctx.Locale.Tr "settings.lookup_avatar_by_mail"}}</label>
 					</div>
 				</div>
-				<div class="field gt-pl-4 {{if .Err_Gravatar}}error{{end}}">
+				<div class="field tw-pl-4 {{if .Err_Gravatar}}error{{end}}">
 					<label for="gravatar">Avatar {{ctx.Locale.Tr "email"}}</label>
 					<input id="gravatar" name="gravatar" value="{{.SignedUser.AvatarEmail}}">
 				</div>
@@ -119,7 +119,7 @@
 					</div>
 				</div>
 
-				<div class="inline field gt-pl-4">
+				<div class="inline field tw-pl-4">
 					<label for="new-avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
 					<input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
 				</div>
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index 41cdae2968..c874ccd878 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -9,7 +9,7 @@
 					<div class="ui middle aligned divided list">
 						{{range $dirI, $dir := .Dirs}}
 							{{$repo := index $.ReposMap $dir}}
-							<div class="item {{if not $repo}}gt-py-2{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}}
+							<div class="item {{if not $repo}}tw-py-1{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}}
 								<div class="content">
 									{{if $repo}}
 										{{if $repo.IsPrivate}}
@@ -30,11 +30,11 @@
 											<span><a href="{{$repo.BaseRepo.Link}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span>
 										{{end}}
 									{{else}}
-										<span class="icon tw-inline-block gt-pt-3">{{svg "octicon-file-directory-fill"}}</span>
-										<span class="name tw-inline-block gt-pt-3">{{$.ContextUser.Name}}/{{$dir}}</span>
+										<span class="icon tw-inline-block tw-pt-2">{{svg "octicon-file-directory-fill"}}</span>
+										<span class="name tw-inline-block tw-pt-2">{{$.ContextUser.Name}}/{{$dir}}</span>
 										<div class="tw-float-right">
 											{{if $.allowAdopt}}
-												<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
+												<button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
 												<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
 													<div class="header">
 														<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
@@ -51,7 +51,7 @@
 												</div>
 											{{end}}
 											{{if $.allowDelete}}
-												<button class="ui button red show-modal gt-p-3" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button>
+												<button class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button>
 												<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
 													<div class="header">
 														<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
@@ -86,15 +86,15 @@
 							<div class="item">
 								<div class="content">
 									{{if .IsPrivate}}
-										{{svg "octicon-lock" 16 "gt-mr-2 iconFloat text gold"}}
+										{{svg "octicon-lock" 16 "tw-mr-1 iconFloat text gold"}}
 									{{else if .IsFork}}
-										{{svg "octicon-repo-forked" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-repo-forked" 16 "tw-mr-1 iconFloat"}}
 									{{else if .IsMirror}}
-										{{svg "octicon-mirror" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-mirror" 16 "tw-mr-1 iconFloat"}}
 									{{else if .IsTemplate}}
-										{{svg "octicon-repo-template" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-repo-template" 16 "tw-mr-1 iconFloat"}}
 									{{else}}
-										{{svg "octicon-repo" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-repo" 16 "tw-mr-1 iconFloat"}}
 									{{end}}
 									<a class="name" href="{{.Link}}">{{.OwnerName}}/{{.Name}}</a>
 									<span>{{FileSize .Size}}</span>
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index c9c051faf4..b29e897215 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -44,104 +44,6 @@ Gitea's private styles use `g-` prefix.
 .interact-bg:hover { background: var(--color-hover) !important; }
 .interact-bg:active { background: var(--color-active) !important; }
 
-.gt-m-0 { margin: 0 !important; }
-.gt-m-1 { margin: .125rem !important; }
-.gt-m-2 { margin: .25rem !important; }
-.gt-m-3 { margin: .5rem !important; }
-.gt-m-4 { margin: 1rem !important; }
-.gt-m-5 { margin: 2rem !important; }
-
-.gt-ml-0 { margin-left: 0 !important; }
-.gt-ml-1 { margin-left: .125rem !important; }
-.gt-ml-2 { margin-left: .25rem !important; }
-.gt-ml-3 { margin-left: .5rem !important; }
-.gt-ml-4 { margin-left: 1rem !important; }
-.gt-ml-5 { margin-left: 2rem !important; }
-
-.gt-mr-0 { margin-right: 0 !important; }
-.gt-mr-1 { margin-right: .125rem !important; }
-.gt-mr-2 { margin-right: .25rem !important; }
-.gt-mr-3 { margin-right: .5rem !important; }
-.gt-mr-4 { margin-right: 1rem !important; }
-.gt-mr-5 { margin-right: 2rem !important; }
-
-.gt-mt-0 { margin-top: 0 !important; }
-.gt-mt-1 { margin-top: .125rem !important; }
-.gt-mt-2 { margin-top: .25rem !important; }
-.gt-mt-3 { margin-top: .5rem !important; }
-.gt-mt-4 { margin-top: 1rem !important; }
-.gt-mt-5 { margin-top: 2rem !important; }
-
-.gt-mb-0 { margin-bottom: 0 !important; }
-.gt-mb-1 { margin-bottom: .125rem !important; }
-.gt-mb-2 { margin-bottom: .25rem !important; }
-.gt-mb-3 { margin-bottom: .5rem !important; }
-.gt-mb-4 { margin-bottom: 1rem !important; }
-.gt-mb-5 { margin-bottom: 2rem !important; }
-
-.gt-mx-0 { margin-left: 0 !important; margin-right: 0 !important; }
-.gt-mx-1 { margin-left: .125rem !important; margin-right: .125rem !important; }
-.gt-mx-2 { margin-left: .25rem !important; margin-right: .25rem !important; }
-.gt-mx-3 { margin-left: .5rem !important; margin-right: .5rem !important; }
-.gt-mx-4 { margin-left: 1rem !important; margin-right: 1rem !important; }
-.gt-mx-5 { margin-left: 2rem !important; margin-right: 2rem !important; }
-
-.gt-my-0 { margin-top: 0 !important; margin-bottom: 0 !important; }
-.gt-my-1 { margin-top: .125rem !important; margin-bottom: .125rem !important; }
-.gt-my-2 { margin-top: .25rem !important; margin-bottom: .25rem !important; }
-.gt-my-3 { margin-top: .5rem !important; margin-bottom: .5rem !important; }
-.gt-my-4 { margin-top: 1rem !important; margin-bottom: 1rem !important; }
-.gt-my-5 { margin-top: 2rem !important; margin-bottom: 2rem !important; }
-
-.gt-p-0 { padding: 0 !important; }
-.gt-p-1 { padding: .125rem !important; }
-.gt-p-2 { padding: .25rem !important; }
-.gt-p-3 { padding: .5rem !important; }
-.gt-p-4 { padding: 1rem !important; }
-.gt-p-5 { padding: 2rem !important; }
-
-.gt-pl-0 { padding-left: 0 !important; }
-.gt-pl-1 { padding-left: .125rem !important; }
-.gt-pl-2 { padding-left: .25rem !important; }
-.gt-pl-3 { padding-left: .5rem !important; }
-.gt-pl-4 { padding-left: 1rem !important; }
-.gt-pl-5 { padding-left: 2rem !important; }
-
-.gt-pr-0 { padding-right: 0 !important; }
-.gt-pr-1 { padding-right: .125rem !important; }
-.gt-pr-2 { padding-right: .25rem !important; }
-.gt-pr-3 { padding-right: .5rem !important; }
-.gt-pr-4 { padding-right: 1rem !important; }
-.gt-pr-5 { padding-right: 2rem !important; }
-
-.gt-pt-0 { padding-top: 0 !important; }
-.gt-pt-1 { padding-top: .125rem !important; }
-.gt-pt-2 { padding-top: .25rem !important; }
-.gt-pt-3 { padding-top: .5rem !important; }
-.gt-pt-4 { padding-top: 1rem !important; }
-.gt-pt-5 { padding-top: 2rem !important; }
-
-.gt-pb-0 { padding-bottom: 0 !important; }
-.gt-pb-1 { padding-bottom: .125rem !important; }
-.gt-pb-2 { padding-bottom: .25rem !important; }
-.gt-pb-3 { padding-bottom: .5rem !important; }
-.gt-pb-4 { padding-bottom: 1rem !important; }
-.gt-pb-5 { padding-bottom: 2rem !important; }
-
-.gt-px-0 { padding-left: 0 !important; padding-right: 0 !important; }
-.gt-px-1 { padding-left: .125rem !important; padding-right: .125rem !important; }
-.gt-px-2 { padding-left: .25rem !important; padding-right: .25rem !important; }
-.gt-px-3 { padding-left: .5rem !important; padding-right: .5rem !important; }
-.gt-px-4 { padding-left: 1rem !important; padding-right: 1rem !important; }
-.gt-px-5 { padding-left: 2rem !important; padding-right: 2rem !important; }
-
-.gt-py-0 { padding-top: 0 !important; padding-bottom: 0 !important; }
-.gt-py-1 { padding-top: .125rem !important; padding-bottom: .125rem !important; }
-.gt-py-2 { padding-top: .25rem !important; padding-bottom: .25rem !important; }
-.gt-py-3 { padding-top: .5rem !important; padding-bottom: .5rem !important; }
-.gt-py-4 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
-.gt-py-5 { padding-top: 2rem !important; padding-bottom: 2rem !important; }
-
 /*
 gt-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element.
 do not use:
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 0f8b43b395..ffdcef2bc8 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -348,7 +348,7 @@ export default sfc; // activate the IDE's Vue plugin
       <h4 class="ui top attached header tw-flex tw-items-center">
         <div class="tw-flex-1 tw-flex tw-items-center">
           {{ textMyRepos }}
-          <span class="ui grey label gt-ml-3">{{ reposTotalCount }}</span>
+          <span class="ui grey label tw-ml-2">{{ reposTotalCount }}</span>
         </div>
         <a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
           <svg-icon name="octicon-plus"/>
@@ -367,7 +367,7 @@ export default sfc; // activate the IDE's Vue plugin
                       otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
                   <input type="checkbox" class="hidden" v-bind.prop="checkboxArchivedFilterProps">
                   <label>
-                    <svg-icon name="octicon-archive" :size="16" class-name="gt-mr-2"/>
+                    <svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/>
                     {{ textShowArchived }}
                   </label>
                 </div>
@@ -376,7 +376,7 @@ export default sfc; // activate the IDE's Vue plugin
                 <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
                   <input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps">
                   <label>
-                    <svg-icon name="octicon-lock" :size="16" class-name="gt-mr-2"/>
+                    <svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/>
                     {{ textShowPrivate }}
                   </label>
                 </div>
@@ -411,7 +411,7 @@ export default sfc; // activate the IDE's Vue plugin
       </div>
       <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="tw-flex tw-items-center gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
+          <li class="tw-flex tw-items-center tw-py-2" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
             <a class="repo-list-link muted" :href="repo.link">
               <svg-icon :name="repoIcon(repo)" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ repo.full_name }}</div>
@@ -421,37 +421,37 @@ export default sfc; // activate the IDE's Vue plugin
             </a>
             <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
               <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
-              <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'gt-ml-3 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
+              <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
             </a>
           </li>
         </ul>
         <div v-if="showMoreReposLink" class="tw-text-center">
-          <div class="divider gt-my-0"/>
-          <div class="ui borderless pagination menu narrow gt-my-3">
+          <div class="divider tw-my-0"/>
+          <div class="ui borderless pagination menu narrow tw-my-2">
             <a
-              class="item navigation gt-py-2" :class="{'disabled': page === 1}"
+              class="item navigation tw-py-1" :class="{'disabled': page === 1}"
               @click="changePage(1)" :title="textFirstPage"
             >
-              <svg-icon name="gitea-double-chevron-left" :size="16" class-name="gt-mr-2"/>
+              <svg-icon name="gitea-double-chevron-left" :size="16" class-name="tw-mr-1"/>
             </a>
             <a
-              class="item navigation gt-py-2" :class="{'disabled': page === 1}"
+              class="item navigation tw-py-1" :class="{'disabled': page === 1}"
               @click="changePage(page - 1)" :title="textPreviousPage"
             >
-              <svg-icon name="octicon-chevron-left" :size="16" clsas-name="gt-mr-2"/>
+              <svg-icon name="octicon-chevron-left" :size="16" clsas-name="tw-mr-1"/>
             </a>
-            <a class="active item gt-py-2">{{ page }}</a>
+            <a class="active item tw-py-1">{{ page }}</a>
             <a
               class="item navigation" :class="{'disabled': page === finalPage}"
               @click="changePage(page + 1)" :title="textNextPage"
             >
-              <svg-icon name="octicon-chevron-right" :size="16" class-name="gt-ml-2"/>
+              <svg-icon name="octicon-chevron-right" :size="16" class-name="tw-ml-1"/>
             </a>
             <a
-              class="item navigation gt-py-2" :class="{'disabled': page === finalPage}"
+              class="item navigation tw-py-1" :class="{'disabled': page === finalPage}"
               @click="changePage(finalPage)" :title="textLastPage"
             >
-              <svg-icon name="gitea-double-chevron-right" :size="16" class-name="gt-ml-2"/>
+              <svg-icon name="gitea-double-chevron-right" :size="16" class-name="tw-ml-1"/>
             </a>
           </div>
         </div>
@@ -461,7 +461,7 @@ export default sfc; // activate the IDE's Vue plugin
       <h4 class="ui top attached header tw-flex tw-items-center">
         <div class="tw-flex-1 tw-flex tw-items-center">
           {{ textMyOrgs }}
-          <span class="ui grey label gt-ml-3">{{ organizationsTotalCount }}</span>
+          <span class="ui grey label tw-ml-2">{{ organizationsTotalCount }}</span>
         </div>
         <a class="tw-flex tw-items-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
           <svg-icon name="octicon-plus"/>
@@ -469,7 +469,7 @@ export default sfc; // activate the IDE's Vue plugin
       </h4>
       <div v-if="organizations.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="tw-flex tw-items-center gt-py-3" v-for="org in organizations" :key="org.name">
+          <li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name">
             <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
               <svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ org.name }}</div>
@@ -479,9 +479,9 @@ export default sfc; // activate the IDE's Vue plugin
                 </span>
               </div>
             </a>
-            <div class="text light grey tw-flex tw-items-center gt-ml-3">
+            <div class="text light grey tw-flex tw-items-center tw-ml-2">
               {{ org.num_repos }}
-              <svg-icon name="octicon-repo" :size="16" class-name="gt-ml-2 gt-mt-1"/>
+              <svg-icon name="octicon-repo" :size="16" class-name="tw-ml-1 tw-mt-0.5"/>
             </div>
           </li>
         </ul>
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index 35245f2190..d58337e093 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -208,7 +208,7 @@ export default {
         <div class="gt-ellipsis">
           {{ locale.show_all_commits }}
         </div>
-        <div class="gt-ellipsis text light-2 gt-mb-0">
+        <div class="gt-ellipsis text light-2 tw-mb-0">
           {{ locale.stats_num_commits }}
         </div>
       </div>
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 66fe49c50b..bc6f1bee7d 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -36,12 +36,12 @@ export default {
 };
 </script>
 <template>
-  <ol class="diff-stats gt-m-0" ref="root" v-if="store.fileListIsVisible">
+  <ol class="diff-stats tw-m-0" ref="root" v-if="store.fileListIsVisible">
     <li v-for="file in store.files" :key="file.NameHash">
       <div class="tw-font-semibold tw-flex tw-items-center pull-right">
-        <span v-if="file.IsBin" class="gt-ml-1 gt-mr-3">{{ store.binaryFileMessage }}</span>
+        <span v-if="file.IsBin" class="tw-ml-0.5 tw-mr-2">{{ store.binaryFileMessage }}</span>
         {{ file.IsBin ? '' : file.Addition + file.Deletion }}
-        <span v-if="!file.IsBin" class="diff-stats-bar gt-mx-3" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
+        <span v-if="!file.IsBin" class="diff-stats-bar tw-mx-2" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
           <div class="diff-stats-add-bar" :style="{ 'width': diffStatsWidth(file.Addition, file.Deletion) }"/>
         </span>
       </div>
@@ -49,7 +49,7 @@ export default {
       <span :class="['status', diffTypeToString(file.Type)]" :data-tooltip-content="diffTypeToString(file.Type)">&nbsp;</span>
       <a class="file gt-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
     </li>
-    <li v-if="store.isIncomplete" class="gt-pt-2">
+    <li v-if="store.isIncomplete" class="tw-pt-1">
       <span class="file tw-flex tw-items-center tw-justify-between">{{ store.tooManyFilesMessage }}
         <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
       </span>
diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue
index 83d57b00d1..cddfee1e04 100644
--- a/web_src/js/components/DiffFileTree.vue
+++ b/web_src/js/components/DiffFileTree.vue
@@ -129,7 +129,7 @@ export default {
   <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
     <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
     <DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item"/>
-    <div v-if="store.isIncomplete" class="gt-pt-2">
+    <div v-if="store.isIncomplete" class="tw-pt-1">
       <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
     </div>
   </div>
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index 35acbdf74f..bd0901a7b5 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -108,7 +108,7 @@ export default {
         <div class="field">
           <textarea name="merge_message_field" rows="5" :placeholder="mergeForm.mergeMessageFieldPlaceHolder" v-model="mergeMessageFieldValue"/>
           <template v-if="mergeMessageFieldValue !== mergeForm.defaultMergeMessage">
-            <button @click.prevent="clearMergeMessage" class="btn gt-mt-2 gt-p-2 interact-fg" :data-tooltip-content="mergeForm.textClearMergeMessageHint">
+            <button @click.prevent="clearMergeMessage" class="btn tw-mt-1 tw-p-1 interact-fg" :data-tooltip-content="mergeForm.textClearMergeMessageHint">
               {{ mergeForm.textClearMergeMessage }}
             </button>
           </template>
@@ -130,7 +130,7 @@ export default {
         {{ mergeForm.textCancel }}
       </button>
 
-      <div class="ui checkbox gt-ml-2" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed">
+      <div class="ui checkbox tw-ml-1" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed">
         <input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge">
         <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label>
       </div>
@@ -177,7 +177,7 @@ export default {
       </div>
 
       <!-- the cancel auto merge button -->
-      <form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="gt-ml-4">
+      <form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="tw-ml-4">
         <input type="hidden" name="_csrf" :value="csrfToken">
         <button class="ui button">
           {{ mergeForm.textAutoMergeCancelSchedule }}
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 83933ef24d..26dffcda9e 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -377,7 +377,7 @@ export function initRepositoryActionView() {
         <button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
           {{ locale.cancel }}
         </button>
-        <button class="ui basic small compact button gt-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
+        <button class="ui basic small compact button tw-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
           {{ locale.rerun_all }}
         </button>
       </div>
@@ -398,10 +398,10 @@ export function initRepositoryActionView() {
             <a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="parseInt(jobIndex) === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id" @mouseenter="onHoverRerunIndex = job.id" @mouseleave="onHoverRerunIndex = -1">
               <div class="job-brief-item-left">
                 <ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/>
-                <span class="job-brief-name gt-mx-3 gt-ellipsis">{{ job.name }}</span>
+                <span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
               </div>
               <span class="job-brief-item-right">
-                <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun gt-mx-3 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/>
+                <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/>
                 <span class="step-summary-duration">{{ job.duration }}</span>
               </span>
             </a>
@@ -436,7 +436,7 @@ export function initRepositoryActionView() {
           </div>
           <div class="job-info-header-right">
             <div class="ui top right pointing dropdown custom jump item" @click.stop="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
-              <button class="btn gt-interact-bg gt-p-3">
+              <button class="btn gt-interact-bg tw-p-2">
                 <SvgIcon name="octicon-gear" :size="18"/>
               </button>
               <div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
@@ -467,9 +467,9 @@ export function initRepositoryActionView() {
               <!-- If the job is done and the job step log is loaded for the first time, show the loading icon
                 currentJobStepsStates[i].cursor === null means the log is loaded for the first time
               -->
-              <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
-              <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['gt-mr-3', !isExpandable(jobStep.status) && 'tw-invisible']"/>
-              <ActionRunStatus :status="jobStep.status" class="gt-mr-3"/>
+              <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+              <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['tw-mr-2', !isExpandable(jobStep.status) && 'tw-invisible']"/>
+              <ActionRunStatus :status="jobStep.status" class="tw-mr-2"/>
 
               <span class="step-summary-msg gt-ellipsis">{{ jobStep.summary }}</span>
               <span class="step-summary-duration">{{ jobStep.duration }}</span>
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index 34e8859609..d297503b2e 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -245,13 +245,13 @@ export default sfc; // activate IDE's Vue plugin
 </script>
 <template>
   <div class="ui dropdown custom">
-    <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex gt-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
-      <span class="text tw-flex tw-items-center gt-mr-2">
+    <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
+      <span class="text tw-flex tw-items-center tw-mr-1">
         <template v-if="release">{{ textReleaseCompare }}</template>
         <template v-else>
           <svg-icon v-if="isViewTag" name="octicon-tag"/>
           <svg-icon v-else name="octicon-git-branch"/>
-          <strong ref="dropdownRefName" class="gt-ml-3">{{ refNameText }}</strong>
+          <strong ref="dropdownRefName" class="tw-ml-2">{{ refNameText }}</strong>
         </template>
       </span>
       <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
@@ -263,10 +263,10 @@ export default sfc; // activate IDE's Vue plugin
       </div>
       <div v-if="showBranchesInDropdown" class="branch-tag-tab">
         <a class="branch-tag-item muted" :class="{active: mode === 'branches'}" href="#" @click="handleTabSwitch('branches')">
-          <svg-icon name="octicon-git-branch" :size="16" class-name="gt-mr-2"/>{{ textBranches }}
+          <svg-icon name="octicon-git-branch" :size="16" class-name="tw-mr-1"/>{{ textBranches }}
         </a>
         <a v-if="!noTag" class="branch-tag-item muted" :class="{active: mode === 'tags'}" href="#" @click="handleTabSwitch('tags')">
-          <svg-icon name="octicon-tag" :size="16" class-name="gt-mr-2"/>{{ textTags }}
+          <svg-icon name="octicon-tag" :size="16" class-name="tw-mr-1"/>{{ textTags }}
         </a>
       </div>
       <div class="branch-tag-divider"/>
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
index f51dac0a6d..adce431264 100644
--- a/web_src/js/components/RepoCodeFrequency.vue
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -150,7 +150,7 @@ export default {
     <div class="tw-flex ui segment main-graph">
       <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
-          <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
+          <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
           {{ locale.loadingInfo }}
         </div>
         <div v-else class="text red">
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 02db9e3e3e..2347c41ae4 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -355,7 +355,7 @@ export default {
     <div class="tw-flex ui segment main-graph">
       <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
-          <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
+          <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
           {{ locale.loadingInfo }}
         </div>
         <div v-else class="text red">
@@ -379,7 +379,7 @@ export default {
           <a :href="contributor.home_link">
             <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link">
           </a>
-          <div class="gt-ml-3">
+          <div class="tw-ml-2">
             <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
             <h4 v-else class="contributor-name">
               {{ contributor.name }}
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
index 601252419a..502af533da 100644
--- a/web_src/js/components/RepoRecentCommits.vue
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -127,7 +127,7 @@ export default {
     <div class="tw-flex ui segment main-graph">
       <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
         <div v-if="isLoading">
-          <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
+          <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
           {{ locale.loadingInfo }}
         </div>
         <div v-else class="text red">
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
index ae4e8299f2..103cc525ad 100644
--- a/web_src/js/components/ScopedAccessTokenSelector.vue
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -87,7 +87,7 @@ export function initScopedAccessTokenCategories() {
 
 </script>
 <template>
-  <div v-for="category in categories" :key="category" class="field gt-pl-2 gt-pb-2 access-token-category">
+  <div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category">
     <label class="category-label" :for="'access-token-scope-' + category">
       {{ category }}
     </label>
diff --git a/web_src/js/features/repo-diff-commit.js b/web_src/js/features/repo-diff-commit.js
index f0466f9320..aa7fc38360 100644
--- a/web_src/js/features/repo-diff-commit.js
+++ b/web_src/js/features/repo-diff-commit.js
@@ -35,7 +35,7 @@ function addBranches(area, branches, defaultBranch) {
 
 function addLink(parent, href, text, tooltip) {
   const link = document.createElement('a');
-  link.classList.add('muted', 'gt-px-2');
+  link.classList.add('muted', 'tw-px-1');
   link.href = href;
   link.textContent = text;
   if (tooltip) {
diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js
index cb03d9e803..0411b51f9c 100644
--- a/web_src/js/features/repo-findfile.js
+++ b/web_src/js/features/repo-findfile.js
@@ -83,7 +83,7 @@ function filterRepoFiles(filter) {
     const cell = document.createElement('td');
     const a = document.createElement('a');
     a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`);
-    a.innerHTML = svg('octicon-file', 16, 'gt-mr-3');
+    a.innerHTML = svg('octicon-file', 16, 'tw-mr-2');
     row.append(cell);
     cell.append(a);
     for (const [index, part] of r.matchResult.entries()) {
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index 6ac7b96b9e..74304d7f4a 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -47,7 +47,7 @@ export function initRepoTopicBar() {
           const topicArray = topics.split(',');
           topicArray.sort();
           for (const topic of topicArray) {
-            const $link = $('<a class="ui repo-topic large label topic gt-m-0"></a>');
+            const $link = $('<a class="ui repo-topic large label topic tw-m-0"></a>');
             $link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`);
             $link.text(topic);
             $link.insertBefore($mgrBtn); // insert all new topics before manage button
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index e7768b066e..9d51ab6b8d 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -18,7 +18,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
   ${svg('octicon-x', 16, 'close icon inside')}
   <div class="header tw-flex tw-items-center tw-justify-between">
     <div>${itemTitleHtml}</div>
-    <div class="ui dropdown dialog-header-options gt-mr-5 gt-hidden">
+    <div class="ui dropdown dialog-header-options tw-mr-8 gt-hidden">
       ${i18nTextOptions}
       ${svg('octicon-triangle-down', 14, 'dropdown icon')}
       <div class="menu">
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 4bdd5e5a8e..9681e648d5 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -109,7 +109,7 @@ function initRepoIssueListAuthorDropdown() {
         const processedResults = []; // to be used by dropdown to generate menu items
         for (const item of resp.results) {
           let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
-          if (item.full_name) html += `<span class="search-fullname gt-ml-3">${htmlEscape(item.full_name)}</span>`;
+          if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`;
           processedResults.push({value: item.user_id, name: html});
         }
         resp.results = processedResults;
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 18978e9e29..43567f4393 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -272,11 +272,11 @@ export function initRepoCommentForm() {
 
       let icon = '';
       if (input_id === '#milestone_id') {
-        icon = svg('octicon-milestone', 18, 'gt-mr-3');
+        icon = svg('octicon-milestone', 18, 'tw-mr-2');
       } else if (input_id === '#project_id') {
-        icon = svg('octicon-project', 18, 'gt-mr-3');
+        icon = svg('octicon-project', 18, 'tw-mr-2');
       } else if (input_id === '#assignee_id') {
-        icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
+        icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
       }
 
       $list.find('.selected').html(`
diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js
index 70a5de6913..02cd484374 100644
--- a/web_src/js/features/tribute.js
+++ b/web_src/js/features/tribute.js
@@ -36,7 +36,7 @@ function makeCollections({mentions, emoji}) {
       menuItemTemplate: (item) => {
         return `
           <div class="tribute-item">
-            <img src="${htmlEscape(item.original.avatar)}" class="gt-mr-3"/>
+            <img src="${htmlEscape(item.original.avatar)}" class="tw-mr-2"/>
             <span class="name">${htmlEscape(item.original.name)}</span>
             ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''}
           </div>

From 8d93cea2969730fc8f3bdeb3704a3b89db0bbcc0 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 17:48:06 +0100
Subject: [PATCH 505/679] Remove fomantic segment module (#30042)

Another CSS-only module. Also, I re-ordered the imports based on
[original fomantic
order](https://github.com/fomantic/Fomantic-UI/blob/2.8.7/src/semantic.less).
---
 web_src/css/base.css                |  45 +-
 web_src/css/index.css               |  18 +-
 web_src/css/modules/segment.css     | 195 ++++++++
 web_src/fomantic/build/semantic.css | 668 ----------------------------
 web_src/fomantic/semantic.json      |   1 -
 5 files changed, 207 insertions(+), 720 deletions(-)
 create mode 100644 web_src/css/modules/segment.css

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 51a4852e7e..3f46e4cd1a 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -360,6 +360,7 @@ ol.ui.list li,
 .ui.vertical.menu {
   background: var(--color-menu);
   border-color: var(--color-secondary);
+  box-shadow: none;
 }
 
 .ui.menu .item {
@@ -576,14 +577,6 @@ ol.ui.list li,
   visibility: visible !important;
 }
 
-.ui.error.segment {
-  border-color: var(--color-error-border) !important;
-}
-
-.ui.warning.segment {
-  border-color: var(--color-warning-border) !important;
-}
-
 .ui.selection.active.dropdown,
 .ui.selection.active.dropdown:hover,
 .ui.selection.active.dropdown .menu,
@@ -930,12 +923,6 @@ input:-webkit-autofill:active,
   background-color: transparent;
 }
 
-.ui.menu,
-.ui.vertical.menu,
-.ui.segment {
-  box-shadow: none;
-}
-
 /* replace fomantic popover box shadows */
 .ui.dropdown .menu,
 .ui.upward.dropdown > .menu,
@@ -1029,19 +1016,6 @@ input:-webkit-autofill:active,
   vertical-align: middle;
 }
 
-.ui .info.segment.top h3,
-.ui .info.segment.top h4 {
-  margin-top: 0;
-}
-
-.ui .info.segment.top h3:last-child {
-  margin-top: 4px;
-}
-
-.ui .info.segment.top > :last-child {
-  margin-bottom: 0;
-}
-
 .ui .form .autofill-dummy {
   position: absolute;
   width: 1px;
@@ -1678,23 +1652,6 @@ a.ui.basic.label:hover {
   margin-left: 0;
 }
 
-.ui.segment,
-.ui.segments,
-.ui.attached.segment {
-  background: var(--color-box-body);
-  color: var(--color-text);
-  border-color: var(--color-secondary);
-}
-
-.ui.segments > .segment {
-  border-color: var(--color-secondary);
-}
-
-.ui.secondary.segment {
-  background: var(--color-secondary-bg);
-  color: var(--color-text-light);
-}
-
 .rss-icon {
   display: inline-flex;
   color: var(--color-text-light-1);
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 7ba19e62d4..4258b85797 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -1,21 +1,25 @@
 @import "./modules/normalize.css";
 @import "./modules/animations.css";
-@import "./modules/grid.css";
+
+/* fomantic replacements */
 @import "./modules/button.css";
+@import "./modules/container.css";
+@import "./modules/divider.css";
+@import "./modules/header.css";
+@import "./modules/segment.css";
+@import "./modules/grid.css";
+@import "./modules/message.css";
+@import "./modules/card.css";
+@import "./modules/modal.css";
+
 @import "./modules/select.css";
 @import "./modules/tippy.css";
-@import "./modules/modal.css";
 @import "./modules/breadcrumb.css";
-@import "./modules/card.css";
 @import "./modules/comment.css";
 @import "./modules/navbar.css";
 @import "./modules/toast.css";
-@import "./modules/divider.css";
 @import "./modules/svg.css";
 @import "./modules/flexcontainer.css";
-@import "./modules/message.css";
-@import "./modules/container.css";
-@import "./modules/header.css";
 
 @import "./shared/flex-list.css";
 @import "./shared/milestone.css";
diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css
new file mode 100644
index 0000000000..8bdd25bfe7
--- /dev/null
+++ b/web_src/css/modules/segment.css
@@ -0,0 +1,195 @@
+/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.segment {
+  position: relative;
+  margin: 1rem 0;
+  padding: 1em;
+  border-radius: 0.28571429rem;
+  border: 1px solid var(--color-secondary);
+  background: var(--color-box-body);
+  color: var(--color-text);
+}
+.ui.segment:first-child {
+  margin-top: 0;
+}
+.ui.segment:last-child {
+  margin-bottom: 0;
+}
+
+.ui.grid.segment {
+  margin: 1rem 0;
+  border-radius: 0.28571429rem;
+}
+
+.ui.segment.tab:last-child {
+  margin-bottom: 1rem;
+}
+
+.ui.segments {
+  flex-direction: column;
+  position: relative;
+  margin: 1rem 0;
+  border: 1px solid var(--color-secondary);
+  border-radius: 0.28571429rem;
+  background: var(--color-box-body);
+  color: var(--color-text);
+}
+.ui.segments:first-child {
+  margin-top: 0;
+}
+.ui.segments:last-child {
+  margin-bottom: 0;
+}
+
+.ui.segments > .segment {
+  top: 0;
+  bottom: 0;
+  border-radius: 0;
+  margin: 0;
+  width: auto;
+  box-shadow: none;
+  border: none;
+  border-top: 1px solid var(--color-secondary);
+}
+.ui.segments:not(.horizontal) > .segment:first-child {
+  top: 0;
+  bottom: 0;
+  border-top: none;
+  margin-top: 0;
+  margin-bottom: 0;
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.segments:not(.horizontal) > .segment:last-child {
+  top: 0;
+  bottom: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.segments:not(.horizontal) > .segment:only-child {
+  border-radius: 0.28571429rem;
+}
+
+.ui.segments > .ui.segments {
+  border-top: 1px solid var(--color-secondary);
+  margin: 1rem;
+}
+.ui.segments > .segments:first-child {
+  border-top: none;
+}
+.ui.segments > .segment + .segments:not(.horizontal) {
+  margin-top: 0;
+}
+
+.ui.horizontal.segments {
+  display: flex;
+  flex-direction: row;
+  background-color: transparent;
+  padding: 0;
+  margin: 1rem 0;
+  border-radius: 0.28571429rem;
+  border: 1px solid var(--color-secondary);
+}
+
+.ui.horizontal.segments > .segment {
+  margin: 0;
+  min-width: 0;
+  border-radius: 0;
+  border: none;
+  box-shadow: none;
+  border-left: 1px solid var(--color-secondary);
+}
+
+.ui.segments > .horizontal.segments:first-child {
+  border-top: none;
+}
+.ui.horizontal.segments:not(.stackable) > .segment:first-child {
+  border-left: none;
+}
+.ui.horizontal.segments > .segment:first-child {
+  border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui.horizontal.segments > .segment:last-child {
+  border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.clearing.segment::after {
+  content: "";
+  display: block;
+  clear: both;
+}
+
+.ui[class*="left aligned"].segment {
+  text-align: left;
+}
+.ui[class*="center aligned"].segment {
+  text-align: center;
+}
+
+.ui.secondary.segment {
+  background: var(--color-secondary-bg);
+  color: var(--color-text-light);
+}
+
+.ui.attached.segment {
+  top: 0;
+  bottom: 0;
+  border-radius: 0;
+  margin: 0 -1px;
+  width: calc(100% + 2px);
+  max-width: calc(100% + 2px);
+  box-shadow: none;
+  border: 1px solid var(--color-secondary);
+  background: var(--color-box-body);
+  color: var(--color-text);
+}
+.ui.attached:not(.message) + .ui.attached.segment:not(.top) {
+  border-top: none;
+}
+
+.ui[class*="top attached"].segment {
+  bottom: 0;
+  margin-bottom: 0;
+  top: 0;
+  margin-top: 1rem;
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+.ui.segment[class*="top attached"]:first-child {
+  margin-top: 0;
+}
+
+.ui.segment[class*="bottom attached"] {
+  bottom: 0;
+  margin-top: 0;
+  top: 0;
+  margin-bottom: 1rem;
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+.ui.segment[class*="bottom attached"]:last-child {
+  margin-bottom: 1rem;
+}
+
+.ui.fitted.segment:not(.horizontally) {
+  padding-top: 0;
+  padding-bottom: 0;
+}
+.ui.fitted.segment:not(.vertically) {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.ui.segments .segment,
+.ui.segment {
+  font-size: 1rem;
+}
+
+.ui.error.segment {
+  border-color: var(--color-error-border) !important;
+}
+
+.ui.warning.segment {
+  border-color: var(--color-warning-border) !important;
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index d9abf343b8..5421641da8 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -13392,674 +13392,6 @@ Floated Menu / Item
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Segment
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Segment
-*******************************/
-
-.ui.segment {
-  position: relative;
-  background: #FFFFFF;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-  margin: 1rem 0;
-  padding: 1em 1em;
-  border-radius: 0.28571429rem;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.segment:first-child {
-  margin-top: 0;
-}
-
-.ui.segment:last-child {
-  margin-bottom: 0;
-}
-
-/* Vertical */
-
-.ui.vertical.segment {
-  margin: 0;
-  padding-left: 0;
-  padding-right: 0;
-  background: none transparent;
-  border-radius: 0;
-  box-shadow: none;
-  border: none;
-  border-bottom: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.vertical.segment:last-child {
-  border-bottom: none;
-}
-
-/*-------------------
-    Loose Coupling
---------------------*/
-
-/* Label */
-
-.ui[class*="bottom attached"].segment > [class*="top attached"].label {
-  border-top-left-radius: 0;
-  border-top-right-radius: 0;
-}
-
-.ui[class*="top attached"].segment > [class*="bottom attached"].label {
-  border-bottom-left-radius: 0;
-  border-bottom-right-radius: 0;
-}
-
-.ui.attached.segment:not(.top):not(.bottom) > [class*="top attached"].label {
-  border-top-left-radius: 0;
-  border-top-right-radius: 0;
-}
-
-.ui.attached.segment:not(.top):not(.bottom) > [class*="bottom attached"].label {
-  border-bottom-left-radius: 0;
-  border-bottom-right-radius: 0;
-}
-
-/* Grid */
-
-.ui.page.grid.segment,
-.ui.grid > .row > .ui.segment.column,
-.ui.grid > .ui.segment.column {
-  padding-top: 2em;
-  padding-bottom: 2em;
-}
-
-.ui.grid.segment {
-  margin: 1rem 0;
-  border-radius: 0.28571429rem;
-}
-
-/* Table */
-
-.ui.basic.table.segment {
-  background: #FFFFFF;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui[class*="very basic"].table.segment {
-  padding: 1em 1em;
-}
-
-/* Tab */
-
-.ui.segment.tab:last-child {
-  margin-bottom: 1rem;
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*-------------------
-       Placeholder
-  --------------------*/
-
-.ui.placeholder.segment {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: stretch;
-  max-width: initial;
-  animation: none;
-  overflow: visible;
-  padding: 1em 1em;
-  min-height: 18rem;
-  background: #F9FAFB;
-  border-color: rgba(34, 36, 38, 0.15);
-  box-shadow: 0 2px 25px 0 rgba(34, 36, 38, 0.05) inset;
-}
-
-.ui.placeholder.segment .button,
-.ui.placeholder.segment textarea {
-  display: block;
-}
-
-.ui.placeholder.segment .field,
-.ui.placeholder.segment textarea,
-.ui.placeholder.segment > .ui.input,
-.ui.placeholder.segment .button {
-  max-width: 15rem;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.ui.placeholder.segment .column .button,
-.ui.placeholder.segment .column .field,
-.ui.placeholder.segment .column textarea,
-.ui.placeholder.segment .column > .ui.input {
-  max-width: 15rem;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.ui.placeholder.segment > .inline {
-  align-self: center;
-}
-
-.ui.placeholder.segment > .inline > .button {
-  display: inline-block;
-  width: auto;
-  margin: 0 0.35714286rem 0 0;
-}
-
-.ui.placeholder.segment > .inline > .button:last-child {
-  margin-right: 0;
-}
-
-/*-------------------
-         Padded
-  --------------------*/
-
-.ui.padded.segment {
-  padding: 1.5em;
-}
-
-.ui[class*="very padded"].segment {
-  padding: 3em;
-}
-
-/* Padded vertical */
-
-.ui.padded.segment.vertical.segment,
-.ui[class*="very padded"].vertical.segment {
-  padding-left: 0;
-  padding-right: 0;
-}
-
-/*-------------------
-         Compact
-  --------------------*/
-
-.ui.compact.segment {
-  display: table;
-}
-
-/* Compact Group */
-
-.ui.compact.segments {
-  display: inline-flex;
-}
-
-.ui.compact.segments .segment,
-.ui.segments .compact.segment {
-  display: block;
-  flex: 0 1 auto;
-}
-
-/*-------------------
-         Circular
-  --------------------*/
-
-.ui.circular.segment {
-  display: table-cell;
-  padding: 2em;
-  text-align: center;
-  vertical-align: middle;
-  border-radius: 500em;
-}
-
-/*-------------------
-         Raised
-  --------------------*/
-
-.ui.raised.raised.segments,
-.ui.raised.raised.segment {
-  box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-/*******************************
-              Groups
-  *******************************/
-
-/* Group */
-
-.ui.segments {
-  flex-direction: column;
-  position: relative;
-  margin: 1rem 0;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-  border-radius: 0.28571429rem;
-}
-
-.ui.segments:first-child {
-  margin-top: 0;
-}
-
-.ui.segments:last-child {
-  margin-bottom: 0;
-}
-
-/* Nested Segment */
-
-.ui.segments > .segment {
-  top: 0;
-  bottom: 0;
-  border-radius: 0;
-  margin: 0;
-  width: auto;
-  box-shadow: none;
-  border: none;
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.segments:not(.horizontal) > .segment:first-child {
-  top: 0;
-  bottom: 0;
-  border-top: none;
-  margin-top: 0;
-  margin-bottom: 0;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-/* Bottom */
-
-.ui.segments:not(.horizontal) > .segment:last-child {
-  top: 0;
-  bottom: 0;
-  margin-top: 0;
-  margin-bottom: 0;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), none;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-/* Only */
-
-.ui.segments:not(.horizontal) > .segment:only-child {
-  border-radius: 0.28571429rem;
-}
-
-/* Nested Group */
-
-.ui.segments > .ui.segments {
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-  margin: 1rem 1rem;
-}
-
-.ui.segments > .segments:first-child {
-  border-top: none;
-}
-
-.ui.segments > .segment + .segments:not(.horizontal) {
-  margin-top: 0;
-}
-
-/* Horizontal Group */
-
-.ui.horizontal.segments {
-  display: flex;
-  flex-direction: row;
-  background-color: transparent;
-  padding: 0;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-  margin: 1rem 0;
-  border-radius: 0.28571429rem;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.stackable.horizontal.segments {
-  flex-wrap: wrap;
-}
-
-/* Nested Horizontal Group */
-
-.ui.segments > .horizontal.segments {
-  margin: 0;
-  background-color: transparent;
-  border-radius: 0;
-  border: none;
-  box-shadow: none;
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/* Horizontal Segment */
-
-.ui.horizontal.segments:not(.compact) > .segment:not(.compact) {
-  flex: 1 1 auto;
-  -ms-flex: 1 1 0;
-  /* Solves #2550 MS Flex */
-}
-
-.ui.horizontal.segments > .segment {
-  margin: 0;
-  min-width: 0;
-  border-radius: 0;
-  border: none;
-  box-shadow: none;
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/* Border Fixes */
-
-.ui.segments > .horizontal.segments:first-child {
-  border-top: none;
-}
-
-.ui.horizontal.segments:not(.stackable) > .segment:first-child {
-  border-left: none;
-}
-
-.ui.horizontal.segments > .segment:first-child {
-  border-radius: 0.28571429rem 0 0 0.28571429rem;
-}
-
-.ui.horizontal.segments > .segment:last-child {
-  border-radius: 0 0.28571429rem 0.28571429rem 0;
-}
-
-/*******************************
-            States
-*******************************/
-
-/*--------------
-      Disabled
-  ---------------*/
-
-.ui.disabled.segment {
-  opacity: var(--opacity-disabled);
-  color: rgba(40, 40, 40, 0.3);
-}
-
-/*--------------
-      Loading
-  ---------------*/
-
-.ui.loading.segment {
-  position: relative;
-  cursor: default;
-  pointer-events: none;
-  text-shadow: none !important;
-  transition: all 0s linear;
-}
-
-.ui.loading.segment:before {
-  position: absolute;
-  content: '';
-  top: 0;
-  left: 0;
-  background: rgba(255, 255, 255, 0.8);
-  width: 100%;
-  height: 100%;
-  border-radius: 0.28571429rem;
-  z-index: 100;
-}
-
-.ui.loading.segment:after {
-  position: absolute;
-  content: '';
-  top: 50%;
-  left: 50%;
-  margin: -1.5em 0 0 -1.5em;
-  width: 3em;
-  height: 3em;
-  animation: loader 0.6s infinite linear;
-  border: 0.2em solid #767676;
-  border-radius: 500rem;
-  box-shadow: 0 0 0 1px transparent;
-  visibility: visible;
-  z-index: 101;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-         Basic
-  --------------------*/
-
-.ui.basic.segment,
-.ui.segments .ui.basic.segment,
-.ui.basic.segments {
-  background: none transparent;
-  box-shadow: none;
-  border: none;
-  border-radius: 0;
-}
-
-/*-------------------
-         Clearing
-  --------------------*/
-
-.ui.clearing.segment:after {
-  content: "";
-  display: block;
-  clear: both;
-}
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.red.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #DB2828;
-}
-
-.ui.orange.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #F2711C;
-}
-
-.ui.yellow.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #FBBD08;
-}
-
-.ui.olive.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #B5CC18;
-}
-
-.ui.green.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #21BA45;
-}
-
-.ui.teal.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #00B5AD;
-}
-
-.ui.blue.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #2185D0;
-}
-
-.ui.violet.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #6435C9;
-}
-
-.ui.purple.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #A333C8;
-}
-
-.ui.pink.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #E03997;
-}
-
-.ui.brown.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #A5673F;
-}
-
-.ui.grey.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #767676;
-}
-
-.ui.black.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #1B1C1D;
-}
-
-/*-------------------
-         Aligned
-  --------------------*/
-
-.ui[class*="left aligned"].segment {
-  text-align: left;
-}
-
-.ui[class*="right aligned"].segment {
-  text-align: right;
-}
-
-.ui[class*="center aligned"].segment {
-  text-align: center;
-}
-
-/*-------------------
-         Floated
-  --------------------*/
-
-.ui.floated.segment,
-.ui[class*="left floated"].segment {
-  float: left;
-  margin-right: 1em;
-}
-
-.ui[class*="right floated"].segment {
-  float: right;
-  margin-left: 1em;
-}
-
-/*-------------------
-     Emphasis
---------------------*/
-
-/* Secondary */
-
-.ui.secondary.segment {
-  background: #F3F4F5;
-  color: rgba(0, 0, 0, 0.6);
-}
-
-/* Tertiary */
-
-.ui.tertiary.segment {
-  background: #DCDDDE;
-  color: rgba(0, 0, 0, 0.6);
-}
-
-/*-------------------
-        Attached
-  --------------------*/
-
-/* Middle */
-
-.ui.attached.segment {
-  top: 0;
-  bottom: 0;
-  border-radius: 0;
-  margin: 0 -1px;
-  width: calc(100% + 2px);
-  max-width: calc(100% + 2px);
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-}
-
-.ui.attached:not(.message) + .ui.attached.segment:not(.top) {
-  border-top: none;
-}
-
-/* Top */
-
-.ui[class*="top attached"].segment {
-  bottom: 0;
-  margin-bottom: 0;
-  top: 0;
-  margin-top: 1rem;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-.ui.segment[class*="top attached"]:first-child {
-  margin-top: 0;
-}
-
-/* Bottom */
-
-.ui.segment[class*="bottom attached"] {
-  bottom: 0;
-  margin-top: 0;
-  top: 0;
-  margin-bottom: 1rem;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), none;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-.ui.segment[class*="bottom attached"]:last-child {
-  margin-bottom: 1rem;
-}
-
-/*--------------
-       Fitted
-  ---------------*/
-
-.ui.fitted.segment:not(.horizontally) {
-  padding-top: 0;
-  padding-bottom: 0;
-}
-
-.ui.fitted.segment:not(.vertically) {
-  padding-left: 0;
-  padding-right: 0;
-}
-
-/*-------------------
-        Size
---------------------*/
-
-.ui.segments .segment,
-.ui.segment {
-  font-size: 1rem;
-}
-
-.ui.mini.segments .segment,
-.ui.mini.segment {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.segments .segment,
-.ui.tiny.segment {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.segments .segment,
-.ui.small.segment {
-  font-size: 0.92857143rem;
-}
-
-.ui.large.segments .segment,
-.ui.large.segment {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.segments .segment,
-.ui.big.segment {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.segments .segment,
-.ui.huge.segment {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.segments .segment,
-.ui.massive.segment {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 9c7cb54cb7..3c9a87c9d7 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -33,7 +33,6 @@
     "menu",
     "modal",
     "search",
-    "segment",
     "tab",
     "table"
   ]

From 0a2f973de9b681a472c96bdfcd945978e88458d8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 18:56:02 +0100
Subject: [PATCH 506/679] Forbid jQuery `is` and fix issues (#30016)

Tested all functionality.

---------

Co-authored-by: Yarden Shoham <git@yardenshoham.com>
---
 .eslintrc.yaml                          | 4 ++--
 web_src/js/features/admin/common.js     | 4 ++--
 web_src/js/features/common-global.js    | 2 +-
 web_src/js/features/repo-legacy.js      | 6 +++---
 web_src/js/modules/fomantic/dropdown.js | 2 +-
 5 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index eeb3e20cb8..ea14d27d4c 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -303,7 +303,7 @@ rules:
   jquery/no-in-array: [2]
   jquery/no-is-array: [2]
   jquery/no-is-function: [2]
-  jquery/no-is: [0]
+  jquery/no-is: [2]
   jquery/no-load: [2]
   jquery/no-map: [2]
   jquery/no-merge: [2]
@@ -440,7 +440,7 @@ rules:
   no-jquery/no-is-numeric: [2]
   no-jquery/no-is-plain-object: [2]
   no-jquery/no-is-window: [2]
-  no-jquery/no-is: [0]
+  no-jquery/no-is: [2]
   no-jquery/no-jquery-constructor: [0]
   no-jquery/no-live: [2]
   no-jquery/no-load-shorthand: [2]
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 0c65f04ab8..3c485d67a6 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -84,7 +84,7 @@ export function initAdminCommon() {
     hideElem($('.oauth2_use_custom_url_field'));
     $('.oauth2_use_custom_url_field input[required]').removeAttr('required');
 
-    if ($('#oauth2_use_custom_url').is(':checked')) {
+    if (document.getElementById('oauth2_use_custom_url')?.checked) {
       for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
         if (applyDefaultValues) {
           $(`#oauth2_${custom}`).val($(`#${provider}_${custom}`).val());
@@ -98,7 +98,7 @@ export function initAdminCommon() {
   }
 
   function onEnableLdapGroupsChange() {
-    toggleElem($('#ldap-group-options'), $('.js-ldap-group-toggle').is(':checked'));
+    toggleElem($('#ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
   }
 
   // New authentication
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index e27935a86e..e2ce01eb49 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -373,7 +373,7 @@ function initGlobalShowModal() {
 
       if (attrTargetAttr) {
         $attrTarget[0][attrTargetAttr] = attrib.value;
-      } else if ($attrTarget.is('input') || $attrTarget.is('textarea')) {
+      } else if ($attrTarget[0].matches('input, textarea')) {
         $attrTarget.val(attrib.value); // FIXME: add more supports like checkbox
       } else {
         $attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 43567f4393..838540fcc0 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -139,7 +139,7 @@ export function initRepoCommentForm() {
 
       hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
 
-      const $clickedItem = $(this);
+      const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment
       const scope = $(this).attr('data-scope');
 
       $(this).parent().find('.item').each(function () {
@@ -148,10 +148,10 @@ export function initRepoCommentForm() {
           if ($(this).attr('data-scope') !== scope) {
             return true;
           }
-          if (!$(this).is($clickedItem) && !$(this).hasClass('checked')) {
+          if (this !== clickedItem && !$(this).hasClass('checked')) {
             return true;
           }
-        } else if (!$(this).is($clickedItem)) {
+        } else if (this !== clickedItem) {
           // Toggle for other labels
           return true;
         }
diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
index 7302078dbd..97aabb44b6 100644
--- a/web_src/js/modules/fomantic/dropdown.js
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -199,7 +199,7 @@ function attachDomEvents($dropdown, $focusable, $menu) {
       if (!$item) $item = $menu.find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
       // if the selected item is clickable, then trigger the click event.
       // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
-      if ($item && ($item.is('a') || $item.hasClass('js-aria-clickable'))) $item[0].click();
+      if ($item && ($item[0].matches('a') || $item.hasClass('js-aria-clickable'))) $item[0].click();
     }
   });
 

From ec3d467f15a683b305ac165c3eba6683628dcb25 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 24 Mar 2024 19:23:38 +0100
Subject: [PATCH 507/679] Migrate `gt-hidden` to `tw-hidden` (#30046)

We have to define this one in helpers.css because tailwind only
generates a single class but certain things rely on this being
double-class. Command ran:

```sh
perl -p -i -e 's#gt-hidden#tw-hidden#g' web_src/js/**/* templates/**/* models/**/* web_src/css/**/*

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 .../contributing/guidelines-frontend.en-us.md |  2 +-
 .../contributing/guidelines-frontend.zh-cn.md |  2 +-
 tailwind.config.js                            |  2 ++
 templates/admin/auth/edit.tmpl                |  8 +++---
 templates/admin/auth/new.tmpl                 |  6 ++--
 templates/admin/auth/source/ldap.tmpl         | 14 +++++-----
 templates/admin/auth/source/oauth.tmpl        |  2 +-
 templates/admin/auth/source/smtp.tmpl         |  2 +-
 templates/admin/auth/source/sspi.tmpl         |  2 +-
 templates/admin/user/edit.tmpl                |  8 +++---
 templates/admin/user/new.tmpl                 |  6 ++--
 templates/base/head_navbar.tmpl               |  6 ++--
 templates/install.tmpl                        |  8 +++---
 templates/org/settings/options.tmpl           |  2 +-
 templates/org/team/new.tmpl                   |  2 +-
 templates/repo/blame.tmpl                     |  2 +-
 .../repo/commit_load_branches_and_tags.tmpl   |  2 +-
 templates/repo/commits_list.tmpl              |  2 +-
 templates/repo/commits_list_small.tmpl        |  2 +-
 templates/repo/create.tmpl                    |  2 +-
 templates/repo/diff/box.tmpl                  | 14 +++++-----
 templates/repo/diff/comment_form.tmpl         |  2 +-
 templates/repo/diff/comments.tmpl             |  4 +--
 templates/repo/diff/compare.tmpl              | 12 ++++----
 templates/repo/diff/conversation.tmpl         |  4 +--
 templates/repo/editor/commit_form.tmpl        |  2 +-
 templates/repo/editor/edit.tmpl               |  2 +-
 templates/repo/editor/patch.tmpl              |  2 +-
 templates/repo/find/files.tmpl                |  2 +-
 templates/repo/graph.tmpl                     |  2 +-
 templates/repo/home.tmpl                      |  2 +-
 .../repo/issue/branch_selector_field.tmpl     |  2 +-
 templates/repo/issue/fields/checkboxes.tmpl   |  4 +--
 templates/repo/issue/fields/dropdown.tmpl     |  2 +-
 templates/repo/issue/fields/input.tmpl        |  2 +-
 templates/repo/issue/fields/markdown.tmpl     |  2 +-
 templates/repo/issue/fields/textarea.tmpl     |  6 ++--
 .../repo/issue/labels/edit_delete_label.tmpl  |  2 +-
 templates/repo/issue/labels/label.tmpl        |  2 +-
 .../repo/issue/labels/labels_sidebar.tmpl     |  2 +-
 templates/repo/issue/list.tmpl                |  2 +-
 templates/repo/issue/new_form.tmpl            |  8 +++---
 templates/repo/issue/search.tmpl              |  2 +-
 templates/repo/issue/view_content.tmpl        |  6 ++--
 .../repo/issue/view_content/comments.tmpl     |  8 +++---
 .../repo/issue/view_content/conversation.tmpl | 12 ++++----
 .../view_content/pull_merge_instruction.tmpl  | 12 ++++----
 .../repo/issue/view_content/sidebar.tmpl      | 12 ++++----
 templates/repo/issue/view_title.tmpl          |  8 +++---
 templates/repo/latest_commit.tmpl             |  2 +-
 templates/repo/migrate/migrating.tmpl         |  6 ++--
 templates/repo/migrate/options.tmpl           |  4 +--
 templates/repo/settings/deploy_keys.tmpl      |  2 +-
 templates/repo/settings/githook_edit.tmpl     |  2 +-
 templates/repo/settings/lfs_file.tmpl         |  2 +-
 templates/repo/settings/protected_branch.tmpl |  2 +-
 templates/repo/settings/webhook/history.tmpl  |  2 +-
 templates/repo/settings/webhook/settings.tmpl |  2 +-
 templates/repo/sub_menu.tmpl                  |  2 +-
 templates/repo/view_file.tmpl                 |  4 +--
 templates/repo/wiki/view.tmpl                 |  2 +-
 templates/user/auth/webauthn_error.tmpl       |  4 +--
 templates/user/dashboard/issues.tmpl          |  2 +-
 .../user/notification/notification_div.tmpl   |  4 +--
 templates/user/settings/applications.tmpl     |  2 +-
 templates/user/settings/keys_gpg.tmpl         |  2 +-
 templates/user/settings/keys_principal.tmpl   |  2 +-
 templates/user/settings/keys_ssh.tmpl         |  2 +-
 templates/user/settings/profile.tmpl          |  4 +--
 web_src/css/helpers.css                       |  6 ++--
 web_src/css/modules/button.css                |  5 ++--
 web_src/css/shared/flex-list.css              |  2 +-
 web_src/js/features/comp/LabelEdit.js         |  6 ++--
 web_src/js/features/notification.js           |  6 ++--
 web_src/js/features/repo-diff.js              |  8 +++---
 web_src/js/features/repo-graph.js             | 12 ++++----
 web_src/js/features/repo-issue-content.js     |  4 +--
 web_src/js/features/repo-issue.js             | 28 +++++++++----------
 web_src/js/features/repo-legacy.js            | 18 ++++++------
 web_src/js/markup/mermaid.js                  |  4 +--
 web_src/js/svg.js                             |  2 +-
 web_src/js/utils/dom.js                       |  8 +++---
 82 files changed, 195 insertions(+), 192 deletions(-)

diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index eec4a88fd0..3535e97903 100644
--- a/docs/content/contributing/guidelines-frontend.en-us.md
+++ b/docs/content/contributing/guidelines-frontend.en-us.md
@@ -118,7 +118,7 @@ However, there are still some special cases, so the current guideline is:
 ### Show/Hide Elements
 
 * Vue components are recommended to use `v-if` and `v-show` to show/hide elements.
-* Go template code should use Gitea's `.gt-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.gt-hidden`'s comment.
+* Go template code should use `.tw-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.tw-hidden`'s comment.
 
 ### Styles and Attributes in Go HTML Template
 
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index 040dba3d76..c7998c6dc5 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -117,7 +117,7 @@ Gitea 使用一些补丁使 Fomantic UI 更具可访问性(参见 `aria.md`)
 ### 显示/隐藏元素
 
 * 推荐在Vue组件中使用`v-if`和`v-show`来显示/隐藏元素。
-* Go 模板代码应使用 Gitea 的 `.gt-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.gt-hidden`的注释以获取更多详细信息。
+* Go 模板代码应使用 `.tw-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.tw-hidden`的注释以获取更多详细信息。
 
 ### Go HTML 模板中的样式和属性
 
diff --git a/tailwind.config.js b/tailwind.config.js
index 0754ab3631..5bce37e023 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -41,6 +41,8 @@ export default {
     // classes that don't work without CSS variables from "@tailwind base" which we don't use
     'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter',
     'backdrop-filter',
+    // we use double-class tw-hidden defined in web_src/css/helpers.css for increased specificity
+    'hidden',
     // unneeded classes
     '[-a-zA-Z:0-9_.]',
   ],
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 25abefae00..e140d6b5eb 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -42,7 +42,7 @@
 						<label for="port">{{ctx.Locale.Tr "admin.auths.port"}}</label>
 						<input id="port" name="port" value="{{$cfg.Port}}"  placeholder="636" required>
 					</div>
-					<div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}">
+					<div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}">
 						<div class="ui checkbox">
 							<label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label>
 							<input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}>
@@ -113,7 +113,7 @@
 							<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if $cfg.GroupsEnabled}}checked{{end}}>
 						</div>
 					</div>
-					<div id="ldap-group-options" class="ui segment secondary {{if not $cfg.GroupsEnabled}}gt-hidden{{end}}">
+					<div id="ldap-group-options" class="ui segment secondary {{if not $cfg.GroupsEnabled}}tw-hidden{{end}}">
 						<div class="field">
 							<label>{{ctx.Locale.Tr "admin.auths.group_search_base"}}</label>
 							<input name="group_dn" value="{{$cfg.GroupDN}}" placeholder="ou=group,dc=mydomain,dc=com">
@@ -148,7 +148,7 @@
 								<input id="use_paged_search" name="use_paged_search" type="checkbox" {{if $cfg.UsePagedSearch}}checked{{end}}>
 							</div>
 						</div>
-						<div class="field required search-page-size{{if not $cfg.UsePagedSearch}} gt-hidden{{end}}">
+						<div class="field required search-page-size{{if not $cfg.UsePagedSearch}} tw-hidden{{end}}">
 							<label for="search_page_size">{{ctx.Locale.Tr "admin.auths.search_page_size"}}</label>
 							<input id="search_page_size" name="search_page_size" value="{{if $cfg.UsePagedSearch}}{{$cfg.SearchPageSize}}{{end}}">
 						</div>
@@ -205,7 +205,7 @@
 						</div>
 						<p class="help">{{ctx.Locale.Tr "admin.auths.force_smtps_helper"}}</p>
 					</div>
-					<div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}">
+					<div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}">
 						<div class="ui checkbox">
 							<label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label>
 							<input name="skip_verify" type="checkbox" {{if $cfg.SkipVerify}}checked{{end}}>
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index 1e7e0d9b35..f130e18f65 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -33,13 +33,13 @@
 				{{template "admin/auth/source/smtp" .}}
 
 				<!-- PAM -->
-				<div class="pam required field {{if not (eq .type 4)}}gt-hidden{{end}}">
+				<div class="pam required field {{if not (eq .type 4)}}tw-hidden{{end}}">
 					<label for="pam_service_name">{{ctx.Locale.Tr "admin.auths.pam_service_name"}}</label>
 					<input id="pam_service_name" name="pam_service_name" value="{{.pam_service_name}}">
 					<label for="pam_email_domain">{{ctx.Locale.Tr "admin.auths.pam_email_domain"}}</label>
 					<input id="pam_email_domain" name="pam_email_domain" value="{{.pam_email_domain}}">
 				</div>
-				<div class="pam optional field {{if not (eq .type 4)}}gt-hidden{{end}}">
+				<div class="pam optional field {{if not (eq .type 4)}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
 						<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if .skip_local_two_fa}}checked{{end}}>
@@ -59,7 +59,7 @@
 						<input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}>
 					</div>
 				</div>
-				<div class="ldap inline field {{if not (eq .type 2)}}gt-hidden{{end}}">
+				<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
 						<input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}>
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index a584ac7628..9754aed55a 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -1,4 +1,4 @@
-<div class="ldap dldap field {{if not (or (eq .type 2) (eq .type 5))}}gt-hidden{{end}}">
+<div class="ldap dldap field {{if not (or (eq .type 2) (eq .type 5))}}tw-hidden{{end}}">
 	<div class="inline required field {{if .Err_SecurityProtocol}}error{{end}}">
 		<label>{{ctx.Locale.Tr "admin.auths.security_protocol"}}</label>
 		<div class="ui selection security-protocol dropdown">
@@ -20,17 +20,17 @@
 		<label for="port">{{ctx.Locale.Tr "admin.auths.port"}}</label>
 		<input id="port" name="port" value="{{.port}}"  placeholder="636">
 	</div>
-	<div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}">
+	<div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}">
 		<div class="ui checkbox">
 			<label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label>
 			<input name="skip_verify" type="checkbox" {{if .skip_verify}}checked{{end}}>
 		</div>
 	</div>
-	<div class="ldap field {{if not (eq .type 2)}}gt-hidden{{end}}">
+	<div class="ldap field {{if not (eq .type 2)}}tw-hidden{{end}}">
 		<label for="bind_dn">{{ctx.Locale.Tr "admin.auths.bind_dn"}}</label>
 		<input id="bind_dn" name="bind_dn" value="{{.bind_dn}}" placeholder="cn=Search,dc=mydomain,dc=com">
 	</div>
-	<div class="ldap field {{if not (eq .type 2)}}gt-hidden{{end}}">
+	<div class="ldap field {{if not (eq .type 2)}}tw-hidden{{end}}">
 		<label for="bind_password">{{ctx.Locale.Tr "admin.auths.bind_password"}}</label>
 		<input id="bind_password" name="bind_password" type="password" autocomplete="off" value="{{.bind_password}}">
 	</div>
@@ -38,7 +38,7 @@
 		<label for="user_base">{{ctx.Locale.Tr "admin.auths.user_base"}}</label>
 		<input id="user_base" name="user_base" value="{{.user_base}}" placeholder="ou=Users,dc=mydomain,dc=com">
 	</div>
-	<div class="dldap required field {{if not (eq .type 5)}}gt-hidden{{end}}">
+	<div class="dldap required field {{if not (eq .type 5)}}tw-hidden{{end}}">
 		<label for="user_dn">{{ctx.Locale.Tr "admin.auths.user_dn"}}</label>
 		<input id="user_dn" name="user_dn" value="{{.user_dn}}" placeholder="uid=%s,ou=Users,dc=mydomain,dc=com">
 	</div>
@@ -115,13 +115,13 @@
 	</div>
 	<!-- ldap group end -->
 
-	<div class="ldap inline field {{if not (eq .type 2)}}gt-hidden{{end}}">
+	<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}">
 		<div class="ui checkbox">
 			<label for="use_paged_search"><strong>{{ctx.Locale.Tr "admin.auths.use_paged_search"}}</strong></label>
 			<input id="use_paged_search" name="use_paged_search" class="use-paged-search" type="checkbox" {{if .use_paged_search}}checked{{end}}>
 		</div>
 	</div>
-	<div class="ldap field search-page-size required {{if or (not (eq .type 2)) (not .use_paged_search)}}gt-hidden{{end}}">
+	<div class="ldap field search-page-size required {{if or (not (eq .type 2)) (not .use_paged_search)}}tw-hidden{{end}}">
 		<label for="search_page_size">{{ctx.Locale.Tr "admin.auths.search_page_size"}}</label>
 		<input id="search_page_size" name="search_page_size" value="{{.search_page_size}}">
 	</div>
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl
index 63ad77e67b..f02c5bdf30 100644
--- a/templates/admin/auth/source/oauth.tmpl
+++ b/templates/admin/auth/source/oauth.tmpl
@@ -1,4 +1,4 @@
-<div class="oauth2 field {{if not (eq .type 6)}}gt-hidden{{end}}">
+<div class="oauth2 field {{if not (eq .type 6)}}tw-hidden{{end}}">
 	<div class="inline required field">
 		<label>{{ctx.Locale.Tr "admin.auths.oauth2_provider"}}</label>
 		<div class="ui selection type dropdown">
diff --git a/templates/admin/auth/source/smtp.tmpl b/templates/admin/auth/source/smtp.tmpl
index c4b0b0e7e4..31195acf65 100644
--- a/templates/admin/auth/source/smtp.tmpl
+++ b/templates/admin/auth/source/smtp.tmpl
@@ -1,4 +1,4 @@
-<div class="smtp field {{if not (eq .type 3)}}gt-hidden{{end}}">
+<div class="smtp field {{if not (eq .type 3)}}tw-hidden{{end}}">
 	<div class="inline required field">
 		<label>{{ctx.Locale.Tr "admin.auths.smtp_auth"}}</label>
 		<div class="ui selection type dropdown">
diff --git a/templates/admin/auth/source/sspi.tmpl b/templates/admin/auth/source/sspi.tmpl
index f835e89bdf..6a3f00f9a8 100644
--- a/templates/admin/auth/source/sspi.tmpl
+++ b/templates/admin/auth/source/sspi.tmpl
@@ -1,4 +1,4 @@
-<div class="sspi field {{if not (eq .type 7)}}gt-hidden{{end}}">
+<div class="sspi field {{if not (eq .type 7)}}tw-hidden{{end}}">
 	<div class="field">
 		<div class="ui checkbox">
 			<label for="sspi_auto_create_users"><strong>{{ctx.Locale.Tr "admin.auths.sspi_auto_create_users"}}</strong></label>
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index 751eb6d83f..41b00defb4 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -53,7 +53,7 @@
 					</div>
 				</div>
 
-				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}gt-hidden{{end}}">
+				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}tw-hidden{{end}}">
 					<label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label>
 					<input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus>
 				</div>
@@ -65,7 +65,7 @@
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
 					<input id="email" name="email" type="email" value="{{.User.Email}}" autofocus required>
 				</div>
-				<div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}gt-hidden{{end}}">
+				<div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}tw-hidden{{end}}">
 					<label for="password">{{ctx.Locale.Tr "password"}}</label>
 					<input id="password" name="password" type="password" autocomplete="new-password">
 					<p class="help">{{ctx.Locale.Tr "admin.users.password_helper"}}</p>
@@ -128,13 +128,13 @@
 						<input name="restricted" type="checkbox" {{if .User.IsRestricted}}checked{{end}}>
 					</div>
 				</div>
-				<div class="inline field {{if DisableGitHooks}}gt-hidden{{end}}">
+				<div class="inline field {{if DisableGitHooks}}tw-hidden{{end}}">
 					<div class="ui checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.users.allow_git_hook_tooltip"}}">
 						<label><strong>{{ctx.Locale.Tr "admin.users.allow_git_hook"}}</strong></label>
 						<input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}} {{if DisableGitHooks}}disabled{{end}}>
 					</div>
 				</div>
-				<div class="inline field {{if or (DisableImportLocal) (.DisableMigrations)}}gt-hidden{{end}}">
+				<div class="inline field {{if or (DisableImportLocal) (.DisableMigrations)}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "admin.users.allow_import_local"}}</strong></label>
 						<input name="allow_import_local" type="checkbox" {{if .User.CanImportLocal}}checked{{end}} {{if DisableImportLocal}}disabled{{end}}>
diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl
index bcb53d8131..b04ebc4b60 100644
--- a/templates/admin/user/new.tmpl
+++ b/templates/admin/user/new.tmpl
@@ -47,7 +47,7 @@
 					</div>
 				</div>
 
-				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}gt-hidden{{end}}">
+				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}tw-hidden{{end}}">
 					<label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label>
 					<input id="login_name" name="login_name" value="{{.login_name}}">
 				</div>
@@ -59,12 +59,12 @@
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
 					<input id="email" name="email" type="email" value="{{.email}}" required>
 				</div>
-				<div class="required local field {{if .Err_Password}}error{{end}} {{if not (eq .login_type "0-0")}}gt-hidden{{end}}">
+				<div class="required local field {{if .Err_Password}}error{{end}} {{if not (eq .login_type "0-0")}}tw-hidden{{end}}">
 					<label for="password">{{ctx.Locale.Tr "password"}}</label>
 					<input id="password" name="password" type="password" autocomplete="new-password" value="{{.password}}" {{if eq .login_type "0-0"}}required{{end}}>
 				</div>
 
-				<div class="inline field local {{if ne .login_type "0-0"}}gt-hidden{{end}}">
+				<div class="inline field local {{if ne .login_type "0-0"}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "auth.allow_password_change"}}</strong></label>
 						<input name="must_change_password" type="checkbox" checked>
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 490fddcf05..addff22c49 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -16,7 +16,7 @@
 			<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 				<div class="tw-relative">
 					{{svg "octicon-bell"}}
-					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
+					<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
 			</a>
 			{{end}}
@@ -75,7 +75,7 @@
 			</div><!-- end dropdown avatar menu -->
 		{{else if .IsSigned}}
 			{{if EnableTimetracking}}
-			<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} gt-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
+			<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
 				<div class="tw-relative">
 					{{svg "octicon-stopwatch"}}
 					<span class="header-stopwatch-dot"></span>
@@ -114,7 +114,7 @@
 			<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 				<div class="tw-relative">
 					{{svg "octicon-bell"}}
-					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
+					<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
 			</a>
 
diff --git a/templates/install.tmpl b/templates/install.tmpl
index b2f449618c..8a6956b546 100644
--- a/templates/install.tmpl
+++ b/templates/install.tmpl
@@ -28,7 +28,7 @@
 						</div>
 					</div>
 
-					<div class="tw-mt-4 gt-hidden" data-db-setting-for="common-host">
+					<div class="tw-mt-4 tw-hidden" data-db-setting-for="common-host">
 						<div class="inline required field {{if .Err_DbSetting}}error{{end}}">
 							<label for="db_host">{{ctx.Locale.Tr "install.host"}}</label>
 							<input id="db_host" name="db_host" value="{{.db_host}}">
@@ -47,7 +47,7 @@
 						</div>
 					</div>
 
-					<div class="tw-mt-4 gt-hidden" data-db-setting-for="postgres">
+					<div class="tw-mt-4 tw-hidden" data-db-setting-for="postgres">
 						<div class="inline required field">
 							<label>{{ctx.Locale.Tr "install.ssl_mode"}}</label>
 							<div class="ui selection database type dropdown">
@@ -68,7 +68,7 @@
 						</div>
 					</div>
 
-					<div class="tw-mt-4 gt-hidden" data-db-setting-for="sqlite3">
+					<div class="tw-mt-4 tw-hidden" data-db-setting-for="sqlite3">
 						<div class="inline required field {{if or .Err_DbPath .Err_DbSetting}}error{{end}}">
 							<label for="db_path">{{ctx.Locale.Tr "install.path"}}</label>
 							<input id="db_path" name="db_path" value="{{.db_path}}">
@@ -347,5 +347,5 @@
 		</div>
 	</div>
 </div>
-<img class="gt-hidden" src="{{AssetUrlPrefix}}/img/loading.png">
+<img class="tw-hidden" src="{{AssetUrlPrefix}}/img/loading.png">
 {{template "base/footer" .}}
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl
index 31c0d85d89..62debfc0ae 100644
--- a/templates/org/settings/options.tmpl
+++ b/templates/org/settings/options.tmpl
@@ -8,7 +8,7 @@
 						{{.CsrfTokenHtml}}
 						<div class="required field {{if .Err_Name}}error{{end}}">
 							<label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}
-								<span class="text red gt-hidden" id="org-name-change-prompt">
+								<span class="text red tw-hidden" id="org-name-change-prompt">
 									<br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}}
 								</span>
 							</label>
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl
index 0de70296fd..9608eac154 100644
--- a/templates/org/team/new.tmpl
+++ b/templates/org/team/new.tmpl
@@ -71,7 +71,7 @@
 							</div>
 							<div class="divider"></div>
 
-							<div class="team-units required grouped field {{if eq .Team.AccessMode 3}}gt-hidden{{end}}">
+							<div class="team-units required grouped field {{if eq .Team.AccessMode 3}}tw-hidden{{end}}">
 								<label>{{ctx.Locale.Tr "org.team_unit_desc"}}</label>
 								<table class="ui celled table">
 									<thead>
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 31b5b99829..1a148a2d1c 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -24,7 +24,7 @@
 				<a class="ui tiny button" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.normal_view"}}</a>
 				<a class="ui tiny button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_history"}}</a>
 				<button class="ui tiny button unescape-button">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
-				<button class="ui tiny button escape-button gt-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
+				<button class="ui tiny button escape-button tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 			</div>
 		</div>
 	</h4>
diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl
index 9ab1e2fe05..ffa0e530e8 100644
--- a/templates/repo/commit_load_branches_and_tags.tmpl
+++ b/templates/repo/commit_load_branches_and_tags.tmpl
@@ -4,7 +4,7 @@
 		data-fetch-url="{{.RepoLink}}/commit/{{.CommitID}}/load-branches-and-tags"
 		data-tooltip-content="{{ctx.Locale.Tr "repo.commit.load_referencing_branches_and_tags"}}"
 	>...</button>
-	<div class="branch-and-tag-detail gt-hidden">
+	<div class="branch-and-tag-detail tw-hidden">
 		<div class="divider"></div>
 		<div>{{ctx.Locale.Tr "repo.commit.contained_in"}}</div>
 		<div class="tw-flex tw-mt-2">
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index 99787f715f..aa7ca88931 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -68,7 +68,7 @@
 							{{end}}
 							{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
 							{{if IsMultilineCommitMessage .Message}}
-							<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
+							<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
 							{{end}}
 						</td>
 						{{if .Committer}}
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index cb867df65a..0af29291d8 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -43,7 +43,7 @@
 			<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
 		{{end}}
 		{{if IsMultilineCommitMessage .Message}}
-			<pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
+			<pre class="commit-body tw-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
 		{{end}}
 	</div>
 {{end}}
diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl
index 73c9ca6a1f..bcd3c16b6a 100644
--- a/templates/repo/create.tmpl
+++ b/templates/repo/create.tmpl
@@ -73,7 +73,7 @@
 						</div>
 					</div>
 
-					<div id="template_units" class="gt-hidden">
+					<div id="template_units" class="tw-hidden">
 						<div class="inline field">
 							<label>{{ctx.Locale.Tr "repo.template.items"}}</label>
 							<div class="ui checkbox">
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index a6ca314b3a..37a4e1e323 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -5,15 +5,15 @@
 			{{if $showFileTree}}
 				<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
 					{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
-					{{svg "octicon-sidebar-collapse" 20 "icon gt-hidden"}}
-					{{svg "octicon-sidebar-expand" 20 "icon gt-hidden"}}
+					{{svg "octicon-sidebar-collapse" 20 "icon tw-hidden"}}
+					{{svg "octicon-sidebar-expand" 20 "icon tw-hidden"}}
 				</button>
 				<script>
 					// Default to true if unset
 					const diffTreeVisible = localStorage?.getItem('diff_file_tree_visible') !== 'false';
 					const diffTreeBtn = document.querySelector('.diff-toggle-file-tree-button');
 					const diffTreeIcon = `.octicon-sidebar-${diffTreeVisible ? 'expand' : 'collapse'}`;
-					diffTreeBtn.querySelector(diffTreeIcon).classList.remove('gt-hidden');
+					diffTreeBtn.querySelector(diffTreeIcon).classList.remove('tw-hidden');
 					diffTreeBtn.setAttribute('data-tooltip-content', diffTreeBtn.getAttribute(diffTreeVisible ? 'data-hide-text' : 'data-show-text'));
 				</script>
 			{{end}}
@@ -89,9 +89,9 @@
 	{{end}}
 	<div id="diff-container">
 		{{if $showFileTree}}
-			<div id="diff-file-tree" class="gt-hidden not-mobile"></div>
+			<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
 			<script>
-				if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('gt-hidden');
+				if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
 			</script>
 		{{end}}
 		{{if .DiffNotAvailable}}
@@ -159,7 +159,7 @@
 								{{end}}
 								{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
 									<button class="ui basic tiny button unescape-button not-mobile">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
-									<button class="ui basic tiny button escape-button gt-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
+									<button class="ui basic tiny button escape-button tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 								{{end}}
 								{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
 									{{if $file.IsDeleted}}
@@ -176,7 +176,7 @@
 							</div>
 						</h4>
 						<div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}>
-							<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} gt-hidden{{end}}">
+							<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} tw-hidden{{end}}">
 								{{if or $file.IsIncomplete $file.IsBin}}
 									<div class="diff-file-body binary">
 										{{if $file.IsIncomplete}}
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl
index d797e89444..6a5dec6c48 100644
--- a/templates/repo/diff/comment_form.tmpl
+++ b/templates/repo/diff/comment_form.tmpl
@@ -1,5 +1,5 @@
 {{if and $.root.SignedUserID (not $.Repository.IsArchived)}}
-	<form class="ui form {{if $.hidden}}gt-hidden comment-form{{end}}" action="{{$.root.Issue.Link}}/files/reviews/comments" method="post">
+	<form class="ui form {{if $.hidden}}tw-hidden comment-form{{end}}" action="{{$.root.Issue.Link}}/files/reviews/comments" method="post">
 	{{$.root.CsrfTokenHtml}}
 		<input type="hidden" name="origin" value="{{if $.root.PageIsPullFiles}}diff{{else}}timeline{{end}}">
 		<input type="hidden" name="latest_commit_id" value="{{$.root.AfterCommitID}}">
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index b03a9291c5..a9120465bd 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -60,8 +60,8 @@
 				<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 			{{end}}
 			</div>
-			<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-			<div class="edit-content-zone gt-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
+			<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+			<div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
 			{{if .Attachments}}
 				{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 			{{end}}
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index ea9c0d471a..d0472577d0 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -75,7 +75,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="scrolling menu reference-list-menu base-tag-list gt-hidden">
+				<div class="scrolling menu reference-list-menu base-tag-list tw-hidden">
 					{{range .Tags}}
 						<div class="item {{if eq $.BaseBranch .}}selected{{end}}" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}">{{$BaseCompareName}}:{{.}}</div>
 					{{end}}
@@ -144,7 +144,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="scrolling menu reference-list-menu head-tag-list gt-hidden">
+				<div class="scrolling menu reference-list-menu head-tag-list tw-hidden">
 					{{range .HeadTags}}
 						<div class="{{if eq $.HeadBranch .}}selected{{end}} item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments .}}">{{$HeadCompareName}}:{{.}}</div>
 					{{end}}
@@ -171,10 +171,10 @@
 	{{if .IsNothingToCompare}}
 		{{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived) .PageIsComparePull}}
 			<div class="ui segment">{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}}</div>
-			<div class="ui info message show-form-container {{if .Flash}}gt-hidden{{end}}">
+			<div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}">
 				<button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button>
 			</div>
-			<div class="pullrequest-form {{if not .Flash}}gt-hidden{{end}}">
+			<div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}">
 				{{template "repo/issue/new_form" .}}
 			</div>
 		{{else if and .HeadIsBranch .BaseIsBranch}}
@@ -204,7 +204,7 @@
 			</div>
 		{{else}}
 			{{if and $.IsSigned (not .Repository.IsArchived)}}
-				<div class="ui info message show-form-container {{if .Flash}}gt-hidden{{end}}">
+				<div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}">
 					<button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button>
 				</div>
 			{{else if .Repository.IsArchived}}
@@ -217,7 +217,7 @@
 				</div>
 			{{end}}
 			{{if $.IsSigned}}
-				<div class="pullrequest-form {{if not .Flash}}gt-hidden{{end}}">
+				<div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}">
 					{{template "repo/issue/new_form" .}}
 				</div>
 			{{end}}
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index 872cbee78b..c263ddcdd6 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -27,14 +27,14 @@
 						{{svg "octicon-unfold" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
 					</button>
-					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-items-center gt-hidden">
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-items-center tw-hidden">
 						{{svg "octicon-fold" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
 					</button>
 				</div>
 			</div>
 		{{end}}
-		<div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}gt-hidden{{end}}">
+		<div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}tw-hidden{{end}}">
 			<div class="comment-list">
 				<ui class="ui comments">
 					{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index 56d96e0f37..0ddbec0e8e 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -57,7 +57,7 @@
 					</label>
 				</div>
 			</div>
-			<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}gt-hidden{{end}}">
+			<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
 				<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
 					{{svg "octicon-git-branch"}}
 					<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index 05a8d96681..1f5652f6b5 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -34,7 +34,7 @@
 					{{end}}
 				</div>
 				<div class="ui bottom attached active tab segment" data-tab="write">
-					<textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
+					<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
 						data-url="{{.Repository.Link}}/markup"
 						data-context="{{.RepoLink}}"
 						data-previewable-extensions="{{.PreviewableExtensions}}"
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
index 1d919814c9..ff5c09667f 100644
--- a/templates/repo/editor/patch.tmpl
+++ b/templates/repo/editor/patch.tmpl
@@ -23,7 +23,7 @@
 					<a class="active item" data-tab="write">{{svg "octicon-code" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
 				</div>
 				<div class="ui bottom attached active tab segment" data-tab="write">
-					<textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-patch"
+					<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-patch"
 						data-context="{{.RepoLink}}"
 						data-line-wrap-extensions="{{.LineWrapExtensions}}">
 {{.FileContent}}</textarea>
diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl
index 703f2eee2f..548ce2f0e8 100644
--- a/templates/repo/find/files.tmpl
+++ b/templates/repo/find/files.tmpl
@@ -13,7 +13,7 @@
 			<tbody>
 			</tbody>
 		</table>
-		<div id="repo-find-file-no-result" class="ui row center tw-mt-8 gt-hidden">
+		<div id="repo-find-file-no-result" class="ui row center tw-mt-8 tw-hidden">
 			<h3>{{ctx.Locale.Tr "repo.find_file.no_matching"}}</h3>
 		</div>
 	</div>
diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl
index 4b40233ac9..9eb4bd4ecb 100644
--- a/templates/repo/graph.tmpl
+++ b/templates/repo/graph.tmpl
@@ -50,7 +50,7 @@
 				</div>
 			</h2>
 			<div class="ui dividing"></div>
-			<div class="is-loading tw-py-32 gt-hidden" id="loading-indicator"></div>
+			<div class="is-loading tw-py-32 tw-hidden" id="loading-indicator"></div>
 			{{template "repo/graph/svgcontainer" .}}
 			{{template "repo/graph/commits" .}}
 		</div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 2418b21b69..4241f77ead 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -24,7 +24,7 @@
 		</div>
 		{{end}}
 		{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-		<div class="ui form gt-hidden tw-flex tw-flex-col tw-mt-4" id="topic_edit">
+		<div class="ui form tw-hidden tw-flex tw-flex-col tw-mt-4" id="topic_edit">
 			<div class="field tw-flex-1 tw-mb-1">
 				<div class="ui fluid multiple search selection dropdown tw-flex-wrap" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
 					<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl
index f182b909d6..b8ac9a6194 100644
--- a/templates/repo/issue/branch_selector_field.tmpl
+++ b/templates/repo/issue/branch_selector_field.tmpl
@@ -42,7 +42,7 @@
 				<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
 			{{end}}
 		</div>
-		<div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} gt-hidden">
+		<div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} tw-hidden">
 			{{if .Reference}}
 				<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
 			{{end}}
diff --git a/templates/repo/issue/fields/checkboxes.tmpl b/templates/repo/issue/fields/checkboxes.tmpl
index 5d98605983..531f401fb7 100644
--- a/templates/repo/issue/fields/checkboxes.tmpl
+++ b/templates/repo/issue/fields/checkboxes.tmpl
@@ -1,8 +1,8 @@
-<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	{{range $i, $opt := .item.Attributes.options}}
 		<div class="field inline">
-			<div class="ui checkbox tw-mr-0 {{if and ($opt.visible) (not (SliceUtils.Contains $opt.visible "form"))}}gt-hidden{{end}}">
+			<div class="ui checkbox tw-mr-0 {{if and ($opt.visible) (not (SliceUtils.Contains $opt.visible "form"))}}tw-hidden{{end}}">
 				<input type="checkbox" name="form-field-{{$.item.ID}}-{{$i}}" {{if $opt.required}}required{{end}}>
 				<label>{{RenderMarkdownToHtml $.context $opt.label}}</label>
 			</div>
diff --git a/templates/repo/issue/fields/dropdown.tmpl b/templates/repo/issue/fields/dropdown.tmpl
index b8df6908e3..f4fa79738c 100644
--- a/templates/repo/issue/fields/dropdown.tmpl
+++ b/templates/repo/issue/fields/dropdown.tmpl
@@ -1,4 +1,4 @@
-<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	{{/* FIXME: required validation */}}
 	<div class="ui fluid selection dropdown {{if .item.Attributes.multiple}}multiple clearable{{end}}">
diff --git a/templates/repo/issue/fields/input.tmpl b/templates/repo/issue/fields/input.tmpl
index ad0fe3d783..039f9a9f34 100644
--- a/templates/repo/issue/fields/input.tmpl
+++ b/templates/repo/issue/fields/input.tmpl
@@ -1,4 +1,4 @@
-<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	<input type="{{if .item.Validations.is_number}}number{{else}}text{{end}}" name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" value="{{.item.Attributes.value}}" {{if .item.Validations.required}}required{{end}} {{if .item.Validations.regex}}pattern="{{.item.Validations.regex}}" title="{{.item.Validations.regex}}"{{end}}>
 </div>
diff --git a/templates/repo/issue/fields/markdown.tmpl b/templates/repo/issue/fields/markdown.tmpl
index 97813cc1d8..934699ed05 100644
--- a/templates/repo/issue/fields/markdown.tmpl
+++ b/templates/repo/issue/fields/markdown.tmpl
@@ -1,3 +1,3 @@
-<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}}">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	<div>{{RenderMarkdownToHtml .Context .item.Attributes.value}}</div>
 </div>
diff --git a/templates/repo/issue/fields/textarea.tmpl b/templates/repo/issue/fields/textarea.tmpl
index 831cea01d5..3ad69e1220 100644
--- a/templates/repo/issue/fields/textarea.tmpl
+++ b/templates/repo/issue/fields/textarea.tmpl
@@ -1,5 +1,5 @@
 {{$useMarkdownEditor := not .item.Attributes.render}}
-<div class="field {{if not .item.VisibleOnForm}}gt-hidden{{end}} {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}} {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
 	{{template "repo/issue/fields/header" .}}
 
 	{{/* the real form element to provide the value */}}
@@ -7,7 +7,7 @@
 
 	{{if $useMarkdownEditor}}
 		{{template "shared/combomarkdowneditor" (dict
-			"ContainerClasses" "gt-hidden"
+			"ContainerClasses" "tw-hidden"
 			"MarkdownPreviewUrl" (print .root.RepoLink "/markup")
 			"MarkdownPreviewContext" .root.RepoLink
 			"TextareaContent" .item.Attributes.value
@@ -16,7 +16,7 @@
 		)}}
 
 		{{if .root.IsAttachmentEnabled}}
-		<div class="tw-mt-4 form-field-dropzone gt-hidden">
+		<div class="tw-mt-4 form-field-dropzone tw-hidden">
 			{{template "repo/upload" .root}}
 		</div>
 		{{end}}
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index 526bc760a2..98e0f47020 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -30,7 +30,7 @@
 				</div>
 				<br>
 				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
-				<div class="desc tw-ml-1 tw-mt-2 gt-hidden label-exclusive-warning">
+				<div class="desc tw-ml-1 tw-mt-2 tw-hidden label-exclusive-warning">
 					{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}
 				</div>
 				<br>
diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl
index d20d5e63d7..3651ba118f 100644
--- a/templates/repo/issue/labels/label.tmpl
+++ b/templates/repo/issue/labels/label.tmpl
@@ -1,5 +1,5 @@
 <a
-	class="item {{if not .label.IsChecked}}gt-hidden{{end}}"
+	class="item {{if not .label.IsChecked}}tw-hidden{{end}}"
 	id="label_{{.label.ID}}"
 	href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}}
 >
diff --git a/templates/repo/issue/labels/labels_sidebar.tmpl b/templates/repo/issue/labels/labels_sidebar.tmpl
index 4f41054a91..be30baba92 100644
--- a/templates/repo/issue/labels/labels_sidebar.tmpl
+++ b/templates/repo/issue/labels/labels_sidebar.tmpl
@@ -1,5 +1,5 @@
 <div class="ui labels list">
-	<span class="no-select item {{if .root.HasSelectedLabel}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
+	<span class="no-select item {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
 	<span class="labels-list">
 		{{range .root.Labels}}
 			{{template "repo/issue/labels/label" dict "root" $.root "label" .}}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 45bddefa42..30edf825f1 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -32,7 +32,7 @@
 
 		{{template "repo/issue/filters" .}}
 
-		<div id="issue-actions" class="issue-list-toolbar gt-hidden">
+		<div id="issue-actions" class="issue-list-toolbar tw-hidden">
 			<div class="issue-list-toolbar-left">
 				{{template "repo/issue/openclose" .}}
 				<!-- Total Tracked Time -->
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 7c73bd182b..058ea8d73e 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -68,7 +68,7 @@
 			</div>
 		</div>
 		<div class="ui select-milestone list">
-			<span class="no-select item {{if .Milestone}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
+			<span class="no-select item {{if .Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
 			<div class="selected">
 				{{if .Milestone}}
 					<a class="item muted sidebar-item-link" href="{{.RepoLink}}/issues?milestone={{.Milestone.ID}}">
@@ -129,7 +129,7 @@
 			</div>
 		</div>
 		<div class="ui select-project list">
-			<span class="no-select item {{if .Project}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
+			<span class="no-select item {{if .Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
 			<div class="selected">
 				{{if .Project}}
 					<a class="item muted sidebar-item-link" href="{{.Project.Link ctx}}">
@@ -165,12 +165,12 @@
 				</div>
 			</div>
 			<div class="ui assignees list">
-				<span class="no-select item {{if .HasSelectedLabel}}gt-hidden{{end}}">
+				<span class="no-select item {{if .HasSelectedLabel}}tw-hidden{{end}}">
 					{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
 				</span>
 				<div class="selected">
 				{{range .Assignees}}
-					<a class="item tw-p-1 muted gt-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
+					<a class="item tw-p-1 muted tw-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
 						{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}}
 					</a>
 				{{end}}
diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl
index 4727b26154..769387b51c 100644
--- a/templates/repo/issue/search.tmpl
+++ b/templates/repo/issue/search.tmpl
@@ -11,7 +11,7 @@
 		{{end}}
 		{{template "shared/search/input" dict "Value" .Keyword}}
 		{{if .PageIsIssueList}}
-			<button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}" data-repo-link="{{.RepoLink}}">{{svg "octicon-hash"}}</button>
+			<button id="issue-list-quick-goto" class="ui small icon button tw-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}" data-repo-link="{{.RepoLink}}">{{svg "octicon-hash"}}</button>
 		{{end}}
 		{{template "shared/search/button"}}
 	</div>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 759de75662..c65b79dea7 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -59,8 +59,8 @@
 								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 							{{end}}
 						</div>
-						<div id="issue-{{.Issue.ID}}-raw" class="raw-content gt-hidden">{{.Issue.Content}}</div>
-						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
+						<div id="issue-{{.Issue.ID}}-raw" class="raw-content tw-hidden">{{.Issue.Content}}</div>
+						<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
 						{{if .Issue.Attachments}}
 							{{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
 						{{end}}
@@ -172,7 +172,7 @@
 {{template "repo/issue/view_content/reference_issue_dialog" .}}
 {{template "shared/user/block_user_dialog" .}}
 
-<div class="gt-hidden" id="no-content">
+<div class="tw-hidden" id="no-content">
 	<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 </div>
 
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index dbfe016e89..f65dc6ee90 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -66,8 +66,8 @@
 								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 							{{end}}
 						</div>
-						<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+						<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+						<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 						{{if .Attachments}}
 							{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 						{{end}}
@@ -440,8 +440,8 @@
 									<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 								{{end}}
 							</div>
-							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+							<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+							<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 							{{if .Attachments}}
 								{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 							{{end}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 1a282968d7..79e7cb498b 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -17,7 +17,7 @@
 			</div>
 			<div>
 				{{if or $invalid $resolved}}
-					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-items-center">
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}tw-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-items-center">
 						{{svg "octicon-unfold" 16 "tw-mr-2"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
@@ -25,7 +25,7 @@
 							{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
 						{{end}}
 					</button>
-					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-items-center">
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}tw-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-items-center">
 						{{svg "octicon-fold" 16 "tw-mr-2"}}
 						{{if $resolved}}
 							{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
@@ -39,7 +39,7 @@
 		{{$diff := (CommentMustAsDiff ctx $comment)}}
 		{{if $diff}}
 			{{$file := (index $diff.Files 0)}}
-			<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} gt-hidden{{end}}">
+			<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}">
 				<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
 					<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
 						<table>
@@ -51,7 +51,7 @@
 				</div>
 			</div>
 		{{end}}
-		<div id="code-comments-{{$comment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} gt-hidden{{end}}">
+		<div id="code-comments-{{$comment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} tw-hidden{{end}}">
 			<div class="ui comments tw-mb-0">
 				{{range .comments}}
 					{{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}}
@@ -95,8 +95,8 @@
 									<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 								{{end}}
 								</div>
-								<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-								<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+								<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+								<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 								{{if .Attachments}}
 									{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 								{{end}}
diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
index 4ff38950cd..d585d36574 100644
--- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl
+++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
@@ -1,6 +1,6 @@
 <div class="divider"></div>
 <div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}} </div>
-<div class="instruct-content tw-mt-2 gt-hidden">
+<div class="instruct-content tw-mt-2 tw-hidden">
 	<div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}</div>
 	{{$localBranch := .PullRequest.HeadBranch}}
 	{{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}
@@ -21,25 +21,25 @@
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --no-ff {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="rebase">
+		<div class="tw-hidden" data-pull-merge-style="rebase">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --ff-only {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="rebase-merge">
+		<div class="tw-hidden" data-pull-merge-style="rebase-merge">
 			<div>git checkout {{$localBranch}}</div>
 			<div>git rebase {{.PullRequest.BaseBranch}}</div>
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --no-ff {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="squash">
+		<div class="tw-hidden" data-pull-merge-style="squash">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --squash {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="fast-forward-only">
+		<div class="tw-hidden" data-pull-merge-style="fast-forward-only">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --ff-only {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="manually-merged">
+		<div class="tw-hidden" data-pull-merge-style="manually-merged">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge {{$localBranch}}</div>
 		</div>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 324175e6cf..bc2a841708 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -47,7 +47,7 @@
 		</div>
 
 		<div class="ui assignees list">
-			<span class="no-select item {{if or .OriginalReviews .PullReviewers}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
+			<span class="no-select item {{if or .OriginalReviews .PullReviewers}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
 			<div class="selected">
 				{{range .PullReviewers}}
 					<div class="item tw-flex tw-items-center tw-py-2">
@@ -140,7 +140,7 @@
 		</div>
 	</div>
 	<div class="ui select-milestone list">
-		<span class="no-select item {{if .Issue.Milestone}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
+		<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
 		<div class="selected">
 			{{if .Issue.Milestone}}
 				<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
@@ -194,7 +194,7 @@
 			</div>
 		</div>
 		<div class="ui select-project list">
-			<span class="no-select item {{if .Issue.Project}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
+			<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
 			<div class="selected">
 				{{if .Issue.Project}}
 					<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
@@ -240,7 +240,7 @@
 		</div>
 	</div>
 	<div class="ui assignees list">
-		<span class="no-select item {{if .Issue.Assignees}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
+		<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
 		<div class="selected">
 			{{range .Issue.Assignees}}
 				<div class="item">
@@ -355,7 +355,7 @@
 	<div class="divider"></div>
 	<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span>
 	<div class="ui form" id="deadline-loader">
-		<div class="ui negative message gt-hidden" id="deadline-err-invalid-date">
+		<div class="ui negative message tw-hidden" id="deadline-err-invalid-date">
 			{{svg "octicon-x" 16 "close icon"}}
 			{{ctx.Locale.Tr "repo.issues.due_date_invalid"}}
 		</div>
@@ -379,7 +379,7 @@
 		{{end}}
 
 		{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-			<div {{if ne .Issue.DeadlineUnix 0}} class="gt-hidden"{{end}} id="deadlineForm">
+			<div {{if ne .Issue.DeadlineUnix 0}} class="tw-hidden"{{end}} id="deadlineForm">
 				<form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" method="post" id="update-issue-deadline-form">
 					{{$.CsrfTokenHtml}}
 					<input required placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}" {{if gt .Issue.DeadlineUnix 0}}value="{{.Issue.DeadlineUnix.FormatDate}}"{{end}} type="date" name="deadlineDate" id="deadlineDate">
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 5b846f6b21..b78ff55cda 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -8,7 +8,7 @@
 		<h1 class="gt-word-break">
 			<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
 </span>
-			<div id="edit-title-input" class="ui input tw-flex-1 gt-hidden">
+			<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
 				<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
 			</div>
 		</h1>
@@ -22,8 +22,8 @@
 		</div>
 		{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
 			<div class="edit-buttons">
-				<button id="cancel-edit-title" class="ui small basic button in-edit gt-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
-				<button id="save-edit-title" class="ui small primary button in-edit gt-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
+				<button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
+				<button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
 			</div>
 		{{end}}
 	</div>
@@ -70,7 +70,7 @@
 							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
 						</span>
 					{{end}}
-					<span id="pull-desc-edit" class="gt-hidden flex-text-block">
+					<span id="pull-desc-edit" class="tw-hidden flex-text-block">
 						<div class="ui floating filter dropdown">
 							<div class="ui basic small button tw-mr-0">
 								<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>
diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl
index ad31c95ad4..f945e9dfa1 100644
--- a/templates/repo/latest_commit.tmpl
+++ b/templates/repo/latest_commit.tmpl
@@ -25,7 +25,7 @@
 	<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
 		{{if IsMultilineCommitMessage .LatestCommit.Message}}
 			<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
-			<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre>
+			<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre>
 		{{end}}
 	</span>
 {{end}}
diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl
index 1d5a231db8..ed03db2ebd 100644
--- a/templates/repo/migrate/migrating.tmpl
+++ b/templates/repo/migrate/migrating.tmpl
@@ -12,7 +12,7 @@
 								<img src="{{AssetUrlPrefix}}/img/loading.png">
 							</div>
 						</div>
-						<div id="repo_migrating_failed_image" class="sixteen wide center aligned centered column gt-hidden">
+						<div id="repo_migrating_failed_image" class="sixteen wide center aligned centered column tw-hidden">
 							<div>
 								<img src="{{AssetUrlPrefix}}/img/failed.png">
 							</div>
@@ -24,7 +24,7 @@
 								<p>{{ctx.Locale.Tr "repo.migrate.migrating" .CloneAddr}}</p>
 								<p id="repo_migrating_progress_message"></p>
 							</div>
-							<div id="repo_migrating_failed" class="gt-hidden">
+							<div id="repo_migrating_failed" class="tw-hidden">
 								{{if .CloneAddr}}
 									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed" .CloneAddr}}</p>
 								{{else}}
@@ -40,7 +40,7 @@
 									{{else}}
 										<button class="ui basic show-modal button" data-modal="#cancel-repo-modal">{{ctx.Locale.Tr "cancel"}}</button>
 									{{end}}
-									<button id="repo_migrating_retry" data-migrating-task-retry-url="{{.Link}}/settings/migrate/retry" class="ui basic button gt-hidden">{{ctx.Locale.Tr "retry"}}</button>
+									<button id="repo_migrating_retry" data-migrating-task-retry-url="{{.Link}}/settings/migrate/retry" class="ui basic button tw-hidden">{{ctx.Locale.Tr "retry"}}</button>
 								</div>
 							{{end}}
 						</div>
diff --git a/templates/repo/migrate/options.tmpl b/templates/repo/migrate/options.tmpl
index 1cf8600749..8a46e5769b 100644
--- a/templates/repo/migrate/options.tmpl
+++ b/templates/repo/migrate/options.tmpl
@@ -14,9 +14,9 @@
 		<input id="lfs" name="lfs" type="checkbox" {{if .lfs}} checked{{end}}>
 		<label>{{ctx.Locale.Tr "repo.migrate_options_lfs"}}</label>
 	</div>
-	<span id="lfs_settings" class="gt-hidden">(<a id="lfs_settings_show" href="#">{{ctx.Locale.Tr "repo.settings.advanced_settings"}}</a>)</span>
+	<span id="lfs_settings" class="tw-hidden">(<a id="lfs_settings_show" href="#">{{ctx.Locale.Tr "repo.settings.advanced_settings"}}</a>)</span>
 </div>
-<div id="lfs_endpoint" class="gt-hidden">
+<div id="lfs_endpoint" class="tw-hidden">
 	<span class="help">{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}</span>
 	<div class="inline field {{if .Err_LFSEndpoint}}error{{end}}">
 		<label>{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.label"}}</label>
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index cc77e79e8c..da1a321785 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -11,7 +11,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			<div class="{{if not .HasError}}gt-hidden{{end}} tw-mb-4" id="add-deploy-key-panel">
+			<div class="{{if not .HasError}}tw-hidden{{end}} tw-mb-4" id="add-deploy-key-panel">
 				<form class="ui form" action="{{.Link}}" method="post">
 					{{.CsrfTokenHtml}}
 					<div class="field">
diff --git a/templates/repo/settings/githook_edit.tmpl b/templates/repo/settings/githook_edit.tmpl
index db8982a282..e20f51b922 100644
--- a/templates/repo/settings/githook_edit.tmpl
+++ b/templates/repo/settings/githook_edit.tmpl
@@ -14,7 +14,7 @@
 					</div>
 					<div class="field">
 						<label for="content">{{ctx.Locale.Tr "repo.settings.githook_content"}}</label>
-						<textarea id="content" name="content" class="gt-hidden">{{if .IsActive}}{{.Content}}{{else}}{{.Sample}}{{end}}</textarea>
+						<textarea id="content" name="content" class="tw-hidden">{{if .IsActive}}{{.Content}}{{else}}{{.Sample}}{{end}}</textarea>
 						<div class="editor-loading is-loading"></div>
 					</div>
 					<div class="inline field">
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
index 7f1d07e46f..43afba96c3 100644
--- a/templates/repo/settings/lfs_file.tmpl
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -5,7 +5,7 @@
 				<a href="{{.LFSFilesLink}}">{{ctx.Locale.Tr "repo.settings.lfs"}}</a> / <span class="truncate sha">{{.LFSFile.Oid}}</span>
 				<div class="ui right">
 					{{if .EscapeStatus.Escaped}}
-						<a class="ui tiny basic button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
+						<a class="ui tiny basic button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
 						<a class="ui tiny basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
 					{{end}}
 					<a class="ui primary tiny button" href="{{.LFSFilesLink}}/find?oid={{.LFSFile.Oid}}&size={{.LFSFile.Size}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a>
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index 6d8578a2f7..fec4d7c8d4 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -179,7 +179,7 @@
 								<tr>
 									<td>
 										<span>{{.}}</span>
-										<span class="status-check-matched-mark gt-hidden" data-status-check="{{.}}">{{ctx.Locale.Tr "repo.settings.protect_status_check_matched"}}</span>
+										<span class="status-check-matched-mark tw-hidden" data-status-check="{{.}}">{{ctx.Locale.Tr "repo.settings.protect_status_check_matched"}}</span>
 									</td>
 								</tr>
 							{{else}}
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl
index e2aee13941..8ee1446a16 100644
--- a/templates/repo/settings/webhook/history.tmpl
+++ b/templates/repo/settings/webhook/history.tmpl
@@ -32,7 +32,7 @@
 							{{TimeSince .Delivered.AsTime ctx.Locale}}
 						</span>
 					</div>
-					<div class="info gt-hidden" id="info-{{.ID}}">
+					<div class="info tw-hidden" id="info-{{.ID}}">
 						<div class="ui top attached tabular menu">
 							<a class="item active" data-tab="request-{{.ID}}">{{ctx.Locale.Tr "repo.settings.webhook.request"}}</a>
 							<a class="item" data-tab="response-{{.ID}}">
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index 3ef8894444..6862ce5a2c 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -22,7 +22,7 @@
 		</div>
 	</div>
 
-	<div class="events fields ui grid {{if not .Webhook.ChooseEvents}}gt-hidden{{end}}">
+	<div class="events fields ui grid {{if not .Webhook.ChooseEvents}}tw-hidden{{end}}">
 		<!-- Repository Events -->
 		<div class="fourteen wide column">
 			<label>{{ctx.Locale.Tr "repo.settings.event_header_repository"}}</label>
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index a0dbe7e10c..000e0a10c5 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -21,7 +21,7 @@
 		{{end}}
 	</div>
 	{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo) .LanguageStats}}
-	<div class="ui segment sub-menu language-stats-details gt-hidden">
+	<div class="ui segment sub-menu language-stats-details tw-hidden">
 		{{range .LanguageStats}}
 		<div class="item">
 			<i class="color-icon" style="background-color: {{.Color}}"></i>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index a5b6fa7a52..b7c1b9eeae 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -52,7 +52,7 @@
 					{{end}}
 					<a class="ui mini basic button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_history"}}</a>
 					{{if .EscapeStatus.Escaped}}
-						<button class="ui mini basic button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
+						<button class="ui mini basic button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
 						<button class="ui mini basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 					{{end}}
 				</div>
@@ -76,7 +76,7 @@
 					{{end}}
 				{{end}}
 			{{else if .EscapeStatus.Escaped}}
-				<button class="ui mini basic button unescape-button tw-mr-1 gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
+				<button class="ui mini basic button unescape-button tw-mr-1 tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
 				<button class="ui mini basic button escape-button tw-mr-1">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 			{{end}}
 			{{if and .ReadmeInList .CanEditReadmeFile}}
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index b5ae36f4f0..409de3ff08 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -45,7 +45,7 @@
 				</div>
 				<div class="eight wide right aligned column">
 					{{if .EscapeStatus.Escaped}}
-						<a class="ui small button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
+						<a class="ui small button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
 						<a class="ui small button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
 					{{end}}
 					{{if and .CanWriteWiki (not .Repository.IsMirror)}}
diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl
index 13659affd5..511ff7c287 100644
--- a/templates/user/auth/webauthn_error.tmpl
+++ b/templates/user/auth/webauthn_error.tmpl
@@ -1,7 +1,7 @@
-<div id="webauthn-error" class="ui negative message gt-hidden">
+<div id="webauthn-error" class="ui negative message tw-hidden">
 	<div class="header">{{ctx.Locale.Tr "webauthn_error"}}</div>
 	<div id="webauthn-error-msg" class="tw-pt-2"></div>
-	<div class="gt-hidden">
+	<div class="tw-hidden">
 		<div data-webauthn-error-msg="browser">{{ctx.Locale.Tr "webauthn_unsupported_browser"}}</div>
 		<div data-webauthn-error-msg="unknown">{{ctx.Locale.Tr "webauthn_error_unknown"}}</div>
 		<div data-webauthn-error-msg="insecure">{{ctx.Locale.Tr "webauthn_error_insecure"}}</div>
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index ea75267de1..89f23163f7 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -52,7 +52,7 @@
 							<input type="hidden" name="sort" value="{{$.SortType}}">
 							<input type="hidden" name="state" value="{{$.State}}">
 							{{template "shared/search/input" dict "Value" $.Keyword}}
-							<button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}">{{svg "octicon-hash"}}</button>
+							<button id="issue-list-quick-goto" class="ui small icon button tw-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}">{{svg "octicon-hash"}}</button>
 							{{template "shared/search/button"}}
 						</div>
 					</form>
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index 44edd6e107..04e79ba749 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -5,7 +5,7 @@
 			<div class="small-menu-items ui compact tiny menu">
 				<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
 					{{ctx.Locale.Tr "notification.unread"}}
-					<div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}gt-hidden{{end}}">{{$notificationUnreadCount}}</div>
+					<div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}tw-hidden{{end}}">{{$notificationUnreadCount}}</div>
 				</a>
 				<a class="{{if eq .Status 2}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=read">
 					{{ctx.Locale.Tr "notification.read"}}
@@ -14,7 +14,7 @@
 			{{if and (eq .Status 1)}}
 				<form action="{{AppSubUrl}}/notifications/purge" method="post">
 					{{$.CsrfTokenHtml}}
-					<div class="{{if not $notificationUnreadCount}}gt-hidden{{end}}">
+					<div class="{{if not $notificationUnreadCount}}tw-hidden{{end}}">
 						<button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}">
 							{{svg "octicon-checklist"}}
 						</button>
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 57f4c36161..5e2ffc3bb3 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -90,7 +90,7 @@
 					{{ctx.Locale.Tr "settings.generate_token"}}
 				</button>
 			</form>{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}}
-			<div id="scoped-access-warning" class="ui warning message center gt-hidden">
+			<div id="scoped-access-warning" class="ui warning message center tw-hidden">
 				{{ctx.Locale.Tr "settings.at_least_one_permission"}}
 			</div>
 		</div>
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index 8ee8ff0ee0..d86d838f18 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -5,7 +5,7 @@
 	</div>
 </h4>
 <div class="ui attached segment">
-	<div class="{{if not .HasGPGError}}gt-hidden{{end}} tw-mb-4" id="add-gpg-key-panel">
+	<div class="{{if not .HasGPGError}}tw-hidden{{end}} tw-mb-4" id="add-gpg-key-panel">
 		<form class="ui form{{if .HasGPGError}} error{{end}}" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<input type="hidden" name="title" value="none">
diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl
index b6acb63c5e..37d8fb0e95 100644
--- a/templates/user/settings/keys_principal.tmpl
+++ b/templates/user/settings/keys_principal.tmpl
@@ -36,7 +36,7 @@
 	</div>
 	<br>
 
-	<div {{if not .HasPrincipalError}}class="gt-hidden"{{end}} id="add-ssh-principal-panel">
+	<div {{if not .HasPrincipalError}}class="tw-hidden"{{end}} id="add-ssh-principal-panel">
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "settings.add_new_principal"}}
 		</h4>
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index f075a51983..d31cc81b66 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -7,7 +7,7 @@
 	</div>
 </h4>
 <div class="ui attached segment">
-	<div class="{{if not .HasSSHError}}gt-hidden{{end}} tw-mb-4" id="add-ssh-key-panel">
+	<div class="{{if not .HasSSHError}}tw-hidden{{end}} tw-mb-4" id="add-ssh-key-panel">
 		<form class="ui form" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="field {{if .Err_Title}}error{{end}}">
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 1beb8a3dfd..aaaf8f30db 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -9,8 +9,8 @@
 				{{.CsrfTokenHtml}}
 				<div class="required field {{if .Err_Name}}error{{end}}">
 					<label for="username">{{ctx.Locale.Tr "username"}}
-						<span class="text red gt-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
-						<span class="text red gt-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span>
+						<span class="text red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
+						<span class="text red tw-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span>
 					</label>
 					<input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" autofocus required {{if or (not .SignedUser.IsLocal) .IsReverseProxy}}disabled{{end}} maxlength="40">
 					{{if or (not .SignedUser.IsLocal) .IsReverseProxy}}
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index b29e897215..66d0f03257 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -45,17 +45,17 @@ Gitea's private styles use `g-` prefix.
 .interact-bg:active { background: var(--color-active) !important; }
 
 /*
-gt-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element.
+tw-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element.
 do not use:
 * "[hidden]" attribute: it's too weak, can not be applied to an element with "display: flex"
 * ".hidden" class: it has been polluted by Fomantic UI in many cases
 * inline style="display: none": it's difficult to tweak
 * jQuery's show/hide/toggle: it can not show/hide elements with "display: xxx !important"
 only use:
-* this ".gt-hidden" class
+* this ".tw-hidden" class
 * showElem/hideElem/toggleElem functions in "utils/dom.js"
 */
-.gt-hidden.gt-hidden { display: none !important; }
+.tw-hidden.tw-hidden { display: none !important; }
 
 @media (max-width: 767.98px) {
   /* double selector so it wins over .tw-flex (old .gt-df) etc */
diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css
index e72260d99b..849956b72c 100644
--- a/web_src/css/modules/button.css
+++ b/web_src/css/modules/button.css
@@ -66,13 +66,14 @@ It needs some tricks to tweak the left/right borders with active state */
   border-left: 1px solid var(--color-secondary-dark-2);
 }
 
+/* TODO: these "tw-hidden" selectors are only used by "blame.tmpl" buttons: Raw/Normal View/History/Unescape, need to be refactored to a clear solution later */
 .ui.buttons .button:first-child,
-.ui.buttons .button.gt-hidden:first-child + .button {
+.ui.buttons .button.tw-hidden:first-child + .button {
   border-left: 1px solid var(--color-light-border);
 }
 
 .ui.buttons .button:last-child,
-.ui.buttons .button:nth-last-child(2):has(+ .button.gt-hidden) {
+.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) {
   border-right: 1px solid var(--color-light-border);
 }
 
diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css
index e1ad6e7f97..6217b45300 100644
--- a/web_src/css/shared/flex-list.css
+++ b/web_src/css/shared/flex-list.css
@@ -86,7 +86,7 @@
   border-top: 1px solid var(--color-secondary);
 }
 
-/* Fomantic UI segment has default "padding: 1em", so here it removes the padding-top and padding-bottom accordingly (there might also be some `gt-hidden` siblings).
+/* Fomantic UI segment has default "padding: 1em", so here it removes the padding-top and padding-bottom accordingly (there might also be some `tw-hidden` siblings).
 Developers could also use "flex-space-fitted" class to remove the first item's padding-top and the last item's padding-bottom */
 .flex-list.flex-space-fitted > .flex-item:first-child,
 .ui.segment > .flex-list > .flex-item:first-child {
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index 44fc9d9b6b..c5992fa355 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -15,14 +15,14 @@ function updateExclusiveLabelEdit(form) {
     $exclusiveField.removeClass('muted');
     $exclusiveField.removeAttr('aria-disabled');
     if ($exclusiveCheckbox[0].checked && $exclusiveCheckbox.data('exclusive-warn')) {
-      $exclusiveWarning.removeClass('gt-hidden');
+      $exclusiveWarning.removeClass('tw-hidden');
     } else {
-      $exclusiveWarning.addClass('gt-hidden');
+      $exclusiveWarning.addClass('tw-hidden');
     }
   } else {
     $exclusiveField.addClass('muted');
     $exclusiveField.attr('aria-disabled', 'true');
-    $exclusiveWarning.addClass('gt-hidden');
+    $exclusiveWarning.addClass('tw-hidden');
   }
 }
 
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 90e19b683b..d9f6e50202 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -35,7 +35,7 @@ async function receiveUpdateCount(event) {
     const data = JSON.parse(event.data);
 
     for (const count of document.querySelectorAll('.notification_count')) {
-      count.classList.toggle('gt-hidden', data.Count === 0);
+      count.classList.toggle('tw-hidden', data.Count === 0);
       count.textContent = `${data.Count}`;
     }
     await updateNotificationTable();
@@ -179,9 +179,9 @@ async function updateNotificationCount() {
 
     const $notificationCount = $('.notification_count');
     if (data.new === 0) {
-      $notificationCount.addClass('gt-hidden');
+      $notificationCount.addClass('tw-hidden');
     } else {
-      $notificationCount.removeClass('gt-hidden');
+      $notificationCount.removeClass('tw-hidden');
     }
 
     $notificationCount.text(`${data.new}`);
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 262ce2abff..4c8b411c64 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -42,8 +42,8 @@ function initRepoDiffFileViewToggle() {
     $this.addClass('active');
 
     const $target = $($this.data('toggle-selector'));
-    $target.parent().children().addClass('gt-hidden');
-    $target.removeClass('gt-hidden');
+    $target.parent().children().addClass('tw-hidden');
+    $target.removeClass('tw-hidden');
   });
 }
 
@@ -118,7 +118,7 @@ export function initRepoDiffConversationNav() {
   // Previous/Next code review conversation
   $(document).on('click', '.previous-conversation', (e) => {
     const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
-    const $conversations = $('.comment-code-cloud:not(.gt-hidden)');
+    const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
     const index = $conversations.index($conversation);
     const previousIndex = index > 0 ? index - 1 : $conversations.length - 1;
     const $previousConversation = $conversations.eq(previousIndex);
@@ -127,7 +127,7 @@ export function initRepoDiffConversationNav() {
   });
   $(document).on('click', '.next-conversation', (e) => {
     const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
-    const $conversations = $('.comment-code-cloud:not(.gt-hidden)');
+    const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
     const index = $conversations.index($conversation);
     const nextIndex = index < $conversations.length - 1 ? index + 1 : 0;
     const $nextConversation = $conversations.eq(nextIndex);
diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
index f2c0d78f34..a5b61bff54 100644
--- a/web_src/js/features/repo-graph.js
+++ b/web_src/js/features/repo-graph.js
@@ -57,9 +57,9 @@ export function initRepoGraphGit() {
     ajaxUrl.searchParams.set('div-only', 'true');
     window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname);
     $('#pagination').empty();
-    $('#rel-container').addClass('gt-hidden');
-    $('#rev-container').addClass('gt-hidden');
-    $('#loading-indicator').removeClass('gt-hidden');
+    $('#rel-container').addClass('tw-hidden');
+    $('#rev-container').addClass('tw-hidden');
+    $('#loading-indicator').removeClass('tw-hidden');
     (async () => {
       const response = await GET(String(ajaxUrl));
       const html = await response.text();
@@ -67,9 +67,9 @@ export function initRepoGraphGit() {
       $('#pagination').html($div.find('#pagination').html());
       $('#rel-container').html($div.find('#rel-container').html());
       $('#rev-container').html($div.find('#rev-container').html());
-      $('#loading-indicator').addClass('gt-hidden');
-      $('#rel-container').removeClass('gt-hidden');
-      $('#rev-container').removeClass('gt-hidden');
+      $('#loading-indicator').addClass('tw-hidden');
+      $('#rel-container').removeClass('tw-hidden');
+      $('#rev-container').removeClass('tw-hidden');
     })();
   };
   const dropdownSelected = params.getAll('branch');
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index 9d51ab6b8d..3c4efe0447 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -18,7 +18,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
   ${svg('octicon-x', 16, 'close icon inside')}
   <div class="header tw-flex tw-items-center tw-justify-between">
     <div>${itemTitleHtml}</div>
-    <div class="ui dropdown dialog-header-options tw-mr-8 gt-hidden">
+    <div class="ui dropdown dialog-header-options tw-mr-8 tw-hidden">
       ${i18nTextOptions}
       ${svg('octicon-triangle-down', 14, 'dropdown icon')}
       <div class="menu">
@@ -76,7 +76,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
         $dialog.find('.comment-diff-data').removeClass('is-loading').html(resp.diffHtml);
         // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden.
         if (resp.canSoftDelete) {
-          $dialog.find('.dialog-header-options').removeClass('gt-hidden');
+          $dialog.find('.dialog-header-options').removeClass('tw-hidden');
         }
       } catch (error) {
         console.error('Error:', error);
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index bf4ec15372..492428b327 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -222,7 +222,7 @@ export function initRepoIssueCodeCommentCancel() {
   $(document).on('click', '.cancel-code-comment', (e) => {
     const $form = $(e.currentTarget).closest('form');
     if ($form.length > 0 && $form.hasClass('comment-form')) {
-      $form.addClass('gt-hidden');
+      $form.addClass('tw-hidden');
       showElem($form.closest('.comment-code-cloud').find('button.comment-form-reply'));
     } else {
       $form.closest('.comment-code-cloud').remove();
@@ -397,7 +397,7 @@ export function initRepoIssueComments() {
 export async function handleReply($el) {
   hideElem($el);
   const $form = $el.closest('.comment-code-cloud').find('.comment-form');
-  $form.removeClass('gt-hidden');
+  $form.removeClass('tw-hidden');
 
   const $textarea = $form.find('textarea');
   let editor = getComboMarkdownEditor($textarea);
@@ -433,10 +433,10 @@ export function initRepoPullRequestReview() {
         if ($diffHeader[0]) {
           offset += $('.diff-detail-box').outerHeight() + $diffHeader.outerHeight();
         }
-        $(`#show-outdated-${id}`).addClass('gt-hidden');
-        $(`#code-comments-${id}`).removeClass('gt-hidden');
-        $(`#code-preview-${id}`).removeClass('gt-hidden');
-        $(`#hide-outdated-${id}`).removeClass('gt-hidden');
+        $(`#show-outdated-${id}`).addClass('tw-hidden');
+        $(`#code-comments-${id}`).removeClass('tw-hidden');
+        $(`#code-preview-${id}`).removeClass('tw-hidden');
+        $(`#hide-outdated-${id}`).removeClass('tw-hidden');
         // if the comment box is folded, expand it
         if ($ancestorDiffBox.attr('data-folded') && $ancestorDiffBox.attr('data-folded') === 'true') {
           setFileFolding($ancestorDiffBox[0], $ancestorDiffBox.find('.fold-file')[0], false);
@@ -452,19 +452,19 @@ export function initRepoPullRequestReview() {
   $(document).on('click', '.show-outdated', function (e) {
     e.preventDefault();
     const id = $(this).data('comment');
-    $(this).addClass('gt-hidden');
-    $(`#code-comments-${id}`).removeClass('gt-hidden');
-    $(`#code-preview-${id}`).removeClass('gt-hidden');
-    $(`#hide-outdated-${id}`).removeClass('gt-hidden');
+    $(this).addClass('tw-hidden');
+    $(`#code-comments-${id}`).removeClass('tw-hidden');
+    $(`#code-preview-${id}`).removeClass('tw-hidden');
+    $(`#hide-outdated-${id}`).removeClass('tw-hidden');
   });
 
   $(document).on('click', '.hide-outdated', function (e) {
     e.preventDefault();
     const id = $(this).data('comment');
-    $(this).addClass('gt-hidden');
-    $(`#code-comments-${id}`).addClass('gt-hidden');
-    $(`#code-preview-${id}`).addClass('gt-hidden');
-    $(`#show-outdated-${id}`).removeClass('gt-hidden');
+    $(this).addClass('tw-hidden');
+    $(`#code-comments-${id}`).addClass('tw-hidden');
+    $(`#code-preview-${id}`).addClass('tw-hidden');
+    $(`#show-outdated-${id}`).removeClass('tw-hidden');
   });
 
   $(document).on('click', 'button.comment-form-reply', async function (e) {
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 838540fcc0..838c131623 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -31,7 +31,7 @@ const {csrfToken} = window.config;
 // if there are draft comments, confirm before reloading, to avoid losing comments
 function reloadConfirmDraftComment() {
   const commentTextareas = [
-    document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
+    document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'),
     document.querySelector('#comment-form textarea'),
   ];
   for (const textarea of commentTextareas) {
@@ -197,15 +197,15 @@ export function initRepoCommentForm() {
       $(this).parent().find('.item').each(function () {
         if ($(this).hasClass('checked')) {
           listIds.push($(this).data('id'));
-          $($(this).data('id-selector')).removeClass('gt-hidden');
+          $($(this).data('id-selector')).removeClass('tw-hidden');
         } else {
-          $($(this).data('id-selector')).addClass('gt-hidden');
+          $($(this).data('id-selector')).addClass('tw-hidden');
         }
       });
       if (listIds.length === 0) {
-        $noSelect.removeClass('gt-hidden');
+        $noSelect.removeClass('tw-hidden');
       } else {
-        $noSelect.addClass('gt-hidden');
+        $noSelect.addClass('tw-hidden');
       }
       $($(this).parent().data('id')).val(listIds.join(','));
       return false;
@@ -234,9 +234,9 @@ export function initRepoCommentForm() {
       }
 
       $list.find('.item').each(function () {
-        $(this).addClass('gt-hidden');
+        $(this).addClass('tw-hidden');
       });
-      $noSelect.removeClass('gt-hidden');
+      $noSelect.removeClass('tw-hidden');
       $($(this).parent().data('id')).val('');
     });
   }
@@ -286,7 +286,7 @@ export function initRepoCommentForm() {
         </a>
       `);
 
-      $(`.ui${select_id}.list .no-select`).addClass('gt-hidden');
+      $(`.ui${select_id}.list .no-select`).addClass('tw-hidden');
       $(input_id).val($(this).data('id'));
     });
     $menu.find('.no-select.item').on('click', function () {
@@ -307,7 +307,7 @@ export function initRepoCommentForm() {
       }
 
       $list.find('.selected').html('');
-      $list.find('.no-select').removeClass('gt-hidden');
+      $list.find('.no-select').removeClass('tw-hidden');
       $(input_id).val('');
     });
   }
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
index 82e9909fec..0549fb3e31 100644
--- a/web_src/js/markup/mermaid.js
+++ b/web_src/js/markup/mermaid.js
@@ -49,7 +49,7 @@ export async function renderMermaid() {
       iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
 
       const mermaidBlock = document.createElement('div');
-      mermaidBlock.classList.add('mermaid-block', 'is-loading', 'gt-hidden');
+      mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
       mermaidBlock.append(iframe);
 
       const btn = makeCodeCopyButton();
@@ -58,7 +58,7 @@ export async function renderMermaid() {
 
       iframe.addEventListener('load', () => {
         pre.replaceWith(mermaidBlock);
-        mermaidBlock.classList.remove('gt-hidden');
+        mermaidBlock.classList.remove('tw-hidden');
         iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
         setTimeout(() => { // avoid flash of iframe background
           mermaidBlock.classList.remove('is-loading');
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 3544b47c3d..20babb331e 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -213,7 +213,7 @@ export const SvgIcon = {
       classes.push(...this.className.split(/\s+/).filter(Boolean));
     }
     if (this.symbolId) {
-      classes.push('gt-hidden', 'svg-symbol-container');
+      classes.push('tw-hidden', 'svg-symbol-container');
       svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`;
     }
     // create VNode
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index 4a6adf478e..59c455e2ab 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -22,11 +22,11 @@ function elementsCall(el, func, ...args) {
  */
 function toggleShown(el, force) {
   if (force === true) {
-    el.classList.remove('gt-hidden');
+    el.classList.remove('tw-hidden');
   } else if (force === false) {
-    el.classList.add('gt-hidden');
+    el.classList.add('tw-hidden');
   } else if (force === undefined) {
-    el.classList.toggle('gt-hidden');
+    el.classList.toggle('tw-hidden');
   } else {
     throw new Error('invalid force argument');
   }
@@ -46,7 +46,7 @@ export function toggleElem(el, force) {
 
 export function isElemHidden(el) {
   const res = [];
-  elementsCall(el, (e) => res.push(e.classList.contains('gt-hidden')));
+  elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
   if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
   return res[0];
 }

From 3f26fe2fa2c7141c9e622297e50a70f3e0003e4d Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 25 Mar 2024 02:51:08 +0800
Subject: [PATCH 508/679] Use db.ListOptions directly instead of Paginator
 interface to make it easier to use and fix performance of /pulls and /issues
 (#29990)

This PR uses `db.ListOptions` instead of `Paginor` to make the code
simpler.
And it also fixed the performance problem when viewing /pulls or
/issues. Before the counting in fact will also do the search.

---------

Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 models/issues/issue_search.go                 | 22 +++++--------------
 models/issues/issue_stats.go                  |  6 ++++-
 modules/indexer/internal/paginator.go         | 21 ++++++------------
 modules/indexer/issues/db/db.go               | 11 ++++++++++
 modules/indexer/issues/indexer.go             |  2 +-
 modules/indexer/issues/internal/model.go      |  2 +-
 .../indexer/issues/internal/tests/tests.go    |  7 ++++++
 .../indexer/issues/meilisearch/meilisearch.go | 12 ++++++++++
 8 files changed, 49 insertions(+), 34 deletions(-)

diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index 4e1bd9e87e..921dd9973e 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -21,7 +21,7 @@ import (
 
 // IssuesOptions represents options of an issue.
 type IssuesOptions struct { //nolint
-	db.Paginator
+	Paginator          *db.ListOptions
 	RepoIDs            []int64 // overwrites RepoCond if the length is not 0
 	AllPublic          bool    // include also all public repositories
 	RepoCond           builder.Cond
@@ -104,23 +104,11 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 		return sess
 	}
 
-	// Warning: Do not use GetSkipTake() for *db.ListOptions
-	// Its implementation could reset the page size with setting.API.MaxResponseItems
-	if listOptions, ok := opts.Paginator.(*db.ListOptions); ok {
-		if listOptions.Page >= 0 && listOptions.PageSize > 0 {
-			var start int
-			if listOptions.Page == 0 {
-				start = 0
-			} else {
-				start = (listOptions.Page - 1) * listOptions.PageSize
-			}
-			sess.Limit(listOptions.PageSize, start)
-		}
-		return sess
+	start := 0
+	if opts.Paginator.Page > 1 {
+		start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize
 	}
-
-	start, limit := opts.Paginator.GetSkipTake()
-	sess.Limit(limit, start)
+	sess.Limit(opts.Paginator.PageSize, start)
 
 	return sess
 }
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
index 32c5674fc9..39326616f8 100644
--- a/models/issues/issue_stats.go
+++ b/models/issues/issue_stats.go
@@ -68,13 +68,17 @@ func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int6
 }
 
 // CountIssues number return of issues by given conditions.
-func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) {
+func CountIssues(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) (int64, error) {
 	sess := db.GetEngine(ctx).
 		Select("COUNT(issue.id) AS count").
 		Table("issue").
 		Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
 	applyConditions(sess, opts)
 
+	for _, cond := range otherConds {
+		sess.And(cond)
+	}
+
 	return sess.Count()
 }
 
diff --git a/modules/indexer/internal/paginator.go b/modules/indexer/internal/paginator.go
index de0a33c06f..ee204bf047 100644
--- a/modules/indexer/internal/paginator.go
+++ b/modules/indexer/internal/paginator.go
@@ -10,7 +10,7 @@ import (
 )
 
 // ParsePaginator parses a db.Paginator into a skip and limit
-func ParsePaginator(paginator db.Paginator, max ...int) (int, int) {
+func ParsePaginator(paginator *db.ListOptions, max ...int) (int, int) {
 	// Use a very large number to indicate no limit
 	unlimited := math.MaxInt32
 	if len(max) > 0 {
@@ -19,22 +19,15 @@ func ParsePaginator(paginator db.Paginator, max ...int) (int, int) {
 	}
 
 	if paginator == nil || paginator.IsListAll() {
+		// It shouldn't happen. In actual usage scenarios, there should not be requests to search all.
+		// But if it does happen, respect it and return "unlimited".
+		// And it's also useful for testing.
 		return 0, unlimited
 	}
 
-	// Warning: Do not use GetSkipTake() for *db.ListOptions
-	// Its implementation could reset the page size with setting.API.MaxResponseItems
-	if listOptions, ok := paginator.(*db.ListOptions); ok {
-		if listOptions.Page >= 0 && listOptions.PageSize > 0 {
-			var start int
-			if listOptions.Page == 0 {
-				start = 0
-			} else {
-				start = (listOptions.Page - 1) * listOptions.PageSize
-			}
-			return start, listOptions.PageSize
-		}
-		return 0, unlimited
+	if paginator.PageSize == 0 {
+		// Do not return any results when searching, it's used to get the total count only.
+		return 0, 0
 	}
 
 	return paginator.GetSkipTake()
diff --git a/modules/indexer/issues/db/db.go b/modules/indexer/issues/db/db.go
index 1016523b72..05ec548435 100644
--- a/modules/indexer/issues/db/db.go
+++ b/modules/indexer/issues/db/db.go
@@ -78,6 +78,17 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		return nil, err
 	}
 
+	// If pagesize == 0, return total count only. It's a special case for search count.
+	if options.Paginator != nil && options.Paginator.PageSize == 0 {
+		total, err := issue_model.CountIssues(ctx, opt, cond)
+		if err != nil {
+			return nil, err
+		}
+		return &internal.SearchResult{
+			Total: total,
+		}, nil
+	}
+
 	ids, total, err := issue_model.IssueIDs(ctx, opt, cond)
 	if err != nil {
 		return nil, err
diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go
index e3bc21b49d..1cb86feb82 100644
--- a/modules/indexer/issues/indexer.go
+++ b/modules/indexer/issues/indexer.go
@@ -308,7 +308,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
 
 // CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.
 func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) {
-	opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} })
+	opts = opts.Copy(func(options *SearchOptions) { options.Paginator = &db_model.ListOptions{PageSize: 0} })
 
 	_, total, err := SearchIssues(ctx, opts)
 	return total, err
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index b7102c35af..e9c4eca559 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -106,7 +106,7 @@ type SearchOptions struct {
 	UpdatedAfterUnix  optional.Option[int64]
 	UpdatedBeforeUnix optional.Option[int64]
 
-	db.Paginator
+	Paginator *db.ListOptions
 
 	SortBy SortBy // sort by field
 }
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 2209377c2f..7f32876d80 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -77,6 +77,13 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) {
 				assert.Equal(t, c.ExpectedIDs, ids)
 				assert.Equal(t, c.ExpectedTotal, result.Total)
 			}
+
+			// test counting
+			c.SearchOptions.Paginator = &db.ListOptions{PageSize: 0}
+			countResult, err := indexer.Search(context.Background(), c.SearchOptions)
+			require.NoError(t, err)
+			assert.Empty(t, countResult.Hits)
+			assert.Equal(t, result.Total, countResult.Total)
 		})
 	}
 }
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index b735c26968..8a7cec6cba 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -218,6 +218,14 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 
 	skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxTotalHits)
 
+	counting := limit == 0
+	if counting {
+		// If set limit to 0, it will be 20 by default, and -1 is not allowed.
+		// See https://www.meilisearch.com/docs/reference/api/search#limit
+		// So set limit to 1 to make the cost as low as possible, then clear the result before returning.
+		limit = 1
+	}
+
 	keyword := options.Keyword
 	if !options.IsFuzzyKeyword {
 		// to make it non fuzzy ("typo tolerance" in meilisearch terms), we have to quote the keyword(s)
@@ -236,6 +244,10 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		return nil, err
 	}
 
+	if counting {
+		searchRes.Hits = nil
+	}
+
 	hits, err := convertHits(searchRes)
 	if err != nil {
 		return nil, err

From 314cd1ec98b1ea015e7585d3f6f5d08218379399 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Mon, 25 Mar 2024 01:44:05 +0200
Subject: [PATCH 509/679] Remove jQuery `.attr` from the repository topic bar
 (#30050)

- Switched from jQuery `.attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested the repository topic bar. It works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-home.js | 56 +++++++++++++++++---------------
 1 file changed, 29 insertions(+), 27 deletions(-)

diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index 74304d7f4a..2b0e38f087 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -6,55 +6,57 @@ import {POST} from '../modules/fetch.js';
 const {appSubUrl} = window.config;
 
 export function initRepoTopicBar() {
-  const $mgrBtn = $('#manage_topic');
-  if (!$mgrBtn.length) return;
-  const $editDiv = $('#topic_edit');
-  const $viewDiv = $('#repo-topics');
-  const $saveBtn = $('#save_topic');
-  const $topicDropdown = $('#topic_edit .dropdown');
-  const $topicForm = $editDiv; // the old logic, $editDiv is topicForm
+  const mgrBtn = document.getElementById('manage_topic');
+  if (!mgrBtn) return;
+  const editDiv = document.getElementById('topic_edit');
+  const viewDiv = document.getElementById('repo-topics');
+  const saveBtn = document.getElementById('save_topic');
+  const topicDropdown = editDiv.querySelector('.dropdown');
+  const $topicDropdown = $(topicDropdown);
+  const $topicForm = $(editDiv);
   const $topicDropdownSearch = $topicDropdown.find('input.search');
   const topicPrompts = {
-    countPrompt: $topicDropdown.attr('data-text-count-prompt'),
-    formatPrompt: $topicDropdown.attr('data-text-format-prompt'),
+    countPrompt: topicDropdown.getAttribute('data-text-count-prompt') ?? undefined,
+    formatPrompt: topicDropdown.getAttribute('data-text-format-prompt') ?? undefined,
   };
 
-  $mgrBtn.on('click', () => {
-    hideElem($viewDiv);
-    showElem($editDiv);
+  mgrBtn.addEventListener('click', () => {
+    hideElem(viewDiv);
+    showElem(editDiv);
     $topicDropdownSearch.trigger('focus');
   });
 
   $('#cancel_topic_edit').on('click', () => {
-    hideElem($editDiv);
-    showElem($viewDiv);
-    $mgrBtn.trigger('focus');
+    hideElem(editDiv);
+    showElem(viewDiv);
+    mgrBtn.focus();
   });
 
-  $saveBtn.on('click', async () => {
+  saveBtn.addEventListener('click', async () => {
     const topics = $('input[name=topics]').val();
 
     const data = new FormData();
     data.append('topics', topics);
 
-    const response = await POST($saveBtn.attr('data-link'), {data});
+    const response = await POST(saveBtn.getAttribute('data-link'), {data});
 
     if (response.ok) {
       const responseData = await response.json();
       if (responseData.status === 'ok') {
-        $viewDiv.children('.topic').remove();
+        $(viewDiv).children('.topic').remove();
         if (topics.length) {
           const topicArray = topics.split(',');
           topicArray.sort();
           for (const topic of topicArray) {
-            const $link = $('<a class="ui repo-topic large label topic tw-m-0"></a>');
-            $link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`);
-            $link.text(topic);
-            $link.insertBefore($mgrBtn); // insert all new topics before manage button
+            const link = document.createElement('a');
+            link.classList.add('ui', 'repo-topic', 'large', 'label', 'topic', 'tw-m-0');
+            link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
+            link.textContent = topic;
+            mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button
           }
         }
-        hideElem($editDiv);
-        showElem($viewDiv);
+        hideElem(editDiv);
+        showElem(viewDiv);
       }
     } else if (response.status === 422) {
       const responseData = await response.json();
@@ -144,14 +146,14 @@ export function initRepoTopicBar() {
     },
     onAdd(addedValue, _addedText, $addedChoice) {
       addedValue = addedValue.toLowerCase().trim();
-      $($addedChoice).attr('data-value', addedValue);
-      $($addedChoice).attr('data-text', addedValue);
+      $($addedChoice)[0].setAttribute('data-value', addedValue);
+      $($addedChoice)[0].setAttribute('data-text', addedValue);
     },
   });
 
   $.fn.form.settings.rules.validateTopic = function (_values, regExp) {
     const $topics = $topicDropdown.children('a.ui.label');
-    const status = $topics.length === 0 || $topics.last().attr('data-value').match(regExp);
+    const status = $topics.length === 0 || $topics.last()[0].getAttribute('data-value').match(regExp);
     if (!status) {
       $topics.last().removeClass('green').addClass('red');
     }

From a7d0c5de4c82d8d206f6c5c51f012ee831502f67 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Mon, 25 Mar 2024 01:50:39 +0200
Subject: [PATCH 510/679] Remove jQuery `.attr` from the label edit exclusive
 checkbox (#30053)

- Switched from jQuery `attr` to plain javascript `getAttribute`
- Tested the label edit exclusive checkbox and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/comp/LabelEdit.js | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index c5992fa355..843657a6b6 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -6,23 +6,23 @@ function isExclusiveScopeName(name) {
 }
 
 function updateExclusiveLabelEdit(form) {
-  const $nameInput = $(`${form} .label-name-input`);
-  const $exclusiveField = $(`${form} .label-exclusive-input-field`);
-  const $exclusiveCheckbox = $(`${form} .label-exclusive-input`);
-  const $exclusiveWarning = $(`${form} .label-exclusive-warning`);
+  const nameInput = document.querySelector(`${form} .label-name-input`);
+  const exclusiveField = document.querySelector(`${form} .label-exclusive-input-field`);
+  const exclusiveCheckbox = document.querySelector(`${form} .label-exclusive-input`);
+  const exclusiveWarning = document.querySelector(`${form} .label-exclusive-warning`);
 
-  if (isExclusiveScopeName($nameInput.val())) {
-    $exclusiveField.removeClass('muted');
-    $exclusiveField.removeAttr('aria-disabled');
-    if ($exclusiveCheckbox[0].checked && $exclusiveCheckbox.data('exclusive-warn')) {
-      $exclusiveWarning.removeClass('tw-hidden');
+  if (isExclusiveScopeName(nameInput.value)) {
+    exclusiveField?.classList.remove('muted');
+    exclusiveField?.removeAttribute('aria-disabled');
+    if (exclusiveCheckbox.checked && exclusiveCheckbox.getAttribute('data-exclusive-warn')) {
+      exclusiveWarning?.classList.remove('tw-hidden');
     } else {
-      $exclusiveWarning.addClass('tw-hidden');
+      exclusiveWarning?.classList.add('tw-hidden');
     }
   } else {
-    $exclusiveField.addClass('muted');
-    $exclusiveField.attr('aria-disabled', 'true');
-    $exclusiveWarning.addClass('tw-hidden');
+    exclusiveField?.classList.add('muted');
+    exclusiveField?.setAttribute('aria-disabled', 'true');
+    exclusiveWarning?.classList.add('tw-hidden');
   }
 }
 

From 428e05662f4f745fe7fef04ce9218a86aa4f1b6c Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Mon, 25 Mar 2024 02:00:54 +0200
Subject: [PATCH 511/679] Remove jQuery `.attr` from the ComboMarkdownEditor
 (#30051)

- Switched from jQuery `attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested the markdown editor and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 .../js/features/comp/ComboMarkdownEditor.js   | 31 +++++++++----------
 1 file changed, 15 insertions(+), 16 deletions(-)

diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index 1e7b554b98..1e728ca201 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -132,34 +132,33 @@ class ComboMarkdownEditor {
 
   setupTab() {
     const $container = $(this.container);
-    const $tabMenu = $container.find('.tabular.menu');
-    const $tabs = $tabMenu.find('> .item');
+    const tabs = $container[0].querySelectorAll('.tabular.menu > .item');
 
     // Fomantic Tab requires the "data-tab" to be globally unique.
     // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
-    const $tabEditor = $tabs.filter(`.item[data-tab-for="markdown-writer"]`);
-    const $tabPreviewer = $tabs.filter(`.item[data-tab-for="markdown-previewer"]`);
-    $tabEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`);
-    $tabPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`);
-    const $panelEditor = $container.find('.ui.tab[data-tab-panel="markdown-writer"]');
-    const $panelPreviewer = $container.find('.ui.tab[data-tab-panel="markdown-previewer"]');
-    $panelEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`);
-    $panelPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`);
+    const tabEditor = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-writer');
+    const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer');
+    tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
+    tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
+    const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]');
+    const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
+    panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
+    panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
     elementIdCounter++;
 
-    $tabEditor[0].addEventListener('click', () => {
+    tabEditor.addEventListener('click', () => {
       requestAnimationFrame(() => {
         this.focus();
       });
     });
 
-    $tabs.tab();
+    $(tabs).tab();
 
-    this.previewUrl = $tabPreviewer.attr('data-preview-url');
-    this.previewContext = $tabPreviewer.attr('data-preview-context');
+    this.previewUrl = tabPreviewer.getAttribute('data-preview-url');
+    this.previewContext = tabPreviewer.getAttribute('data-preview-context');
     this.previewMode = this.options.previewMode ?? 'comment';
     this.previewWiki = this.options.previewWiki ?? false;
-    $tabPreviewer.on('click', async () => {
+    tabPreviewer.addEventListener('click', async () => {
       const formData = new FormData();
       formData.append('mode', this.previewMode);
       formData.append('context', this.previewContext);
@@ -167,7 +166,7 @@ class ComboMarkdownEditor {
       formData.append('wiki', this.previewWiki);
       const response = await POST(this.previewUrl, {data: formData});
       const data = await response.text();
-      renderPreviewPanelContent($panelPreviewer, data);
+      renderPreviewPanelContent($(panelPreviewer), data);
     });
   }
 

From 2e31a2800e1112ee0ab5a8d3c66b0fba2e737870 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Mon, 25 Mar 2024 06:30:38 +0200
Subject: [PATCH 512/679] Remove jQuery `.attr` from the reaction selector
 (#30052)

- Switched from jQuery `attr` to plain javascript `getAttribute`
- Tested the reaction selector and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/comp/ReactionSelector.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js
index 6df4bde069..fc966c3985 100644
--- a/web_src/js/features/comp/ReactionSelector.js
+++ b/web_src/js/features/comp/ReactionSelector.js
@@ -7,9 +7,9 @@ export function initCompReactionSelector($parent) {
 
     if ($(this).hasClass('disabled')) return;
 
-    const actionUrl = $(this).closest('[data-action-url]').attr('data-action-url');
-    const reactionContent = $(this).attr('data-reaction-content');
-    const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true';
+    const actionUrl = this.closest('[data-action-url]')?.getAttribute('data-action-url');
+    const reactionContent = this.getAttribute('data-reaction-content');
+    const hasReacted = this.closest('.ui.segment.reactions')?.querySelector(`a[data-reaction-content="${reactionContent}"]`)?.getAttribute('data-has-reacted') === 'true';
 
     const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
       data: new URLSearchParams({content: reactionContent}),

From c6c4d66004c70b24abc8048b39b660b8361a0395 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Mon, 25 Mar 2024 15:00:16 +0800
Subject: [PATCH 513/679] Fix misuse of `TxContext` (#30061)

Help #29999, or its tests cannot pass.

Also, add some comments to clarify the usage of `TxContext`.

I don't check all usages of `TxContext` because there are too many
(almost 140+). It's a better idea to replace them with `WithTx` instead
of checking them one by one. However, that may be another refactoring
PR.
---
 models/db/context.go    | 10 ++++++++++
 models/issues/review.go |  2 +-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/models/db/context.go b/models/db/context.go
index cda608af19..43f612518a 100644
--- a/models/db/context.go
+++ b/models/db/context.go
@@ -120,6 +120,16 @@ func (c *halfCommitter) Close() error {
 
 // TxContext represents a transaction Context,
 // it will reuse the existing transaction in the parent context or create a new one.
+// Some tips to use:
+//
+//	1 It's always recommended to use `WithTx` in new code instead of `TxContext`, since `WithTx` will handle the transaction automatically.
+//	2. To maintain the old code which uses `TxContext`:
+//	  a. Always call `Close()` before returning regardless of whether `Commit()` has been called.
+//	  b. Always call `Commit()` before returning if there are no errors, even if the code did not change any data.
+//	  c. Remember the `Committer` will be a halfCommitter when a transaction is being reused.
+//	     So calling `Commit()` will do nothing, but calling `Close()` without calling `Commit()` will rollback the transaction.
+//	     And all operations submitted by the caller stack will be rollbacked as well, not only the operations in the current function.
+//	  d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback.
 func TxContext(parentCtx context.Context) (*Context, Committer, error) {
 	if sess, ok := inTransaction(parentCtx); ok {
 		return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil
diff --git a/models/issues/review.go b/models/issues/review.go
index 70aba0f94d..455bcda50a 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -620,7 +620,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
 
 	// skip it when reviewer hase been request to review
 	if review != nil && review.Type == ReviewTypeRequest {
-		return nil, nil
+		return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
 	}
 
 	// if the reviewer is an official reviewer,

From 475b6e839caa88994318f905f0965c3b418f876a Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 25 Mar 2024 15:51:23 +0800
Subject: [PATCH 514/679] Fix Add/Remove WIP on pull request title failure
 (#29999)

Fix #29997
---
 services/issue/issue.go               | 27 +++++++++++----------------
 services/issue/pull.go                | 16 ++++++++++------
 tests/integration/pull_review_test.go | 16 +++++++++++++++-
 3 files changed, 36 insertions(+), 23 deletions(-)

diff --git a/services/issue/issue.go b/services/issue/issue.go
index 94b0ee6f69..c7fa9f3300 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -17,6 +17,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
@@ -85,25 +86,19 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 		}
 	}
 
-	var reviewNotifers []*ReviewRequestNotifier
-
-	if err := db.WithTx(ctx, func(ctx context.Context) error {
-		if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
-			return err
-		}
-
-		if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
-			var err error
-			reviewNotifers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
-			if err != nil {
-				return err
-			}
-		}
-		return nil
-	}); err != nil {
+	if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
 		return err
 	}
 
+	var reviewNotifers []*ReviewRequestNotifier
+	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
+		var err error
+		reviewNotifers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
+		if err != nil {
+			log.Error("PullRequestCodeOwnersReview: %v", err)
+		}
+	}
+
 	notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle)
 	ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers)
 
diff --git a/services/issue/pull.go b/services/issue/pull.go
index 8e85c11e9b..b7b63a7024 100644
--- a/services/issue/pull.go
+++ b/services/issue/pull.go
@@ -40,7 +40,7 @@ type ReviewRequestNotifier struct {
 	ReviewTeam *org_model.Team
 }
 
-func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
+func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
 	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
 
 	if pr.IsWorkInProgress(ctx) {
@@ -90,7 +90,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue,
 
 	// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
 	// between the merge base and the head commit but not the base branch and the head commit
-	changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.HeadCommitID)
+	changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitRefName())
 	if err != nil {
 		return nil, err
 	}
@@ -112,9 +112,13 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue,
 
 	notifiers := make([]*ReviewRequestNotifier, 0, len(uniqUsers)+len(uniqTeams))
 
+	if err := issue.LoadPoster(ctx); err != nil {
+		return nil, err
+	}
+
 	for _, u := range uniqUsers {
-		if u.ID != pull.Poster.ID {
-			comment, err := issues_model.AddReviewRequest(ctx, pull, u, pull.Poster)
+		if u.ID != issue.Poster.ID {
+			comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
 			if err != nil {
 				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
 				return nil, err
@@ -122,12 +126,12 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue,
 			notifiers = append(notifiers, &ReviewRequestNotifier{
 				Comment: comment,
 				IsAdd:   true,
-				Reviwer: pull.Poster,
+				Reviwer: u,
 			})
 		}
 	}
 	for _, t := range uniqTeams {
-		comment, err := issues_model.AddTeamReviewRequest(ctx, pull, t, pull.Poster)
+		comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
 		if err != nil {
 			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
 			return nil, err
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index bdfecb3280..9a5877697c 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -15,6 +15,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/test"
+	issue_service "code.gitea.io/gitea/services/issue"
 	repo_service "code.gitea.io/gitea/services/repository"
 	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
@@ -87,8 +88,21 @@ func TestPullView_CodeOwner(t *testing.T) {
 			session := loginUser(t, "user2")
 			testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request")
 
-			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch"})
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "codeowner-basebranch"})
 			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
+			assert.NoError(t, pr.LoadIssue(db.DefaultContext))
+
+			err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request")
+			assert.NoError(t, err)
+			prUpdated1 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+			assert.NoError(t, prUpdated1.LoadIssue(db.DefaultContext))
+			assert.EqualValues(t, "[WIP] Test Pull Request", prUpdated1.Issue.Title)
+
+			err = issue_service.ChangeTitle(db.DefaultContext, prUpdated1.Issue, user2, "Test Pull Request2")
+			assert.NoError(t, err)
+			prUpdated2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+			assert.NoError(t, prUpdated2.LoadIssue(db.DefaultContext))
+			assert.EqualValues(t, "Test Pull Request2", prUpdated2.Issue.Title)
 		})
 
 		// change the default branch CODEOWNERS file to change README.md's codeowner

From bbaf62589fe538be4afc86455d772360de80e7d8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 25 Mar 2024 11:14:43 +0100
Subject: [PATCH 515/679] Fix button hover border (#30048)

Fix regression from https://github.com/go-gitea/gitea/pull/30014. The
rule was to broad and affecting things like `primary` button
unintentionally.
---
 web_src/css/modules/button.css | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css
index 849956b72c..faeed8c9a1 100644
--- a/web_src/css/modules/button.css
+++ b/web_src/css/modules/button.css
@@ -11,7 +11,6 @@
 .ui.button:hover {
   background: var(--color-hover);
   color: var(--color-text);
-  border-color: var(--color-secondary-dark-2);
 }
 
 .page-content .ui.button {
@@ -62,6 +61,10 @@ It needs some tricks to tweak the left/right borders with active state */
   border-right: none;
 }
 
+.ui.buttons .button:hover {
+  border-color: var(--color-secondary-dark-2);
+}
+
 .ui.buttons .button:hover + .button {
   border-left: 1px solid var(--color-secondary-dark-2);
 }

From 8e79aed573a3597c028bfc3598bd78f12e8a3ac3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 25 Mar 2024 21:25:22 +0800
Subject: [PATCH 516/679] Fix git grep search limit, add test (#30071)

Fix #30069
---
 modules/git/grep.go      |  8 +++++++-
 modules/git/grep_test.go | 10 ++++++++++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/modules/git/grep.go b/modules/git/grep.go
index e533995984..a6c486112a 100644
--- a/modules/git/grep.go
+++ b/modules/git/grep.go
@@ -24,6 +24,7 @@ type GrepResult struct {
 
 type GrepOptions struct {
 	RefName           string
+	MaxResultLimit    int
 	ContextLineNumber int
 	IsFuzzy           bool
 }
@@ -59,6 +60,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
 		cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
 	}
 	cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
+	opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
 	stderr := bytes.Buffer{}
 	err = cmd.Run(&RunOpts{
 		Dir:    repo.Path,
@@ -82,7 +84,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
 					continue
 				}
 				if line == "" {
-					if len(results) >= 50 {
+					if len(results) >= opts.MaxResultLimit {
 						cancel()
 						break
 					}
@@ -101,6 +103,10 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
 			return scanner.Err()
 		},
 	})
+	// git grep exits by cancel (killed), usually it is caused by the limit of results
+	if IsErrorExitCode(err, -1) && stderr.Len() == 0 {
+		return results, nil
+	}
 	// git grep exits with 1 if no results are found
 	if IsErrorExitCode(err, 1) && stderr.Len() == 0 {
 		return nil, nil
diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go
index 3993fa7ffc..b5fa437c53 100644
--- a/modules/git/grep_test.go
+++ b/modules/git/grep_test.go
@@ -31,6 +31,16 @@ func TestGrepSearch(t *testing.T) {
 		},
 	}, res)
 
+	res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
+	assert.NoError(t, err)
+	assert.Equal(t, []*GrepResult{
+		{
+			Filename:    "java-hello/main.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] args)"},
+		},
+	}, res)
+
 	res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, res, 0)

From 8717c1c2bef1afcc6b0bb2d84627b158b95836b0 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 25 Mar 2024 14:52:54 +0100
Subject: [PATCH 517/679] Fix menu buttons in issues and release (#30056)

Fix regression from https://github.com/go-gitea/gitea/pull/30033

These buttons had lost their border because `.ui.header` sets `none` but
`.ui.menu` has it, after the migration, the order of styles changed and
header won. I see no reason why those have the `header` class in first
place, besides for semantic meaning.

Before:
<img width="491" alt="Screenshot 2024-03-25 at 00 39 27"
src="https://github.com/go-gitea/gitea/assets/115237/fa1b7505-75cf-4854-a97f-db3c46f31e93">

After:
<img width="496" alt="Screenshot 2024-03-25 at 00 39 14"
src="https://github.com/go-gitea/gitea/assets/115237/8f6bdc07-9596-436b-8c82-9af283300004">
---
 templates/repo/issue/navbar.tmpl       | 2 +-
 templates/repo/release_tag_header.tmpl | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/repo/issue/navbar.tmpl b/templates/repo/issue/navbar.tmpl
index 16110597ed..30e42c77cc 100644
--- a/templates/repo/issue/navbar.tmpl
+++ b/templates/repo/issue/navbar.tmpl
@@ -1,4 +1,4 @@
-<h2 class="ui compact small menu header small-menu-items issue-list-navbar">
+<h2 class="ui compact small menu small-menu-items issue-list-navbar">
 	<a class="{{if .PageIsLabels}}active {{end}}item" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
 	<a class="{{if .PageIsMilestones}}active {{end}}item" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
 </h2>
diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl
index cc69cecd6d..ab1e58620d 100644
--- a/templates/repo/release_tag_header.tmpl
+++ b/templates/repo/release_tag_header.tmpl
@@ -4,7 +4,7 @@
 {{if $canReadReleases}}
 	<div class="tw-flex">
 		<div class="tw-flex-1 tw-flex tw-items-center">
-			<h2 class="ui compact small menu header small-menu-items">
+			<h2 class="ui compact small menu small-menu-items">
 				<a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
 				{{if $canReadCode}}
 					<a class="{{if or .PageIsTagList .PageIsSingleTag}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a>

From f73d891fc4979cbd704d145f7e892f73d0eb5e39 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 25 Mar 2024 16:40:50 +0100
Subject: [PATCH 518/679] Remove fomantic table module (#30047)

Big CSS module. I tested basic functionality on admin and commits table.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/base.css                |   80 --
 web_src/css/index.css               |    1 +
 web_src/css/modules/table.css       |  356 +++++++
 web_src/fomantic/build/semantic.css | 1362 ---------------------------
 web_src/fomantic/semantic.json      |    3 +-
 5 files changed, 358 insertions(+), 1444 deletions(-)
 create mode 100644 web_src/css/modules/table.css

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 3f46e4cd1a..7431f1dbd1 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -620,86 +620,6 @@ ol.ui.list li,
   color: var(--color-primary);
 }
 
-.ui.attached.table {
-  border-color: var(--color-secondary);
-}
-
-.ui.table {
-  color: var(--color-text);
-  background: var(--color-box-body);
-  border-color: var(--color-secondary);
-  text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */
-}
-
-.ui.table th,
-.ui.table td {
-  transition: none;
-}
-
-.ui.table > tr > td,
-.ui.table > tbody > tr > td {
-  border-top-color: var(--color-secondary-alpha-50);
-}
-
-.ui.striped.table > tr:nth-child(2n),
-.ui.striped.table > tbody > tr:nth-child(2n),
-.ui.basic.striped.table > tbody > tr:nth-child(2n) {
-  background: var(--color-light);
-}
-
-.ui.ui.ui.ui.table tr.active,
-.ui.ui.table td.active {
-  color: var(--color-text);
-  background: var(--color-active);
-}
-
-.ui.ui.selectable.table > tbody > tr:hover,
-.ui.table tbody tr td.selectable:hover {
-  color: var(--color-text);
-  background-color: var(--color-secondary-alpha-40);
-}
-
-.ui.ui.ui.ui.table tr.grey:not(.marked),
-.ui.ui.table td.grey:not(.marked) {
-  background: var(--color-body);
-  color: var(--color-text);
-}
-
-.ui.table > thead > tr > th {
-  background: var(--color-box-header);
-  border-color: var(--color-secondary);
-  color: var(--color-text);
-}
-
-.ui.basic.table > tbody > tr {
-  border-color: var(--color-secondary);
-}
-
-.ui.table > tfoot > tr > th,
-.ui.table > tfoot > tr > td {
-  border-color: var(--color-secondary);
-  background: var(--color-box-body);
-  color: var(--color-text);
-}
-
-/* reduce table padding, needed especially for dense admin tables */
-.ui.table > thead > tr > th,
-.ui.table > tbody > tr > td,
-.ui.table > tr > td {
-  padding: 6px 5px;
-}
-/* use more horizontal padding on first and last items for visuals */
-.ui.table > thead > tr > th:first-of-type,
-.ui.table > tbody > tr > td:first-of-type,
-.ui.table > tr > td:first-of-type {
-  padding-left: 10px;
-}
-.ui.table > thead > tr > th:last-of-type,
-.ui.table > tbody > tr > td:last-of-type,
-.ui.table > tr > td:last-of-type {
-  padding-right: 10px;
-}
-
 img.ui.avatar,
 .ui.avatar img,
 .ui.avatar svg {
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 4258b85797..74b5617e1c 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -9,6 +9,7 @@
 @import "./modules/segment.css";
 @import "./modules/grid.css";
 @import "./modules/message.css";
+@import "./modules/table.css";
 @import "./modules/card.css";
 @import "./modules/modal.css";
 
diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css
new file mode 100644
index 0000000000..962a5f52a6
--- /dev/null
+++ b/web_src/css/modules/table.css
@@ -0,0 +1,356 @@
+/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.table {
+  width: 100%;
+  margin: 1em 0;
+  border: 1px solid var(--color-secondary);
+  border-radius: 0.28571429rem;
+  vertical-align: middle;
+  border-collapse: separate;
+  border-spacing: 0;
+  color: var(--color-text);
+  background: var(--color-box-body);
+  border-color: var(--color-secondary);
+  text-align: start;
+}
+
+.ui.table:first-child {
+  margin-top: 0;
+}
+.ui.table:last-child {
+  margin-bottom: 0;
+}
+.ui.table > thead,
+.ui.table > tbody {
+  vertical-align: inherit;
+}
+
+.ui.table > thead > tr > th {
+  background: var(--color-box-header);
+  color: var(--color-text);
+  padding: 6px 5px;
+  vertical-align: inherit;
+  font-weight: var(--font-weight-normal);
+  border-bottom: 1px solid var(--color-secondary);
+  border-left: none;
+}
+.ui.table > thead > tr > th:first-child {
+  border-left: none;
+}
+.ui.table > thead > tr:first-child > th:first-child {
+  border-radius: 0.28571429rem 0 0;
+}
+.ui.table > thead > tr:first-child > th:last-child {
+  border-radius: 0 0.28571429rem 0 0;
+}
+.ui.table > thead > tr:first-child > th:only-child {
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.table > tfoot > tr > th,
+.ui.table > tfoot > tr > td {
+  border-top: 1px solid var(--color-secondary);
+  background: var(--color-box-body);
+  color: var(--color-text);
+  padding: 0.78571429em;
+  vertical-align: inherit;
+  font-weight: var(--font-weight-normal);
+}
+.ui.table > tfoot > tr > th:first-child,
+.ui.table > tfoot > tr > td:first-child {
+  border-left: none;
+}
+.ui.table > tfoot > tr:first-child > th:first-child,
+.ui.table > tfoot > tr:first-child > td:first-child {
+  border-radius: 0 0 0 0.28571429rem;
+}
+.ui.table > tfoot > tr:first-child > th:last-child,
+.ui.table > tfoot > tr:first-child > td:last-child {
+  border-radius: 0 0 0.28571429rem;
+}
+.ui.table > tfoot > tr:first-child > th:only-child,
+.ui.table > tfoot > tr:first-child > td:only-child {
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.table > tr > td,
+.ui.table > tbody > tr > td {
+  border-top: 1px solid var(--color-secondary-alpha-50);
+  padding: 6px 5px;
+}
+.ui.table > tr:first-child > td,
+.ui.table > tbody > tr:first-child > td {
+  border-top: none;
+}
+
+.ui.table.segment {
+  padding: 0;
+}
+.ui.table.segment::after {
+  display: none;
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui.table:not(.unstackable) {
+    width: 100%;
+    padding: 0;
+  }
+  .ui.table:not(.unstackable) > thead,
+  .ui.table:not(.unstackable) > thead > tr,
+  .ui.table:not(.unstackable) > tfoot,
+  .ui.table:not(.unstackable) > tfoot > tr,
+  .ui.table:not(.unstackable) > tbody,
+  .ui.table:not(.unstackable) > tr,
+  .ui.table:not(.unstackable) > tbody > tr,
+  .ui.table:not(.unstackable) > tr > th,
+  .ui.table:not(.unstackable) > thead > tr > th,
+  .ui.table:not(.unstackable) > tbody > tr > th,
+  .ui.table:not(.unstackable) > tfoot > tr > th,
+  .ui.table:not(.unstackable) > tr > td,
+  .ui.table:not(.unstackable) > tbody > tr > td,
+  .ui.table:not(.unstackable) > tfoot > tr > td {
+    display: block !important;
+    width: auto !important;
+  }
+  .ui.table:not(.unstackable) > thead {
+    display: block;
+  }
+  .ui.table:not(.unstackable) > tfoot {
+    display: block;
+  }
+  .ui.ui.ui.ui.table:not(.unstackable) > tr,
+  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr,
+  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr,
+  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr {
+    padding-top: 1em;
+    padding-bottom: 1em;
+  }
+  .ui.ui.ui.ui.table:not(.unstackable) > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > tr > td,
+  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > td,
+  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > td {
+    background: none;
+    border: none;
+    padding: 0.25em 0.75em;
+  }
+  .ui.table:not(.unstackable) > tr > th:first-child,
+  .ui.table:not(.unstackable) > thead > tr > th:first-child,
+  .ui.table:not(.unstackable) > tbody > tr > th:first-child,
+  .ui.table:not(.unstackable) > tfoot > tr > th:first-child,
+  .ui.table:not(.unstackable) > tr > td:first-child,
+  .ui.table:not(.unstackable) > tbody > tr > td:first-child,
+  .ui.table:not(.unstackable) > tfoot > tr > td:first-child {
+    font-weight: var(--font-weight-normal);
+  }
+}
+
+.ui.table th.collapsing,
+.ui.table td.collapsing {
+  width: 1px;
+  white-space: nowrap;
+}
+
+.ui.fixed.table {
+  table-layout: fixed;
+}
+.ui.fixed.table th,
+.ui.fixed.table td {
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.ui.attached.table {
+  top: 0;
+  bottom: 0;
+  border-radius: 0;
+  margin: 0 -1px;
+  width: calc(100% + 2px);
+  max-width: calc(100% + 2px);
+  border: 1px solid var(--color-secondary);
+}
+.ui.attached + .ui.attached.table:not(.top) {
+  border-top: none;
+}
+
+.ui[class*="bottom attached"].table {
+  bottom: 0;
+  margin-top: 0;
+  top: 0;
+  margin-bottom: 1em;
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+.ui[class*="bottom attached"].table:last-child {
+  margin-bottom: 0;
+}
+
+.ui.striped.table > tr:nth-child(2n),
+.ui.striped.table > tbody > tr:nth-child(2n) {
+  background: var(--color-light);
+}
+
+.ui.table[class*="single line"],
+.ui.table [class*="single line"] {
+  white-space: nowrap;
+}
+
+/* Column Width */
+.ui.table th.one.wide,
+.ui.table td.one.wide {
+  width: 6.25%;
+}
+.ui.table th.two.wide,
+.ui.table td.two.wide {
+  width: 12.5%;
+}
+.ui.table th.three.wide,
+.ui.table td.three.wide {
+  width: 18.75%;
+}
+.ui.table th.four.wide,
+.ui.table td.four.wide {
+  width: 25%;
+}
+.ui.table th.five.wide,
+.ui.table td.five.wide {
+  width: 31.25%;
+}
+.ui.table th.six.wide,
+.ui.table td.six.wide {
+  width: 37.5%;
+}
+.ui.table th.seven.wide,
+.ui.table td.seven.wide {
+  width: 43.75%;
+}
+.ui.table th.eight.wide,
+.ui.table td.eight.wide {
+  width: 50%;
+}
+.ui.table th.nine.wide,
+.ui.table td.nine.wide {
+  width: 56.25%;
+}
+.ui.table th.ten.wide,
+.ui.table td.ten.wide {
+  width: 62.5%;
+}
+.ui.table th.eleven.wide,
+.ui.table td.eleven.wide {
+  width: 68.75%;
+}
+.ui.table th.twelve.wide,
+.ui.table td.twelve.wide {
+  width: 75%;
+}
+.ui.table th.thirteen.wide,
+.ui.table td.thirteen.wide {
+  width: 81.25%;
+}
+.ui.table th.fourteen.wide,
+.ui.table td.fourteen.wide {
+  width: 87.5%;
+}
+.ui.table th.fifteen.wide,
+.ui.table td.fifteen.wide {
+  width: 93.75%;
+}
+.ui.table th.sixteen.wide,
+.ui.table td.sixteen.wide {
+  width: 100%;
+}
+
+.ui.basic.table {
+  background: transparent;
+  border: 1px solid var(--color-secondary);
+}
+.ui.basic.table > thead > tr > th,
+.ui.basic.table > tbody > tr > th,
+.ui.basic.table > tfoot > tr > th,
+.ui.basic.table > tr > th {
+  background: transparent;
+  border-left: none;
+}
+.ui.basic.table > tbody > tr {
+  border-bottom: 1px solid var(--color-secondary);
+}
+.ui.basic.table > tbody > tr > td,
+.ui.basic.table > tfoot > tr > td,
+.ui.basic.table > tr > td {
+  background: transparent;
+}
+.ui.basic.striped.table > tbody > tr:nth-child(2n) {
+  background: var(--color-light);
+}
+
+.ui[class*="very basic"].table {
+  border: none;
+}
+.ui[class*="very basic"].table:not(.striped) > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > thead > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tr > td:first-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:first-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:first-child {
+  padding-left: 0;
+}
+.ui[class*="very basic"].table:not(.striped) > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > thead > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tr > td:last-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:last-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:last-child {
+  padding-right: 0;
+}
+.ui[class*="very basic"].table:not(.striped) > thead > tr:first-child > th {
+  padding-top: 0;
+}
+
+.ui.celled.table > tr > th,
+.ui.celled.table > thead > tr > th,
+.ui.celled.table > tbody > tr > th,
+.ui.celled.table > tfoot > tr > th,
+.ui.celled.table > tr > td,
+.ui.celled.table > tbody > tr > td,
+.ui.celled.table > tfoot > tr > td {
+  border-left: 1px solid var(--color-secondary-alpha-50);
+}
+.ui.celled.table > tr > th:first-child,
+.ui.celled.table > thead > tr > th:first-child,
+.ui.celled.table > tbody > tr > th:first-child,
+.ui.celled.table > tfoot > tr > th:first-child,
+.ui.celled.table > tr > td:first-child,
+.ui.celled.table > tbody > tr > td:first-child,
+.ui.celled.table > tfoot > tr > td:first-child {
+  border-left: none;
+}
+
+.ui.compact.table > tr > th,
+.ui.compact.table > thead > tr > th,
+.ui.compact.table > tbody > tr > th,
+.ui.compact.table > tfoot > tr > th {
+  padding-left: 0.7em;
+  padding-right: 0.7em;
+}
+.ui.compact.table > tr > td,
+.ui.compact.table > tbody > tr > td,
+.ui.compact.table > tfoot > tr > td {
+  padding: 0.5em 0.7em;
+}
+
+/* use more horizontal padding on first and last items for visuals */
+.ui.table > thead > tr > th:first-of-type,
+.ui.table > tbody > tr > td:first-of-type,
+.ui.table > tr > td:first-of-type {
+  padding-left: 10px;
+}
+.ui.table > thead > tr > th:last-of-type,
+.ui.table > tbody > tr > td:last-of-type,
+.ui.table > tr > td:last-of-type {
+  padding-right: 10px;
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 5421641da8..05a3387563 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -13476,1366 +13476,4 @@ Floated Menu / Item
 
 /*******************************
         User Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Table
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-             Table
-*******************************/
-
-/* Prototype */
-
-.ui.table {
-  width: 100%;
-  background: #FFFFFF;
-  margin: 1em 0;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: none;
-  border-radius: 0.28571429rem;
-  text-align: left;
-  vertical-align: middle;
-  color: rgba(0, 0, 0, 0.87);
-  border-collapse: separate;
-  border-spacing: 0;
-}
-
-.ui.table:first-child {
-  margin-top: 0;
-}
-
-.ui.table:last-child {
-  margin-bottom: 0;
-}
-
-.ui.table > thead,
-.ui.table > tbody {
-  text-align: inherit;
-  vertical-align: inherit;
-}
-
-/*******************************
-             Parts
-*******************************/
-
-/* Table Content */
-
-.ui.table th,
-.ui.table td {
-  transition: background 0.1s ease, color 0.1s ease;
-}
-
-/* Rowspan helper class */
-
-.ui.table th.rowspanned,
-.ui.table td.rowspanned {
-  display: none;
-}
-
-/* Headers */
-
-.ui.table > thead {
-  box-shadow: none;
-}
-
-.ui.table > thead > tr > th {
-  cursor: auto;
-  background: #F9FAFB;
-  text-align: inherit;
-  color: rgba(0, 0, 0, 0.87);
-  padding: 0.92857143em 0.78571429em;
-  vertical-align: inherit;
-  font-style: none;
-  font-weight: 500;
-  text-transform: none;
-  border-bottom: 1px solid rgba(34, 36, 38, 0.1);
-  border-left: none;
-}
-
-.ui.table > thead > tr > th:first-child {
-  border-left: none;
-}
-
-.ui.table > thead > tr:first-child > th:first-child {
-  border-radius: 0.28571429rem 0 0 0;
-}
-
-.ui.table > thead > tr:first-child > th:last-child {
-  border-radius: 0 0.28571429rem 0 0;
-}
-
-.ui.table > thead > tr:first-child > th:only-child {
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-/* Footer */
-
-.ui.table > tfoot {
-  box-shadow: none;
-}
-
-.ui.table > tfoot > tr > th,
-.ui.table > tfoot > tr > td {
-  cursor: auto;
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-  background: #F9FAFB;
-  text-align: inherit;
-  color: rgba(0, 0, 0, 0.87);
-  padding: 0.78571429em 0.78571429em;
-  vertical-align: inherit;
-  font-style: normal;
-  font-weight: normal;
-  text-transform: none;
-}
-
-.ui.table > tfoot > tr > th:first-child,
-.ui.table > tfoot > tr > td:first-child {
-  border-left: none;
-}
-
-.ui.table > tfoot > tr:first-child > th:first-child,
-.ui.table > tfoot > tr:first-child > td:first-child {
-  border-radius: 0 0 0 0.28571429rem;
-}
-
-.ui.table > tfoot > tr:first-child > th:last-child,
-.ui.table > tfoot > tr:first-child > td:last-child {
-  border-radius: 0 0 0.28571429rem 0;
-}
-
-.ui.table > tfoot > tr:first-child > th:only-child,
-.ui.table > tfoot > tr:first-child > td:only-child {
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-/* Table Row */
-
-.ui.table > tr > td,
-.ui.table > tbody > tr > td {
-  border-top: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-.ui.table > tr:first-child > td,
-.ui.table > tbody > tr:first-child > td {
-  border-top: none;
-}
-
-/* Repeated tbody */
-
-.ui.table > tbody + tbody tr:first-child > td {
-  border-top: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-/* Table Cells */
-
-.ui.table > tbody > tr > td,
-.ui.table > tr > td {
-  padding: 0.78571429em 0.78571429em;
-  text-align: inherit;
-}
-
-/* Icons */
-
-.ui.table > i.icon {
-  vertical-align: baseline;
-}
-
-.ui.table > i.icon:only-child {
-  margin: 0;
-}
-
-/* Table Segment */
-
-.ui.table.segment {
-  padding: 0;
-}
-
-.ui.table.segment:after {
-  display: none;
-}
-
-.ui.table.segment.stacked:after {
-  display: block;
-}
-
-/* Responsive */
-
-@media only screen and (max-width: 767.98px) {
-  .ui.table:not(.unstackable) {
-    width: 100%;
-    padding: 0;
-  }
-
-  .ui.table:not(.unstackable) > thead,
-  .ui.table:not(.unstackable) > thead > tr,
-  .ui.table:not(.unstackable) > tfoot,
-  .ui.table:not(.unstackable) > tfoot > tr,
-  .ui.table:not(.unstackable) > tbody,
-  .ui.table:not(.unstackable) > tr,
-  .ui.table:not(.unstackable) > tbody > tr,
-  .ui.table:not(.unstackable) > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > thead > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > tbody > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > tfoot > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > tr > td:not(.rowspanned),
-  .ui.table:not(.unstackable) > tbody > tr > td:not(.rowspanned),
-  .ui.table:not(.unstackable) > tfoot > tr > td:not(.rowspanned) {
-    display: block !important;
-    width: auto !important;
-  }
-
-  .ui.table:not(.unstackable) > thead {
-    display: block;
-  }
-
-  .ui.table:not(.unstackable) > tfoot {
-    display: block;
-  }
-
-  .ui.ui.ui.ui.table:not(.unstackable) > tr,
-  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr,
-  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr,
-  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr {
-    padding-top: 1em;
-    padding-bottom: 1em;
-    box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.1) inset;
-  }
-
-  .ui.ui.ui.ui.table:not(.unstackable) > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > tr > td,
-  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > td,
-  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > td {
-    background: none;
-    border: none;
-    padding: 0.25em 0.75em;
-    box-shadow: none;
-  }
-
-  .ui.table:not(.unstackable) > tr > th:first-child,
-  .ui.table:not(.unstackable) > thead > tr > th:first-child,
-  .ui.table:not(.unstackable) > tbody > tr > th:first-child,
-  .ui.table:not(.unstackable) > tfoot > tr > th:first-child,
-  .ui.table:not(.unstackable) > tr > td:first-child,
-  .ui.table:not(.unstackable) > tbody > tr > td:first-child,
-  .ui.table:not(.unstackable) > tfoot > tr > td:first-child {
-    font-weight: 500;
-  }
-
-  /* Definition Table */
-
-  .ui.definition.table:not(.unstackable) > thead > tr > th:first-child {
-    box-shadow: none !important;
-  }
-}
-
-/*******************************
-            Coupling
-*******************************/
-
-/* UI Image */
-
-.ui.table .collapsing .image,
-.ui.table .collapsing .image img {
-  max-width: none;
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*--------------
-    Complex
----------------*/
-
-.ui.structured.table {
-  border-collapse: collapse;
-}
-
-.ui.structured.table > thead > tr > th {
-  border-left: none;
-  border-right: none;
-}
-
-.ui.structured.sortable.table > thead > tr > th {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-  border-right: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.structured.basic.table > tr > th,
-.ui.structured.basic.table > thead > tr > th,
-.ui.structured.basic.table > tbody > tr > th,
-.ui.structured.basic.table > tfoot > tr > th {
-  border-left: none;
-  border-right: none;
-}
-
-.ui.structured.celled.table > tr > th,
-.ui.structured.celled.table > thead > tr > th,
-.ui.structured.celled.table > tbody > tr > th,
-.ui.structured.celled.table > tfoot > tr > th,
-.ui.structured.celled.table > tr > td,
-.ui.structured.celled.table > tbody > tr > td,
-.ui.structured.celled.table > tfoot > tr > td {
-  border-left: 1px solid rgba(34, 36, 38, 0.1);
-  border-right: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-/*--------------
-     Definition
-  ---------------*/
-
-.ui.definition.table > thead:not(.full-width) > tr > th:first-child {
-  pointer-events: none;
-  background: #FFFFFF;
-  font-weight: normal;
-  color: rgba(0, 0, 0, 0.4);
-  box-shadow: -0.1em -0.2em 0 0.1em #FFFFFF;
-  -moz-transform: scale(1);
-}
-
-.ui.definition.table > tfoot:not(.full-width) > tr > th:first-child {
-  pointer-events: none;
-  background: #FFFFFF;
-  font-weight: normal;
-  color: rgba(0, 0, 0, 0.4);
-  box-shadow: -0.1em 0.2em 0 0.1em #FFFFFF;
-  -moz-transform: scale(1);
-}
-
-/* Highlight Defining Column */
-
-.ui.definition.table > tr > td:first-child:not(.ignored),
-.ui.definition.table > tbody > tr > td:first-child:not(.ignored),
-.ui.definition.table > tfoot > tr > td:first-child:not(.ignored),
-.ui.definition.table tr td.definition {
-  background: rgba(0, 0, 0, 0.03);
-  font-weight: 500;
-  color: rgba(0, 0, 0, 0.95);
-  text-transform: '';
-  box-shadow: '';
-  text-align: '';
-  font-size: 1em;
-  padding-left: '';
-  padding-right: '';
-}
-
-/* Fix 2nd Column */
-
-.ui.definition.table > thead:not(.full-width) > tr > th:nth-child(2) {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.definition.table > tfoot:not(.full-width) > tr > th:nth-child(2),
-.ui.definition.table > tfoot:not(.full-width) > tr > td:nth-child(2) {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.definition.table > tr > td:nth-child(2),
-.ui.definition.table > tbody > tr > td:nth-child(2) {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/*******************************
-             States
-*******************************/
-
-/*--------------
-      Positive
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.positive,
-.ui.ui.table td.positive {
-  box-shadow: 0 0 0 #A3C293 inset;
-  background: #FCFFF5;
-  color: #2C662D;
-}
-
-/*--------------
-       Negative
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.negative,
-.ui.ui.table td.negative {
-  box-shadow: 0 0 0 #E0B4B4 inset;
-  background: #FFF6F6;
-  color: #9F3A38;
-}
-
-/*--------------
-        Error
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.error,
-.ui.ui.table td.error {
-  box-shadow: 0 0 0 #E0B4B4 inset;
-  background: #FFF6F6;
-  color: #9F3A38;
-}
-
-/*--------------
-       Warning
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.warning,
-.ui.ui.table td.warning {
-  box-shadow: 0 0 0 #C9BA9B inset;
-  background: #FFFAF3;
-  color: #573A08;
-}
-
-/*--------------
-       Active
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.active,
-.ui.ui.table td.active {
-  box-shadow: 0 0 0 rgba(0, 0, 0, 0.87) inset;
-  background: #E0E0E0;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*--------------
-       Disabled
-  ---------------*/
-
-.ui.table tr.disabled td,
-.ui.table tr td.disabled,
-.ui.table tr.disabled:hover,
-.ui.table tr:hover td.disabled {
-  pointer-events: none;
-  color: rgba(40, 40, 40, 0.3);
-}
-
-/*******************************
-          Variations
-*******************************/
-
-/*--------------
-   Text Alignment
-  ---------------*/
-
-.ui.table[class*="left aligned"],
-.ui.table [class*="left aligned"] {
-  text-align: left;
-}
-
-.ui.table[class*="center aligned"],
-.ui.table [class*="center aligned"] {
-  text-align: center;
-}
-
-.ui.table[class*="right aligned"],
-.ui.table [class*="right aligned"] {
-  text-align: right;
-}
-
-/*------------------
-   Vertical Alignment
-  ------------------*/
-
-.ui.table[class*="top aligned"],
-.ui.table [class*="top aligned"] {
-  vertical-align: top;
-}
-
-.ui.table[class*="middle aligned"],
-.ui.table [class*="middle aligned"] {
-  vertical-align: middle;
-}
-
-.ui.table[class*="bottom aligned"],
-.ui.table [class*="bottom aligned"] {
-  vertical-align: bottom;
-}
-
-/*--------------
-      Collapsing
-  ---------------*/
-
-.ui.table th.collapsing,
-.ui.table td.collapsing {
-  width: 1px;
-  white-space: nowrap;
-}
-
-/*--------------
-       Fixed
-  ---------------*/
-
-.ui.fixed.table {
-  table-layout: fixed;
-}
-
-.ui.fixed.table th,
-.ui.fixed.table td {
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-/*--------------
-     Selectable
-  ---------------*/
-
-.ui.ui.selectable.table > tbody > tr:hover,
-.ui.table tbody tr td.selectable:hover {
-  background: rgba(0, 0, 0, 0.05);
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/* Selectable Cell Link */
-
-.ui.table tbody tr td.selectable {
-  padding: 0;
-}
-
-.ui.table tbody tr td.selectable > a:not(.ui) {
-  display: block;
-  color: inherit;
-  padding: 0.78571429em 0.78571429em;
-}
-
-.ui.table > tr > td.selectable,
-.ui.table > tbody > tr > td.selectable,
-.ui.selectable.table > tbody > tr,
-.ui.selectable.table > tr {
-  cursor: pointer;
-}
-
-/* Other States */
-
-.ui.ui.selectable.table tr.error:hover,
-.ui.table tr td.selectable.error:hover,
-.ui.selectable.table tr:hover td.error {
-  background: #ffe7e7;
-  color: #943634;
-}
-
-.ui.ui.selectable.table tr.warning:hover,
-.ui.table tr td.selectable.warning:hover,
-.ui.selectable.table tr:hover td.warning {
-  background: #fff4e4;
-  color: #493107;
-}
-
-.ui.ui.selectable.table tr.active:hover,
-.ui.table tr td.selectable.active:hover,
-.ui.selectable.table tr:hover td.active {
-  background: #E0E0E0;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.ui.selectable.table tr.positive:hover,
-.ui.table tr td.selectable.positive:hover,
-.ui.selectable.table tr:hover td.positive {
-  background: #f7ffe6;
-  color: #275b28;
-}
-
-.ui.ui.selectable.table tr.negative:hover,
-.ui.table tr td.selectable.negative:hover,
-.ui.selectable.table tr:hover td.negative {
-  background: #ffe7e7;
-  color: #943634;
-}
-
-/*-------------------
-        Attached
-  --------------------*/
-
-/* Middle */
-
-.ui.attached.table {
-  top: 0;
-  bottom: 0;
-  border-radius: 0;
-  margin: 0 -1px;
-  width: calc(100% + 2px);
-  max-width: calc(100% + 2px);
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-}
-
-.ui.attached + .ui.attached.table:not(.top) {
-  border-top: none;
-}
-
-/* Top */
-
-.ui[class*="top attached"].table {
-  bottom: 0;
-  margin-bottom: 0;
-  top: 0;
-  margin-top: 1em;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-.ui.table[class*="top attached"]:first-child {
-  margin-top: 0;
-}
-
-/* Bottom */
-
-.ui[class*="bottom attached"].table {
-  bottom: 0;
-  margin-top: 0;
-  top: 0;
-  margin-bottom: 1em;
-  box-shadow: none, none;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-.ui[class*="bottom attached"].table:last-child {
-  margin-bottom: 0;
-}
-
-/*--------------
-       Striped
-  ---------------*/
-
-/* Table Striping */
-
-.ui.striped.table > tr:nth-child(2n),
-.ui.striped.table > tbody > tr:nth-child(2n) {
-  background-color: rgba(0, 0, 50, 0.02);
-}
-
-/* Allow striped active hover */
-
-.ui.striped.selectable.selectable.selectable.table tbody tr.active:hover {
-  background: #EFEFEF;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-   Single Line
----------------*/
-
-.ui.table[class*="single line"],
-.ui.table [class*="single line"] {
-  white-space: nowrap;
-}
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.primary.table {
-  border-top: 0.2em solid #2185D0;
-}
-
-.ui.ui.ui.ui.table tr.primary:not(.marked),
-.ui.ui.table td.primary:not(.marked) {
-  background: #ddf4ff;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.ui.selectable.table tr.primary:not(.marked):hover,
-.ui.table tr td.selectable.primary:not(.marked):hover,
-.ui.selectable.table tr:hover td.primary:not(.marked) {
-  background: #d3f1ff;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.secondary.table {
-  border-top: 0.2em solid #1B1C1D;
-}
-
-.ui.ui.ui.ui.table tr.secondary:not(.marked),
-.ui.ui.table td.secondary:not(.marked) {
-  background: #dddddd;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.ui.selectable.table tr.secondary:not(.marked):hover,
-.ui.table tr td.selectable.secondary:not(.marked):hover,
-.ui.selectable.table tr:hover td.secondary:not(.marked) {
-  background: #e2e2e2;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.red.table {
-  border-top: 0.2em solid #DB2828;
-}
-
-.ui.ui.ui.ui.table tr.red:not(.marked),
-.ui.ui.table td.red:not(.marked) {
-  background: #ffe1df;
-  color: #DB2828;
-}
-
-.ui.ui.selectable.table tr.red:not(.marked):hover,
-.ui.table tr td.selectable.red:not(.marked):hover,
-.ui.selectable.table tr:hover td.red:not(.marked) {
-  background: #ffd7d5;
-  color: #DB2828;
-}
-
-.ui.orange.table {
-  border-top: 0.2em solid #F2711C;
-}
-
-.ui.ui.ui.ui.table tr.orange:not(.marked),
-.ui.ui.table td.orange:not(.marked) {
-  background: #ffe7d1;
-  color: #F2711C;
-}
-
-.ui.ui.selectable.table tr.orange:not(.marked):hover,
-.ui.table tr td.selectable.orange:not(.marked):hover,
-.ui.selectable.table tr:hover td.orange:not(.marked) {
-  background: #fae1cc;
-  color: #F2711C;
-}
-
-.ui.yellow.table {
-  border-top: 0.2em solid #FBBD08;
-}
-
-.ui.ui.ui.ui.table tr.yellow:not(.marked),
-.ui.ui.table td.yellow:not(.marked) {
-  background: #fff9d2;
-  color: #B58105;
-}
-
-.ui.ui.selectable.table tr.yellow:not(.marked):hover,
-.ui.table tr td.selectable.yellow:not(.marked):hover,
-.ui.selectable.table tr:hover td.yellow:not(.marked) {
-  background: #fbf5cc;
-  color: #B58105;
-}
-
-.ui.olive.table {
-  border-top: 0.2em solid #B5CC18;
-}
-
-.ui.ui.ui.ui.table tr.olive:not(.marked),
-.ui.ui.table td.olive:not(.marked) {
-  background: #f7fae4;
-  color: #8ABC1E;
-}
-
-.ui.ui.selectable.table tr.olive:not(.marked):hover,
-.ui.table tr td.selectable.olive:not(.marked):hover,
-.ui.selectable.table tr:hover td.olive:not(.marked) {
-  background: #f6fada;
-  color: #8ABC1E;
-}
-
-.ui.green.table {
-  border-top: 0.2em solid #21BA45;
-}
-
-.ui.ui.ui.ui.table tr.green:not(.marked),
-.ui.ui.table td.green:not(.marked) {
-  background: #d5f5d9;
-  color: #1EBC30;
-}
-
-.ui.ui.selectable.table tr.green:not(.marked):hover,
-.ui.table tr td.selectable.green:not(.marked):hover,
-.ui.selectable.table tr:hover td.green:not(.marked) {
-  background: #d2eed5;
-  color: #1EBC30;
-}
-
-.ui.teal.table {
-  border-top: 0.2em solid #00B5AD;
-}
-
-.ui.ui.ui.ui.table tr.teal:not(.marked),
-.ui.ui.table td.teal:not(.marked) {
-  background: #e2ffff;
-  color: #10A3A3;
-}
-
-.ui.ui.selectable.table tr.teal:not(.marked):hover,
-.ui.table tr td.selectable.teal:not(.marked):hover,
-.ui.selectable.table tr:hover td.teal:not(.marked) {
-  background: #d8ffff;
-  color: #10A3A3;
-}
-
-.ui.blue.table {
-  border-top: 0.2em solid #2185D0;
-}
-
-.ui.ui.ui.ui.table tr.blue:not(.marked),
-.ui.ui.table td.blue:not(.marked) {
-  background: #ddf4ff;
-  color: #2185D0;
-}
-
-.ui.ui.selectable.table tr.blue:not(.marked):hover,
-.ui.table tr td.selectable.blue:not(.marked):hover,
-.ui.selectable.table tr:hover td.blue:not(.marked) {
-  background: #d3f1ff;
-  color: #2185D0;
-}
-
-.ui.violet.table {
-  border-top: 0.2em solid #6435C9;
-}
-
-.ui.ui.ui.ui.table tr.violet:not(.marked),
-.ui.ui.table td.violet:not(.marked) {
-  background: #ece9fe;
-  color: #6435C9;
-}
-
-.ui.ui.selectable.table tr.violet:not(.marked):hover,
-.ui.table tr td.selectable.violet:not(.marked):hover,
-.ui.selectable.table tr:hover td.violet:not(.marked) {
-  background: #e3deff;
-  color: #6435C9;
-}
-
-.ui.purple.table {
-  border-top: 0.2em solid #A333C8;
-}
-
-.ui.ui.ui.ui.table tr.purple:not(.marked),
-.ui.ui.table td.purple:not(.marked) {
-  background: #f8e3ff;
-  color: #A333C8;
-}
-
-.ui.ui.selectable.table tr.purple:not(.marked):hover,
-.ui.table tr td.selectable.purple:not(.marked):hover,
-.ui.selectable.table tr:hover td.purple:not(.marked) {
-  background: #f5d9ff;
-  color: #A333C8;
-}
-
-.ui.pink.table {
-  border-top: 0.2em solid #E03997;
-}
-
-.ui.ui.ui.ui.table tr.pink:not(.marked),
-.ui.ui.table td.pink:not(.marked) {
-  background: #ffe8f9;
-  color: #E03997;
-}
-
-.ui.ui.selectable.table tr.pink:not(.marked):hover,
-.ui.table tr td.selectable.pink:not(.marked):hover,
-.ui.selectable.table tr:hover td.pink:not(.marked) {
-  background: #ffdef6;
-  color: #E03997;
-}
-
-.ui.brown.table {
-  border-top: 0.2em solid #A5673F;
-}
-
-.ui.ui.ui.ui.table tr.brown:not(.marked),
-.ui.ui.table td.brown:not(.marked) {
-  background: #f7e5d2;
-  color: #A5673F;
-}
-
-.ui.ui.selectable.table tr.brown:not(.marked):hover,
-.ui.table tr td.selectable.brown:not(.marked):hover,
-.ui.selectable.table tr:hover td.brown:not(.marked) {
-  background: #efe0cf;
-  color: #A5673F;
-}
-
-.ui.grey.table {
-  border-top: 0.2em solid #767676;
-}
-
-.ui.ui.ui.ui.table tr.grey:not(.marked),
-.ui.ui.table td.grey:not(.marked) {
-  background: #DCDDDE;
-  color: #767676;
-}
-
-.ui.ui.selectable.table tr.grey:not(.marked):hover,
-.ui.table tr td.selectable.grey:not(.marked):hover,
-.ui.selectable.table tr:hover td.grey:not(.marked) {
-  background: #c2c4c5;
-  color: #767676;
-}
-
-.ui.black.table {
-  border-top: 0.2em solid #1B1C1D;
-}
-
-.ui.ui.ui.ui.table tr.black:not(.marked),
-.ui.ui.table td.black:not(.marked) {
-  background: #545454;
-  color: #FFFFFF;
-}
-
-.ui.ui.selectable.table tr.black:not(.marked):hover,
-.ui.table tr td.selectable.black:not(.marked):hover,
-.ui.selectable.table tr:hover td.black:not(.marked) {
-  background: #000000;
-  color: #FFFFFF;
-}
-
-/*--------------
-  Column Count
----------------*/
-
-/* Grid Based */
-
-.ui.one.column.table td {
-  width: 100%;
-}
-
-.ui.two.column.table td {
-  width: 50%;
-}
-
-.ui.three.column.table td {
-  width: 33.33333333%;
-}
-
-.ui.four.column.table td {
-  width: 25%;
-}
-
-.ui.five.column.table td {
-  width: 20%;
-}
-
-.ui.six.column.table td {
-  width: 16.66666667%;
-}
-
-.ui.seven.column.table td {
-  width: 14.28571429%;
-}
-
-.ui.eight.column.table td {
-  width: 12.5%;
-}
-
-.ui.nine.column.table td {
-  width: 11.11111111%;
-}
-
-.ui.ten.column.table td {
-  width: 10%;
-}
-
-.ui.eleven.column.table td {
-  width: 9.09090909%;
-}
-
-.ui.twelve.column.table td {
-  width: 8.33333333%;
-}
-
-.ui.thirteen.column.table td {
-  width: 7.69230769%;
-}
-
-.ui.fourteen.column.table td {
-  width: 7.14285714%;
-}
-
-.ui.fifteen.column.table td {
-  width: 6.66666667%;
-}
-
-.ui.sixteen.column.table td {
-  width: 6.25%;
-}
-
-/* Column Width */
-
-.ui.table th.one.wide,
-.ui.table td.one.wide {
-  width: 6.25%;
-}
-
-.ui.table th.two.wide,
-.ui.table td.two.wide {
-  width: 12.5%;
-}
-
-.ui.table th.three.wide,
-.ui.table td.three.wide {
-  width: 18.75%;
-}
-
-.ui.table th.four.wide,
-.ui.table td.four.wide {
-  width: 25%;
-}
-
-.ui.table th.five.wide,
-.ui.table td.five.wide {
-  width: 31.25%;
-}
-
-.ui.table th.six.wide,
-.ui.table td.six.wide {
-  width: 37.5%;
-}
-
-.ui.table th.seven.wide,
-.ui.table td.seven.wide {
-  width: 43.75%;
-}
-
-.ui.table th.eight.wide,
-.ui.table td.eight.wide {
-  width: 50%;
-}
-
-.ui.table th.nine.wide,
-.ui.table td.nine.wide {
-  width: 56.25%;
-}
-
-.ui.table th.ten.wide,
-.ui.table td.ten.wide {
-  width: 62.5%;
-}
-
-.ui.table th.eleven.wide,
-.ui.table td.eleven.wide {
-  width: 68.75%;
-}
-
-.ui.table th.twelve.wide,
-.ui.table td.twelve.wide {
-  width: 75%;
-}
-
-.ui.table th.thirteen.wide,
-.ui.table td.thirteen.wide {
-  width: 81.25%;
-}
-
-.ui.table th.fourteen.wide,
-.ui.table td.fourteen.wide {
-  width: 87.5%;
-}
-
-.ui.table th.fifteen.wide,
-.ui.table td.fifteen.wide {
-  width: 93.75%;
-}
-
-.ui.table th.sixteen.wide,
-.ui.table td.sixteen.wide {
-  width: 100%;
-}
-
-/*--------------
-      Sortable
-  ---------------*/
-
-.ui.sortable.table > thead > tr > th {
-  cursor: pointer;
-  white-space: nowrap;
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.sortable.table > thead > tr > th:first-child {
-  border-left: none;
-}
-
-.ui.sortable.table thead th.sorted,
-.ui.sortable.table thead th.sorted:hover {
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-}
-
-.ui.sortable.table > thead > tr > th:after {
-  display: none;
-  font-style: normal;
-  font-weight: normal;
-  text-decoration: inherit;
-  content: '';
-  height: 1em;
-  width: auto;
-  opacity: 0.8;
-  margin: 0 0 0 0.5em;
-  font-family: 'Icons';
-}
-
-.ui.sortable.table thead th.ascending:after {
-  content: '\f0d8';
-}
-
-.ui.sortable.table thead th.descending:after {
-  content: '\f0d7';
-}
-
-/* Hover */
-
-.ui.sortable.table th.disabled:hover {
-  cursor: auto;
-  color: rgba(40, 40, 40, 0.3);
-}
-
-.ui.sortable.table > thead > tr > th:hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.sortable.table:not(.basic) > thead > tr > th:hover {
-  background: rgba(0, 0, 0, 0.05);
-}
-
-/* Sorted */
-
-.ui.sortable.table thead th.sorted {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.sortable.table:not(.basic) thead th.sorted {
-  background: rgba(0, 0, 0, 0.05);
-}
-
-.ui.sortable.table thead th.sorted:after {
-  display: inline-block;
-}
-
-/* Sorted Hover */
-
-.ui.sortable.table thead th.sorted:hover {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.sortable.table:not(.basic) thead th.sorted:hover {
-  background: rgba(0, 0, 0, 0.05);
-}
-
-/*--------------
-     Collapsing
-  ---------------*/
-
-.ui.collapsing.table {
-  width: auto;
-}
-
-/*--------------
-        Basic
-  ---------------*/
-
-.ui.basic.table {
-  background: transparent;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: none;
-}
-
-.ui.basic.table > thead,
-.ui.basic.table > tfoot {
-  box-shadow: none;
-}
-
-.ui.basic.table > thead > tr > th,
-.ui.basic.table > tbody > tr > th,
-.ui.basic.table > tfoot > tr > th,
-.ui.basic.table > tr > th {
-  background: transparent;
-  border-left: none;
-}
-
-.ui.basic.table > tbody > tr {
-  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
-}
-
-.ui.basic.table > tbody > tr > td,
-.ui.basic.table > tfoot > tr > td,
-.ui.basic.table > tr > td {
-  background: transparent;
-}
-
-.ui.basic.striped.table > tbody > tr:nth-child(2n) {
-  background-color: rgba(0, 0, 0, 0.05);
-}
-
-/* Very Basic */
-
-.ui[class*="very basic"].table {
-  border: none;
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td {
-  padding: '';
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > td:first-child {
-  padding-left: 0;
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > td:last-child {
-  padding-right: 0;
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr:first-child > th {
-  padding-top: 0;
-}
-
-/*--------------
-       Celled
-  ---------------*/
-
-.ui.celled.table > tr > th,
-.ui.celled.table > thead > tr > th,
-.ui.celled.table > tbody > tr > th,
-.ui.celled.table > tfoot > tr > th,
-.ui.celled.table > tr > td,
-.ui.celled.table > tbody > tr > td,
-.ui.celled.table > tfoot > tr > td {
-  border-left: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-.ui.celled.table > tr > th:first-child,
-.ui.celled.table > thead > tr > th:first-child,
-.ui.celled.table > tbody > tr > th:first-child,
-.ui.celled.table > tfoot > tr > th:first-child,
-.ui.celled.table > tr > td:first-child,
-.ui.celled.table > tbody > tr > td:first-child,
-.ui.celled.table > tfoot > tr > td:first-child {
-  border-left: none;
-}
-
-/*--------------
-       Padded
-  ---------------*/
-
-.ui.padded.table > tr > th,
-.ui.padded.table > thead > tr > th,
-.ui.padded.table > tbody > tr > th,
-.ui.padded.table > tfoot > tr > th {
-  padding-left: 1em;
-  padding-right: 1em;
-}
-
-.ui.padded.table > tr > th,
-.ui.padded.table > thead > tr > th,
-.ui.padded.table > tbody > tr > th,
-.ui.padded.table > tfoot > tr > th,
-.ui.padded.table > tr > td,
-.ui.padded.table > tbody > tr > td,
-.ui.padded.table > tfoot > tr > td {
-  padding: 1em 1em;
-}
-
-/* Very */
-
-.ui[class*="very padded"].table > tr > th,
-.ui[class*="very padded"].table > thead > tr > th,
-.ui[class*="very padded"].table > tbody > tr > th,
-.ui[class*="very padded"].table > tfoot > tr > th {
-  padding-left: 1.5em;
-  padding-right: 1.5em;
-}
-
-.ui[class*="very padded"].table > tr > td,
-.ui[class*="very padded"].table > tbody > tr > td,
-.ui[class*="very padded"].table > tfoot > tr > td {
-  padding: 1.5em 1.5em;
-}
-
-/*--------------
-       Compact
-  ---------------*/
-
-.ui.compact.table > tr > th,
-.ui.compact.table > thead > tr > th,
-.ui.compact.table > tbody > tr > th,
-.ui.compact.table > tfoot > tr > th {
-  padding-left: 0.7em;
-  padding-right: 0.7em;
-}
-
-.ui.compact.table > tr > td,
-.ui.compact.table > tbody > tr > td,
-.ui.compact.table > tfoot > tr > td {
-  padding: 0.5em 0.7em;
-}
-
-/* Very */
-
-.ui[class*="very compact"].table > tr > th,
-.ui[class*="very compact"].table > thead > tr > th,
-.ui[class*="very compact"].table > tbody > tr > th,
-.ui[class*="very compact"].table > tfoot > tr > th {
-  padding-left: 0.6em;
-  padding-right: 0.6em;
-}
-
-.ui[class*="very compact"].table > tr > td,
-.ui[class*="very compact"].table > tbody > tr > td,
-.ui[class*="very compact"].table > tfoot > tr > td {
-  padding: 0.4em 0.6em;
-}
-
-/*--------------
-      Sizes
----------------*/
-
-/* Standard */
-
-.ui.table {
-  font-size: 1em;
-}
-
-.ui.mini.table {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.table {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.table {
-  font-size: 0.9em;
-}
-
-.ui.large.table {
-  font-size: 1.1em;
-}
-
-.ui.big.table {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.table {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.table {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Site Overrides
 *******************************/
\ No newline at end of file
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 3c9a87c9d7..6fbb0e7b97 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -33,7 +33,6 @@
     "menu",
     "modal",
     "search",
-    "tab",
-    "table"
+    "tab"
   ]
 }

From 8fe26fb314f1710139728d9118b455fc6a16cce2 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 25 Mar 2024 19:37:55 +0100
Subject: [PATCH 519/679] Refactor all `.length === 0` patterns in JS (#30045)

This pattern comes of often during review, so let's fix it once and for
all. Did not test, but changes are trivial enough imho.
---
 web_src/js/components/DiffCommitSelector.vue    |  4 ++--
 web_src/js/components/RepoActionView.vue        |  2 +-
 web_src/js/components/RepoBranchTagSelector.vue |  8 +++++---
 web_src/js/features/admin/common.js             |  4 +---
 web_src/js/features/common-global.js            |  2 +-
 web_src/js/features/comp/SearchUserBox.js       |  2 +-
 web_src/js/features/repo-diff.js                |  3 +--
 web_src/js/features/repo-editor.js              | 10 ++++------
 web_src/js/features/repo-findfile.js            |  2 +-
 web_src/js/features/repo-home.js                |  4 ++--
 web_src/js/features/repo-issue.js               | 14 ++++++--------
 web_src/js/features/repo-legacy.js              | 12 ++++--------
 web_src/js/features/repo-settings.js            |  2 +-
 web_src/js/features/user-settings.js            |  2 +-
 14 files changed, 31 insertions(+), 40 deletions(-)

diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index d58337e093..cbb1f20873 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -103,7 +103,7 @@ export default {
       this.menuVisible = !this.menuVisible;
       // load our commits when the menu is not yet visible (it'll be toggled after loading)
       // and we got no commits
-      if (this.commits.length === 0 && this.menuVisible && !this.isLoading) {
+      if (!this.commits.length && this.menuVisible && !this.isLoading) {
         this.isLoading = true;
         try {
           await this.fetchCommits();
@@ -216,7 +216,7 @@ export default {
       <div
         v-if="lastReviewCommitSha != null" role="menuitem"
         class="vertical item"
-        :class="{disabled: commitsSinceLastReview === 0}"
+        :class="{disabled: !commitsSinceLastReview}"
         @keydown.enter="changesSinceLastReviewClick()"
         @click="changesSinceLastReviewClick()"
       >
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 26dffcda9e..7a2dc02d08 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -453,7 +453,7 @@ export function initRepositoryActionView() {
                   {{ locale.showFullScreen }}
                 </a>
                 <div class="divider"/>
-                <a :class="['item', currentJob.steps.length === 0 ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank">
+                <a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank">
                   <i class="icon"><SvgIcon name="octicon-download"/></i>
                   {{ locale.downloadLogs }}
                 </a>
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index d297503b2e..4e977ab185 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -19,17 +19,19 @@ const sfc = {
       });
 
       // TODO: fix this anti-pattern: side-effects-in-computed-properties
-      this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1);
+      this.active = !items.length && this.showCreateNewBranch ? 0 : -1;
       return items;
     },
     showNoResults() {
-      return this.filteredItems.length === 0 && !this.showCreateNewBranch;
+      return !this.filteredItems.length && !this.showCreateNewBranch;
     },
     showCreateNewBranch() {
       if (this.disableCreateBranch || !this.searchTerm) {
         return false;
       }
-      return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0;
+      return !this.items.filter((item) => {
+        return item.name.toLowerCase() === this.searchTerm.toLowerCase();
+      }).length;
     },
     formActionUrl() {
       return `${this.repoLink}/branches/_new/${this.branchNameSubURL}`;
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 3c485d67a6..4e64bff330 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -6,9 +6,7 @@ import {POST} from '../../modules/fetch.js';
 const {appSubUrl} = window.config;
 
 export function initAdminCommon() {
-  if ($('.page-content.admin').length === 0) {
-    return;
-  }
+  if (!$('.page-content.admin').length) return;
 
   // check whether appUrl(ROOT_URL) is correct, if not, show an error message
   checkAppUrl();
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index e2ce01eb49..18849ba7c1 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -19,7 +19,7 @@ const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
 export function initGlobalFormDirtyLeaveConfirm() {
   // Warn users that try to leave a page after entering data into a form.
   // Except on sign-in pages, and for forms marked as 'ignore-dirty'.
-  if ($('.user.signin').length === 0) {
+  if (!$('.user.signin').length) {
     $('form:not(.ignore-dirty)').areYouSure();
   }
 }
diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js
index 83d7044f11..081c47425f 100644
--- a/web_src/js/features/comp/SearchUserBox.js
+++ b/web_src/js/features/comp/SearchUserBox.js
@@ -34,7 +34,7 @@ export function initCompSearchUserBox() {
           }
         });
 
-        if (allowEmailInput && items.length === 0 && looksLikeEmailAddressCheck.test(searchQuery)) {
+        if (allowEmailInput && !items.length && looksLikeEmailAddressCheck.test(searchQuery)) {
           const resultItem = {
             title: searchQuery,
             description: allowEmailDescription,
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 4c8b411c64..596b1ea380 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -212,8 +212,7 @@ function initRepoDiffShowMore() {
 
 export function initRepoDiffView() {
   initRepoDiffConversationForm();
-  const $diffFileList = $('#diff-file-list');
-  if ($diffFileList.length === 0) return;
+  if (!$('#diff-file-list').length) return;
   initDiffFileTree();
   initDiffCommitSelect();
   initRepoDiffShowMore();
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index 1ab0a57865..da3bda8c1d 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -39,11 +39,9 @@ function initEditPreviewTab($form) {
 }
 
 function initEditorForm() {
-  if ($('.repository .edit.form').length === 0) {
-    return;
-  }
-
-  initEditPreviewTab($('.repository .edit.form'));
+  const $form = $('.repository .edit.form');
+  if (!$form) return;
+  initEditPreviewTab($form);
 }
 
 function getCursorPosition($e) {
@@ -165,7 +163,7 @@ export function initRepoEditor() {
 
     commitButton?.addEventListener('click', (e) => {
       // A modal which asks if an empty file should be committed
-      if ($editArea.val().length === 0) {
+      if (!$editArea.val()) {
         $('#edit-empty-content-modal').modal({
           onApprove() {
             $('.edit.form').trigger('submit');
diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js
index 0411b51f9c..cff5068a1e 100644
--- a/web_src/js/features/repo-findfile.js
+++ b/web_src/js/features/repo-findfile.js
@@ -77,7 +77,7 @@ function filterRepoFiles(filter) {
 
   const filterResult = filterRepoFilesWeighted(files, filter);
 
-  toggleElem(repoFindFileNoResult, filterResult.length === 0);
+  toggleElem(repoFindFileNoResult, !filterResult.length);
   for (const r of filterResult) {
     const row = document.createElement('tr');
     const cell = document.createElement('td');
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index 2b0e38f087..e195c23c37 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -153,11 +153,11 @@ export function initRepoTopicBar() {
 
   $.fn.form.settings.rules.validateTopic = function (_values, regExp) {
     const $topics = $topicDropdown.children('a.ui.label');
-    const status = $topics.length === 0 || $topics.last()[0].getAttribute('data-value').match(regExp);
+    const status = !$topics.length || $topics.last()[0].getAttribute('data-value').match(regExp);
     if (!status) {
       $topics.last().removeClass('green').addClass('red');
     }
-    return status && $topicDropdown.children('a.ui.label.red').length === 0;
+    return status && !$topicDropdown.children('a.ui.label.red').length;
   };
 
   $topicForm.form({
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 492428b327..20a854fb47 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -362,7 +362,7 @@ export async function updateIssuesMeta(url, action, issue_ids, id) {
 }
 
 export function initRepoIssueComments() {
-  if ($('.repository.view.issue .timeline').length === 0) return;
+  if (!$('.repository.view.issue .timeline').length) return;
 
   $('.re-request-review').on('click', async function (e) {
     e.preventDefault();
@@ -377,7 +377,7 @@ export function initRepoIssueComments() {
 
   $(document).on('click', (event) => {
     const $urlTarget = $(':target');
-    if ($urlTarget.length === 0) return;
+    if (!$urlTarget.length) return;
 
     const urlTargetId = $urlTarget.attr('id');
     if (!urlTargetId) return;
@@ -385,7 +385,7 @@ export function initRepoIssueComments() {
 
     const $target = $(event.target);
 
-    if ($target.closest(`#${urlTargetId}`).length === 0) {
+    if (!$target.closest(`#${urlTargetId}`).length) {
       const scrollPosition = $(window).scrollTop();
       window.location.hash = '';
       $(window).scrollTop(scrollPosition);
@@ -478,9 +478,7 @@ export function initRepoPullRequestReview() {
   }
 
   // The following part is only for diff views
-  if ($('.repository.pull.diff').length === 0) {
-    return;
-  }
+  if (!$('.repository.pull.diff').length) return;
 
   const $reviewBtn = $('.js-btn-review');
   const $panel = $reviewBtn.parent().find('.review-box-panel');
@@ -529,7 +527,7 @@ export function initRepoPullRequestReview() {
 
     const $td = $ntr.find(`.add-comment-${side}`);
     const $commentCloud = $td.find('.comment-code-cloud');
-    if ($commentCloud.length === 0 && !$ntr.find('button[name="pending_review"]').length) {
+    if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
       try {
         const response = await GET($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
         const html = await response.text();
@@ -626,7 +624,7 @@ export function initRepoIssueTitleEdit() {
     };
 
     const pullrequest_target_update_url = $(this).attr('data-target-update-url');
-    if ($editInput.val().length === 0 || $editInput.val() === $issueTitle.text()) {
+    if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
       $editInput.val($issueTitle.text());
       await pullrequest_targetbranch_change(pullrequest_target_update_url);
     } else {
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 838c131623..e96afe484e 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -50,9 +50,7 @@ function reloadConfirmDraftComment() {
 
 export function initRepoCommentForm() {
   const $commentForm = $('.comment.form');
-  if ($commentForm.length === 0) {
-    return;
-  }
+  if (!$commentForm.length) return;
 
   if ($commentForm.find('.field.combo-editor-dropzone').length) {
     // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form
@@ -202,7 +200,7 @@ export function initRepoCommentForm() {
           $($(this).data('id-selector')).addClass('tw-hidden');
         }
       });
-      if (listIds.length === 0) {
+      if (!listIds.length) {
         $noSelect.removeClass('tw-hidden');
       } else {
         $noSelect.addClass('tw-hidden');
@@ -329,7 +327,7 @@ async function onEditContent(event) {
   let comboMarkdownEditor;
 
   const setupDropzone = async ($dropzone) => {
-    if ($dropzone.length === 0) return null;
+    if (!$dropzone.length) return null;
 
     let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
     let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
@@ -482,9 +480,7 @@ async function onEditContent(event) {
 }
 
 export function initRepository() {
-  if ($('.page-content.repository').length === 0) {
-    return;
-  }
+  if (!$('.page-content.repository').length) return;
 
   initRepoBranchTagSelector('.js-branch-tag-selector');
 
diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index dc1db8ab29..0ea44130d0 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -71,7 +71,7 @@ export function initRepoSettingSearchTeamBox() {
 }
 
 export function initRepoSettingGitHook() {
-  if ($('.edit.githook').length === 0) return;
+  if (!$('.edit.githook').length) return;
   const filename = document.querySelector('.hook-filename').textContent;
   const _promise = createMonaco($('#content')[0], filename, {language: 'shell'});
 }
diff --git a/web_src/js/features/user-settings.js b/web_src/js/features/user-settings.js
index 0dd908f34a..2d8c53e457 100644
--- a/web_src/js/features/user-settings.js
+++ b/web_src/js/features/user-settings.js
@@ -1,7 +1,7 @@
 import {hideElem, showElem} from '../utils/dom.js';
 
 export function initUserSettings() {
-  if (document.querySelectorAll('.user.settings.profile').length === 0) return;
+  if (!document.querySelectorAll('.user.settings.profile').length) return;
 
   const usernameInput = document.getElementById('username');
   if (!usernameInput) return;

From 13921569dd5f77ee7d8352d0036ff649b03e72c8 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 26 Mar 2024 05:18:58 +0900
Subject: [PATCH 520/679] Add muted class to author name in repo commit list
 (#29989)

Before:

![image](https://github.com/go-gitea/gitea/assets/18380374/f6b3728c-ed9a-4e47-8755-89373235dff2)

After:

![image](https://github.com/go-gitea/gitea/assets/18380374/272c85e3-620d-4758-ae4d-ad90b54e142c)

If repo is a mirror, external user's name will be white, but if user is
existed, then you will see blue names and white names together:

![image](https://github.com/go-gitea/gitea/assets/18380374/747622da-56e3-4162-b391-919787a8cee4)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 templates/repo/commits_list.tmpl | 8 ++++----
 web_src/css/repo.css             | 1 +
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index aa7ca88931..bae9924141 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -13,16 +13,16 @@
 				{{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}}
 				{{range .Commits}}
 					<tr>
-						<td class="author">
+						<td class="author tw-flex">
 							{{$userName := .Author.Name}}
 							{{if .User}}
 								{{if .User.FullName}}
 									{{$userName = .User.FullName}}
 								{{end}}
-								{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-1"}}<a href="{{.User.HomeLink}}">{{$userName}}</a>
+								{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
 							{{else}}
-								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-1"}}
-								{{$userName}}
+								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}}
+								<span class="author-wrapper">{{$userName}}</span>
 							{{end}}
 						</td>
 						<td class="sha">
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 2014dfc370..18f28dc4a6 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2438,6 +2438,7 @@ tbody.commit-list {
 
 .author-wrapper {
   max-width: 180px;
+  align-self: center;
 }
 
 /* in the commit list, messages can wrap so we can use inline */

From a9a5734185a1a2837cfae42de7b17cf41dbb218a Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 26 Mar 2024 01:03:12 +0200
Subject: [PATCH 521/679] Remove jQuery `.attr` from the code line range
 selection (#30077)

- Switched from jQuery `attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested the code line range selection and it works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-code.js | 42 +++++++++++++++-----------------
 1 file changed, 20 insertions(+), 22 deletions(-)

diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index befa090004..cb5afa8318 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -28,40 +28,38 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
   $linesEls.closest('tr').removeClass('active');
 
   // add hashchange to permalink
-  const $refInNewIssue = $('a.ref-in-new-issue');
-  const $copyPermalink = $('a.copy-line-permalink');
-  const $viewGitBlame = $('a.view_git_blame');
+  const refInNewIssue = document.querySelector('a.ref-in-new-issue');
+  const copyPermalink = document.querySelector('a.copy-line-permalink');
+  const viewGitBlame = document.querySelector('a.view_git_blame');
 
   const updateIssueHref = function (anchor) {
-    if (!$refInNewIssue.length) {
-      return;
-    }
-    const urlIssueNew = $refInNewIssue.attr('data-url-issue-new');
-    const urlParamBodyLink = $refInNewIssue.attr('data-url-param-body-link');
+    if (!refInNewIssue) return;
+    const urlIssueNew = refInNewIssue.getAttribute('data-url-issue-new');
+    const urlParamBodyLink = refInNewIssue.getAttribute('data-url-param-body-link');
     const issueContent = `${toAbsoluteUrl(urlParamBodyLink)}#${anchor}`; // the default content for issue body
-    $refInNewIssue.attr('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`);
+    refInNewIssue.setAttribute('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`);
   };
 
   const updateViewGitBlameFragment = function (anchor) {
-    if (!$viewGitBlame.length) return;
-    let href = $viewGitBlame.attr('href');
+    if (!viewGitBlame) return;
+    let href = viewGitBlame.getAttribute('href');
     href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`;
     if (anchor.length !== 0) {
       href = `${href}#${anchor}`;
     }
-    $viewGitBlame.attr('href', href);
+    viewGitBlame.setAttribute('href', href);
   };
 
-  const updateCopyPermalinkUrl = function(anchor) {
-    if (!$copyPermalink.length) return;
-    let link = $copyPermalink.attr('data-url');
+  const updateCopyPermalinkUrl = function (anchor) {
+    if (!copyPermalink) return;
+    let link = copyPermalink.getAttribute('data-url');
     link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`;
-    $copyPermalink.attr('data-url', link);
+    copyPermalink.setAttribute('data-url', link);
   };
 
   if ($selectionStartEls) {
-    let a = parseInt($selectionEndEl.attr('rel').slice(1));
-    let b = parseInt($selectionStartEls.attr('rel').slice(1));
+    let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1));
+    let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1));
     let c;
     if (a !== b) {
       if (a > b) {
@@ -85,11 +83,11 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
     }
   }
   $selectionEndEl.closest('tr').addClass('active');
-  changeHash(`#${$selectionEndEl.attr('rel')}`);
+  changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`);
 
-  updateIssueHref($selectionEndEl.attr('rel'));
-  updateViewGitBlameFragment($selectionEndEl.attr('rel'));
-  updateCopyPermalinkUrl($selectionEndEl.attr('rel'));
+  updateIssueHref($selectionEndEl[0].getAttribute('rel'));
+  updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel'));
+  updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel'));
 }
 
 function showLineButton() {

From bcb151c220c3fa6003810e436acdda9cc0501e58 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 26 Mar 2024 00:14:17 +0100
Subject: [PATCH 522/679] Enable eslint `space-before-function-paren` (#30078)

Anonymous are set to ignore as I [couldn't
decide](https://github.com/go-gitea/gitea/pull/30077#discussion_r1538117497).
No current violations.

Rule docs: https://eslint.style/rules/js/space-before-function-paren
---
 .eslintrc.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index ea14d27d4c..50b3ca05a0 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -167,7 +167,7 @@ rules:
   "@stylistic/js/semi-spacing": [2, {before: false, after: true}]
   "@stylistic/js/semi-style": [2, last]
   "@stylistic/js/space-before-blocks": [2, always]
-  "@stylistic/js/space-before-function-paren": [0]
+  "@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}]
   "@stylistic/js/space-in-parens": [2, never]
   "@stylistic/js/space-infix-ops": [2]
   "@stylistic/js/space-unary-ops": [2]

From 08aec2c20adae8e6f04cac08566a8decd818e5cd Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 26 Mar 2024 15:45:11 +0900
Subject: [PATCH 523/679] Fix panic for `fixBrokenRepoUnits16961` (#30068)

![image](https://github.com/go-gitea/gitea/assets/18380374/508b3ceb-f53d-4d3b-a781-97c1542af1cb)
---
 services/doctor/fix16961.go | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/services/doctor/fix16961.go b/services/doctor/fix16961.go
index d3f36d8d5c..50d9ac6621 100644
--- a/services/doctor/fix16961.go
+++ b/services/doctor/fix16961.go
@@ -216,6 +216,12 @@ func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed boo
 		return false, nil
 	}
 
+	var cfg any
+	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
+	if err == nil {
+		return false, nil
+	}
+
 	switch repoUnit.Type {
 	case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
 		cfg := &repo_model.UnitConfig{}

From dd75237c3492140140c7413d788bd961692048d8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 26 Mar 2024 07:50:04 +0100
Subject: [PATCH 524/679] Fix table header text-align (#30084)

Fix regression from https://github.com/go-gitea/gitea/pull/30047.
Apparently tables have certain user-agent styles that center inside
`<th>` etc. Restored the original fomantic rules for these.

Before:
<img width="1332" alt="Screenshot 2024-03-25 at 21 59 33"
src="https://github.com/go-gitea/gitea/assets/115237/e06a5509-b505-4752-9b6e-91d5ed49f61d">

After:
<img width="1330" alt="Screenshot 2024-03-25 at 21 59 40"
src="https://github.com/go-gitea/gitea/assets/115237/6444817f-dd61-4a1e-a8b3-959c2780148d">
---
 web_src/css/modules/table.css | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css
index 962a5f52a6..eabca31a17 100644
--- a/web_src/css/modules/table.css
+++ b/web_src/css/modules/table.css
@@ -23,11 +23,13 @@
 }
 .ui.table > thead,
 .ui.table > tbody {
+  text-align: inherit;
   vertical-align: inherit;
 }
 
 .ui.table > thead > tr > th {
   background: var(--color-box-header);
+  text-align: inherit;
   color: var(--color-text);
   padding: 6px 5px;
   vertical-align: inherit;
@@ -52,6 +54,7 @@
 .ui.table > tfoot > tr > td {
   border-top: 1px solid var(--color-secondary);
   background: var(--color-box-body);
+  text-align: inherit;
   color: var(--color-text);
   padding: 0.78571429em;
   vertical-align: inherit;
@@ -78,6 +81,7 @@
 .ui.table > tbody > tr > td {
   border-top: 1px solid var(--color-secondary-alpha-50);
   padding: 6px 5px;
+  text-align: inherit;
 }
 .ui.table > tr:first-child > td,
 .ui.table > tbody > tr:first-child > td {

From ecbc9cee2b69cd9707acb1e23ccbca048484c460 Mon Sep 17 00:00:00 2001
From: crazeteam <164632007+crazeteam@users.noreply.github.com>
Date: Tue, 26 Mar 2024 15:48:53 +0800
Subject: [PATCH 525/679] Remove repetitive words (#30091)

remove repetitive words

Signed-off-by: crazeteam <lilujing@outlook.com>
---
 routers/api/v1/repo/file.go | 2 +-
 routers/web/repo/issue.go   | 2 +-
 tests/e2e/e2e_test.go       | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 4895f7b1b3..156033f58a 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -145,7 +145,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
 		return
 	}
 
-	// OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
+	// OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
 	dataRc, err := blob.DataAsync()
 	if err != nil {
 		ctx.ServerError("DataAsync", err)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 930a71d35f..12233d0e17 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1601,7 +1601,7 @@ func ViewIssue(ctx *context.Context) {
 	}
 	marked[issue.PosterID] = issue.ShowRole
 
-	// Render comments and and fetch participants.
+	// Render comments and fetch participants.
 	participants[0] = issue.Poster
 
 	if err := issue.Comments.LoadAttachmentsByIssue(ctx); err != nil {
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index df4fe95fdb..d15aa9a027 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -75,7 +75,7 @@ func TestMain(m *testing.M) {
 
 // TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.js" files in this directory and build a test for each.
 func TestE2e(t *testing.T) {
-	// Find the paths of all e2e test files in test test directory.
+	// Find the paths of all e2e test files in test directory.
 	searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js")
 	paths, err := filepath.Glob(searchGlob)
 	if err != nil {

From a4455d313e2c129dc9734292035b89339577174d Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 26 Mar 2024 08:56:44 +0100
Subject: [PATCH 526/679] Fix alignment in actions right view (#29979)

Fixes: https://github.com/go-gitea/gitea/issues/29974, Regression from
https://github.com/go-gitea/gitea/pull/29640.

Depending on the number of steps on the left side, the right side will
vertically expand. Collapse it with `align-self`.

<img width="1308" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/9bcede9c-d869-4f3f-8a10-026c74c03f71">
---
 web_src/js/components/RepoActionView.vue | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 7a2dc02d08..d56192526e 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -628,6 +628,8 @@ export function initRepositoryActionView() {
   flex-direction: column;
   border: 1px solid var(--color-console-border);
   border-radius: var(--border-radius);
+  background: var(--color-console-bg);
+  align-self: flex-start;
 }
 
 /* begin fomantic button overrides */
@@ -687,10 +689,8 @@ export function initRepositoryActionView() {
   justify-content: space-between;
   align-items: center;
   padding: 0 12px;
-  background-color: var(--color-console-bg);
   position: sticky;
   top: 0;
-  border-radius: var(--border-radius);
   height: 60px;
   z-index: 1;
 }
@@ -711,7 +711,6 @@ export function initRepositoryActionView() {
 }
 
 .job-step-container {
-  background-color: var(--color-console-bg);
   max-height: 100%;
   border-radius: 0 0 var(--border-radius) var(--border-radius);
   border-top: 1px solid var(--color-console-border);

From 9cf0f0bb040162509702ec9aaf7df6662ecc13b1 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 26 Mar 2024 17:24:13 +0900
Subject: [PATCH 527/679] Fix gitea doctor will remove repo-avatar files when
 execute command `storage-archives` (#30094)

Fix #30037
---
 services/doctor/storage.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/doctor/storage.go b/services/doctor/storage.go
index f338537864..787df27549 100644
--- a/services/doctor/storage.go
+++ b/services/doctor/storage.go
@@ -162,7 +162,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
 		if opts.RepoArchives || opts.All {
 			if err := commonCheckStorage(ctx, logger, autofix,
 				&commonStorageCheckOptions{
-					storer: storage.RepoAvatars,
+					storer: storage.RepoArchives,
 					isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
 						exists, err := repo.ExistsRepoArchiverWithStoragePath(ctx, path)
 						if err == nil || errors.Is(err, util.ErrInvalidArgument) {

From c1ac72150885b327f56ea61273e27b16d6da5435 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 26 Mar 2024 10:41:40 +0100
Subject: [PATCH 528/679] Update JS any PY dependencies, remove workarounds
 (#30085)

- Update dependencies via `make update-js update-py svg`
- Remove `postcss` workaround -
https://github.com/postcss/postcss/issues/1914
- Remove `happy-dom` workaround -
https://github.com/capricorn86/happy-dom/pull/1365.
- Tested Katex and Asciinema
---
 package-lock.json | 417 ++++++++++++++++++++++------------------------
 package.json      |  28 ++--
 poetry.lock       |   9 +-
 web_src/js/svg.js |   2 +-
 webpack.config.js |   1 -
 5 files changed, 220 insertions(+), 237 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index ef1164cac3..1c169c5636 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,10 +14,10 @@
         "@github/relative-time-element": "4.3.1",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-        "@primer/octicons": "19.8.0",
+        "@primer/octicons": "19.9.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
-        "asciinema-player": "3.7.0",
+        "asciinema-player": "3.7.1",
         "chart.js": "4.4.2",
         "chartjs-adapter-dayjs-4": "1.0.4",
         "chartjs-plugin-zoom": "2.0.1",
@@ -32,7 +32,7 @@
         "htmx.org": "1.9.11",
         "idiomorph": "0.3.0",
         "jquery": "3.7.1",
-        "katex": "0.16.9",
+        "katex": "0.16.10",
         "license-checker-webpack-plugin": "0.2.1",
         "mermaid": "10.9.0",
         "mini-css-extract-plugin": "2.8.1",
@@ -40,7 +40,7 @@
         "monaco-editor": "0.47.0",
         "monaco-editor-webpack-plugin": "7.1.0",
         "pdfobject": "2.3.0",
-        "postcss": "8.4.35",
+        "postcss": "8.4.38",
         "postcss-loader": "8.1.1",
         "postcss-nesting": "12.1.0",
         "pretty-ms": "9.0.0",
@@ -59,7 +59,7 @@
         "vue-chartjs": "5.3.0",
         "vue-loader": "17.4.2",
         "vue3-calendar-heatmap": "2.0.5",
-        "webpack": "5.90.3",
+        "webpack": "5.91.0",
         "webpack-cli": "5.1.4",
         "wrap-ansi": "9.0.0"
       },
@@ -77,24 +77,24 @@
         "eslint-plugin-jquery": "1.5.1",
         "eslint-plugin-no-jquery": "2.7.0",
         "eslint-plugin-no-use-extend-native": "0.5.0",
-        "eslint-plugin-regexp": "2.3.0",
+        "eslint-plugin-regexp": "2.4.0",
         "eslint-plugin-sonarjs": "0.24.0",
         "eslint-plugin-unicorn": "51.0.1",
-        "eslint-plugin-vitest": "0.3.26",
-        "eslint-plugin-vitest-globals": "1.4.0",
-        "eslint-plugin-vue": "9.23.0",
-        "eslint-plugin-vue-scoped-css": "2.7.2",
+        "eslint-plugin-vitest": "0.4.0",
+        "eslint-plugin-vitest-globals": "1.5.0",
+        "eslint-plugin-vue": "9.24.0",
+        "eslint-plugin-vue-scoped-css": "2.8.0",
         "eslint-plugin-wc": "2.0.4",
-        "happy-dom": "14.2.0",
+        "happy-dom": "14.3.7",
         "markdownlint-cli": "0.39.0",
         "postcss-html": "1.6.0",
-        "stylelint": "16.2.1",
+        "stylelint": "16.3.0",
         "stylelint-declaration-block-no-ignored-properties": "2.8.0",
         "stylelint-declaration-strict-value": "1.10.4",
         "svgo": "3.2.0",
-        "updates": "15.3.1",
+        "updates": "16.0.0",
         "vite-string-plugin": "1.1.5",
-        "vitest": "1.3.1"
+        "vitest": "1.4.0"
       },
       "engines": {
         "node": ">= 18.0.0"
@@ -516,6 +516,16 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@dual-bundle/import-meta-resolve": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz",
+      "integrity": "sha512-ZKXyJeFAzcpKM2kk8ipoGIPUqx9BX52omTGnfwjJvxOCaZTM2wtDK7zN0aIgPRbT9XYAlha0HtmZ+XKteuh0Gw==",
+      "dev": true,
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.20.2",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -1365,9 +1375,9 @@
       }
     },
     "node_modules/@primer/octicons": {
-      "version": "19.8.0",
-      "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.8.0.tgz",
-      "integrity": "sha512-Imze/fyW41Io5fN+27T5EAeXJrgBjMbz6nzU+wYbRylXvIAjLPUvaJPVoStiFlgSU+TjTUJqg5A9rgMDzTyMCg==",
+      "version": "19.9.0",
+      "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.9.0.tgz",
+      "integrity": "sha512-uAZa9cMgWkzbEsZnYWB7tg0vt7QprubD7ljtprz2fBJ8CjyqoxFRRsFvH4UiJdjK/3o87ODgDkhiflyJXDh+Lg==",
       "dependencies": {
         "object-assign": "^4.1.1"
       }
@@ -2238,16 +2248,16 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz",
-      "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz",
+      "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "7.3.1",
-        "@typescript-eslint/type-utils": "7.3.1",
-        "@typescript-eslint/utils": "7.3.1",
-        "@typescript-eslint/visitor-keys": "7.3.1",
+        "@typescript-eslint/scope-manager": "7.4.0",
+        "@typescript-eslint/type-utils": "7.4.0",
+        "@typescript-eslint/utils": "7.4.0",
+        "@typescript-eslint/visitor-keys": "7.4.0",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
@@ -2273,15 +2283,15 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz",
-      "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz",
+      "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.3.1",
-        "@typescript-eslint/types": "7.3.1",
-        "@typescript-eslint/typescript-estree": "7.3.1",
-        "@typescript-eslint/visitor-keys": "7.3.1",
+        "@typescript-eslint/scope-manager": "7.4.0",
+        "@typescript-eslint/types": "7.4.0",
+        "@typescript-eslint/typescript-estree": "7.4.0",
+        "@typescript-eslint/visitor-keys": "7.4.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2301,13 +2311,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz",
-      "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz",
+      "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.3.1",
-        "@typescript-eslint/visitor-keys": "7.3.1"
+        "@typescript-eslint/types": "7.4.0",
+        "@typescript-eslint/visitor-keys": "7.4.0"
       },
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -2318,13 +2328,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz",
-      "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz",
+      "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.3.1",
-        "@typescript-eslint/utils": "7.3.1",
+        "@typescript-eslint/typescript-estree": "7.4.0",
+        "@typescript-eslint/utils": "7.4.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2345,9 +2355,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz",
-      "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz",
+      "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==",
       "dev": true,
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -2358,13 +2368,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz",
-      "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz",
+      "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.3.1",
-        "@typescript-eslint/visitor-keys": "7.3.1",
+        "@typescript-eslint/types": "7.4.0",
+        "@typescript-eslint/visitor-keys": "7.4.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2386,17 +2396,17 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz",
-      "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz",
+      "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "7.3.1",
-        "@typescript-eslint/types": "7.3.1",
-        "@typescript-eslint/typescript-estree": "7.3.1",
+        "@typescript-eslint/scope-manager": "7.4.0",
+        "@typescript-eslint/types": "7.4.0",
+        "@typescript-eslint/typescript-estree": "7.4.0",
         "semver": "^7.5.4"
       },
       "engines": {
@@ -2411,12 +2421,12 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz",
-      "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz",
+      "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.3.1",
+        "@typescript-eslint/types": "7.4.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2447,13 +2457,13 @@
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
-      "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
+      "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
       "dev": true,
       "dependencies": {
-        "@vitest/spy": "1.3.1",
-        "@vitest/utils": "1.3.1",
+        "@vitest/spy": "1.4.0",
+        "@vitest/utils": "1.4.0",
         "chai": "^4.3.10"
       },
       "funding": {
@@ -2461,12 +2471,12 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
-      "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
+      "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
       "dev": true,
       "dependencies": {
-        "@vitest/utils": "1.3.1",
+        "@vitest/utils": "1.4.0",
         "p-limit": "^5.0.0",
         "pathe": "^1.1.1"
       },
@@ -2502,9 +2512,9 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
-      "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
+      "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
       "dev": true,
       "dependencies": {
         "magic-string": "^0.30.5",
@@ -2528,9 +2538,9 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
-      "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
+      "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
       "dev": true,
       "dependencies": {
         "tinyspy": "^2.2.0"
@@ -2540,9 +2550,9 @@
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
-      "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
+      "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
       "dev": true,
       "dependencies": {
         "diff-sequences": "^29.6.3",
@@ -3187,9 +3197,9 @@
       }
     },
     "node_modules/asciinema-player": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.7.0.tgz",
-      "integrity": "sha512-0RDc4j7TkjyhAwxkDe3vNqjAcizc7tubYW2VZi/06csY8iAoSC2uRvSyfNzh9ONDZu8pdf0bZJ91A84Gexb3tg==",
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.7.1.tgz",
+      "integrity": "sha512-zDJteGjBzNQhHEnD0aG7GqV3E53sOyKb1WCxKNRm2PquU70Lq3s4xxb91wyDS0hBJ3J/TB8aY3y8gjGPN+T23A==",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
         "solid-js": "^1.3.0"
@@ -3475,9 +3485,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001599",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
-      "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
+      "version": "1.0.30001600",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
+      "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -4721,9 +4731,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.0.10",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.10.tgz",
-      "integrity": "sha512-WZDL8ZHTliEVP3Lk4phtvjg8SNQ3YMc5WVstxE8cszKZrFjzI4PF4ZTIk9VGAc9vZADO7uGO2V/ZiStcRSAT4Q=="
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz",
+      "integrity": "sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg=="
     },
     "node_modules/domutils": {
       "version": "3.1.0",
@@ -4766,9 +4776,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.713",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.713.tgz",
-      "integrity": "sha512-vDarADhwntXiULEdmWd77g2dV6FrNGa8ecAC29MZ4TwPut2fvosD0/5sJd1qWNNe8HcJFAC+F5Lf9jW1NPtWmw=="
+      "version": "1.4.716",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz",
+      "integrity": "sha512-t/MXMzFKQC3UfMDpw7V5wdB/UAB8dWx4hEsy+fpPYJWW3gqh3u5T1uXp6vR+H6dGCPBxkRo+YBcapBLvbGQHRw=="
     },
     "node_modules/elkjs": {
       "version": "0.9.2",
@@ -4899,19 +4909,19 @@
       }
     },
     "node_modules/es-aggregate-error": {
-      "version": "1.0.12",
-      "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.12.tgz",
-      "integrity": "sha512-j0PupcmELoVbYS2NNrsn5zcLLEsryQwP02x8fRawh7c2eEaPHwJFAxltZsqV7HJjsF57+SMpYyVRWgbVLfOagg==",
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz",
+      "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.1.1",
+        "define-data-property": "^1.1.4",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.3",
-        "es-errors": "^1.1.0",
+        "es-abstract": "^1.23.2",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
         "globalthis": "^1.0.3",
-        "has-property-descriptors": "^1.0.1",
-        "set-function-name": "^2.0.1"
+        "has-property-descriptors": "^1.0.2",
+        "set-function-name": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -4967,9 +4977,9 @@
       }
     },
     "node_modules/es-module-lexer": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.2.tgz",
-      "integrity": "sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw=="
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz",
+      "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw=="
     },
     "node_modules/es-object-atoms": {
       "version": "1.0.0",
@@ -5164,9 +5174,9 @@
       }
     },
     "node_modules/eslint-compat-utils": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.4.1.tgz",
-      "integrity": "sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz",
+      "integrity": "sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==",
       "dev": true,
       "dependencies": {
         "semver": "^7.5.4"
@@ -5598,9 +5608,9 @@
       }
     },
     "node_modules/eslint-plugin-regexp": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.3.0.tgz",
-      "integrity": "sha512-T8JUs7ssRGbuXb+CGfdUJbcxTBMCNOpNqNBLuC8JUKAEIez72J37RaOi5/4dAUsGz92GbWVtqTLPSJZGyP/sQA==",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.4.0.tgz",
+      "integrity": "sha512-OL2S6VPjQhs9s/NclQ0qattVq1J0GU8ox70/HIVy5Dxw+qbbdd7KQkyucsez2clEQjvdtDe12DTnPphFFUyXFg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
@@ -5664,12 +5674,12 @@
       }
     },
     "node_modules/eslint-plugin-vitest": {
-      "version": "0.3.26",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.26.tgz",
-      "integrity": "sha512-oxe5JSPgRjco8caVLTh7Ti8PxpwJdhSV0hTQAmkFcNcmy/9DnqLB/oNVRA11RmVRP//2+jIIT6JuBEcpW3obYg==",
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.4.0.tgz",
+      "integrity": "sha512-3oWgZIwdWVBQ5plvkmOBjreIGLQRdYb7x54OP8uIRHeZyRVJIdOn9o/qWVb9292fDMC8jn7H7d9TSFBZqhrykQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/utils": "^7.1.1"
+        "@typescript-eslint/utils": "^7.2.0"
       },
       "engines": {
         "node": "^18.0.0 || >= 20.0.0"
@@ -5688,18 +5698,19 @@
       }
     },
     "node_modules/eslint-plugin-vitest-globals": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.4.0.tgz",
-      "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.5.0.tgz",
+      "integrity": "sha512-ZSsVOaOIig0oVLzRTyk8lUfBfqzWxr/J3/NFMfGGRIkGQPejJYmDH3gXmSJxAojts77uzAGB/UmVrwi2DC4LYA==",
       "dev": true
     },
     "node_modules/eslint-plugin-vue": {
-      "version": "9.23.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.23.0.tgz",
-      "integrity": "sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw==",
+      "version": "9.24.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz",
+      "integrity": "sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
+        "globals": "^13.24.0",
         "natural-compare": "^1.4.0",
         "nth-check": "^2.1.1",
         "postcss-selector-parser": "^6.0.15",
@@ -5715,13 +5726,13 @@
       }
     },
     "node_modules/eslint-plugin-vue-scoped-css": {
-      "version": "2.7.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.7.2.tgz",
-      "integrity": "sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==",
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.8.0.tgz",
+      "integrity": "sha512-JXb3Um4+AhuDGxSX6FAGCI0p811xF7W8L7yxC8wmAEZEI/teTjlpC09noqQZHXn53RZ/TGQJ8Onaq4teYLxBbg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "eslint-compat-utils": "^0.4.0",
+        "eslint-compat-utils": "^0.5.0",
         "lodash": "^4.17.21",
         "postcss": "^8.4.31",
         "postcss-safe-parser": "^6.0.0",
@@ -6502,9 +6513,9 @@
       }
     },
     "node_modules/happy-dom": {
-      "version": "14.2.0",
-      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.2.0.tgz",
-      "integrity": "sha512-vTqF/9MEkRKgYy5eKq9W0uiNmkgnVAmJhRwn8x8fQBR7lc4C84859jLhgZ1lR4Gi/t70oSdgvtLpxlHjgdJrAw==",
+      "version": "14.3.7",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.3.7.tgz",
+      "integrity": "sha512-lUfDRGzjrVJF2pnvh13OL+qEJ9eDpcedVLm77a3aMg8gPGKXfG+xFMNk3cOWetjucU8FveJ4qcSC/EX55nJ4fQ==",
       "dev": true,
       "dependencies": {
         "entities": "^4.5.0",
@@ -7539,9 +7550,9 @@
       "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ=="
     },
     "node_modules/katex": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz",
-      "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==",
+      "version": "0.16.10",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
+      "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
       "funding": [
         "https://opencollective.com/katex",
         "https://github.com/sponsors/katex"
@@ -7584,9 +7595,9 @@
       }
     },
     "node_modules/known-css-properties": {
-      "version": "0.29.0",
-      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz",
-      "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
+      "version": "0.30.0",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.30.0.tgz",
+      "integrity": "sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==",
       "dev": true
     },
     "node_modules/language-subtag-registry": {
@@ -9346,9 +9357,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.35",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
-      "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
+      "version": "8.4.38",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
       "funding": [
         {
           "type": "opencollective",
@@ -9366,7 +9377,7 @@
       "dependencies": {
         "nanoid": "^3.3.7",
         "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
+        "source-map-js": "^1.2.0"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -10676,14 +10687,17 @@
       }
     },
     "node_modules/string.prototype.trimstart": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
-      "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+      "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -10776,15 +10790,16 @@
       "dev": true
     },
     "node_modules/stylelint": {
-      "version": "16.2.1",
-      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.1.tgz",
-      "integrity": "sha512-SfIMGFK+4n7XVAyv50CpVfcGYWG4v41y6xG7PqOgQSY8M/PgdK0SQbjWFblxjJZlN9jNq879mB4BCZHJRIJ1hA==",
+      "version": "16.3.0",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.3.0.tgz",
+      "integrity": "sha512-hqC6xNTbQ5HRGQXfIW4HwXcx09raIFz4W4XFbraeqWqYRVVY/ibYvI0dsu0ORMQY8re2bpDdCAeIa2cm+QJ4Sw==",
       "dev": true,
       "dependencies": {
-        "@csstools/css-parser-algorithms": "^2.5.0",
-        "@csstools/css-tokenizer": "^2.2.3",
-        "@csstools/media-query-list-parser": "^2.1.7",
-        "@csstools/selector-specificity": "^3.0.1",
+        "@csstools/css-parser-algorithms": "^2.6.1",
+        "@csstools/css-tokenizer": "^2.2.4",
+        "@csstools/media-query-list-parser": "^2.1.9",
+        "@csstools/selector-specificity": "^3.0.2",
+        "@dual-bundle/import-meta-resolve": "^4.0.0",
         "balanced-match": "^2.0.0",
         "colord": "^2.9.3",
         "cosmiconfig": "^9.0.0",
@@ -10798,19 +10813,19 @@
         "globby": "^11.1.0",
         "globjoin": "^0.1.4",
         "html-tags": "^3.3.1",
-        "ignore": "^5.3.0",
+        "ignore": "^5.3.1",
         "imurmurhash": "^0.1.4",
         "is-plain-object": "^5.0.0",
-        "known-css-properties": "^0.29.0",
+        "known-css-properties": "^0.30.0",
         "mathml-tag-names": "^2.1.3",
-        "meow": "^13.1.0",
+        "meow": "^13.2.0",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
         "picocolors": "^1.0.0",
-        "postcss": "^8.4.33",
+        "postcss": "^8.4.38",
         "postcss-resolve-nested-selector": "^0.1.1",
         "postcss-safe-parser": "^7.0.0",
-        "postcss-selector-parser": "^6.0.15",
+        "postcss-selector-parser": "^6.0.16",
         "postcss-value-parser": "^4.2.0",
         "resolve-from": "^5.0.0",
         "string-width": "^4.2.3",
@@ -11418,9 +11433,9 @@
       "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
     },
     "node_modules/tinypool": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
-      "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.3.tgz",
+      "integrity": "sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
@@ -11611,9 +11626,9 @@
       }
     },
     "node_modules/typed-array-length": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz",
-      "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
+      "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.7",
@@ -11737,12 +11752,12 @@
       }
     },
     "node_modules/updates": {
-      "version": "15.3.1",
-      "resolved": "https://registry.npmjs.org/updates/-/updates-15.3.1.tgz",
-      "integrity": "sha512-DqHT1aJ6p6jVLWRiAeuVx/TQotvEwUjgrY1Mlc0a2qYk+eKEQVXugQ4M+6QoVMA3X1NFAVsb02d93pmWam4bBA==",
+      "version": "16.0.0",
+      "resolved": "https://registry.npmjs.org/updates/-/updates-16.0.0.tgz",
+      "integrity": "sha512-Ra3QUu/rfbSCsG83zNNvoRQt0FVT3qULBSALYTlwTDX6oyz7R5GQAYwqJoIG/RDjYAXpwr3usrInoyHHTP6cag==",
       "dev": true,
       "bin": {
-        "updates": "bin/updates.js"
+        "updates": "dist/updates.js"
       },
       "engines": {
         "node": ">=18"
@@ -11825,9 +11840,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.2.tgz",
-      "integrity": "sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==",
+      "version": "5.2.6",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
+      "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==",
       "dev": true,
       "dependencies": {
         "esbuild": "^0.20.1",
@@ -11880,9 +11895,9 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
-      "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
+      "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
       "dev": true,
       "dependencies": {
         "cac": "^6.7.14",
@@ -11927,34 +11942,6 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
-    "node_modules/vite/node_modules/postcss": {
-      "version": "8.4.38",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
-      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/postcss/"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/postcss"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "dependencies": {
-        "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.2.0"
-      },
-      "engines": {
-        "node": "^10 || ^12 || >=14"
-      }
-    },
     "node_modules/vite/node_modules/rollup": {
       "version": "4.13.0",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
@@ -11988,16 +11975,16 @@
       }
     },
     "node_modules/vitest": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
-      "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
+      "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
       "dev": true,
       "dependencies": {
-        "@vitest/expect": "1.3.1",
-        "@vitest/runner": "1.3.1",
-        "@vitest/snapshot": "1.3.1",
-        "@vitest/spy": "1.3.1",
-        "@vitest/utils": "1.3.1",
+        "@vitest/expect": "1.4.0",
+        "@vitest/runner": "1.4.0",
+        "@vitest/snapshot": "1.4.0",
+        "@vitest/spy": "1.4.0",
+        "@vitest/utils": "1.4.0",
         "acorn-walk": "^8.3.2",
         "chai": "^4.3.10",
         "debug": "^4.3.4",
@@ -12011,7 +11998,7 @@
         "tinybench": "^2.5.1",
         "tinypool": "^0.8.2",
         "vite": "^5.0.0",
-        "vite-node": "1.3.1",
+        "vite-node": "1.4.0",
         "why-is-node-running": "^2.2.2"
       },
       "bin": {
@@ -12026,8 +12013,8 @@
       "peerDependencies": {
         "@edge-runtime/vm": "*",
         "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "1.3.1",
-        "@vitest/ui": "1.3.1",
+        "@vitest/browser": "1.4.0",
+        "@vitest/ui": "1.4.0",
         "happy-dom": "*",
         "jsdom": "*"
       },
@@ -12186,25 +12173,25 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.90.3",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
-      "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
+      "version": "5.91.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
+      "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==",
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^1.0.5",
-        "@webassemblyjs/ast": "^1.11.5",
-        "@webassemblyjs/wasm-edit": "^1.11.5",
-        "@webassemblyjs/wasm-parser": "^1.11.5",
+        "@webassemblyjs/ast": "^1.12.1",
+        "@webassemblyjs/wasm-edit": "^1.12.1",
+        "@webassemblyjs/wasm-parser": "^1.12.1",
         "acorn": "^8.7.1",
         "acorn-import-assertions": "^1.9.0",
         "browserslist": "^4.21.10",
         "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.15.0",
+        "enhanced-resolve": "^5.16.0",
         "es-module-lexer": "^1.2.1",
         "eslint-scope": "5.1.1",
         "events": "^3.2.0",
         "glob-to-regexp": "^0.4.1",
-        "graceful-fs": "^4.2.9",
+        "graceful-fs": "^4.2.11",
         "json-parse-even-better-errors": "^2.3.1",
         "loader-runner": "^4.2.0",
         "mime-types": "^2.1.27",
@@ -12212,7 +12199,7 @@
         "schema-utils": "^3.2.0",
         "tapable": "^2.1.1",
         "terser-webpack-plugin": "^5.3.10",
-        "watchpack": "^2.4.0",
+        "watchpack": "^2.4.1",
         "webpack-sources": "^3.2.3"
       },
       "bin": {
diff --git a/package.json b/package.json
index ced10f9c99..1eeb4d196c 100644
--- a/package.json
+++ b/package.json
@@ -13,10 +13,10 @@
     "@github/relative-time-element": "4.3.1",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-    "@primer/octicons": "19.8.0",
+    "@primer/octicons": "19.9.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
-    "asciinema-player": "3.7.0",
+    "asciinema-player": "3.7.1",
     "chart.js": "4.4.2",
     "chartjs-adapter-dayjs-4": "1.0.4",
     "chartjs-plugin-zoom": "2.0.1",
@@ -31,7 +31,7 @@
     "htmx.org": "1.9.11",
     "idiomorph": "0.3.0",
     "jquery": "3.7.1",
-    "katex": "0.16.9",
+    "katex": "0.16.10",
     "license-checker-webpack-plugin": "0.2.1",
     "mermaid": "10.9.0",
     "mini-css-extract-plugin": "2.8.1",
@@ -39,7 +39,7 @@
     "monaco-editor": "0.47.0",
     "monaco-editor-webpack-plugin": "7.1.0",
     "pdfobject": "2.3.0",
-    "postcss": "8.4.35",
+    "postcss": "8.4.38",
     "postcss-loader": "8.1.1",
     "postcss-nesting": "12.1.0",
     "pretty-ms": "9.0.0",
@@ -58,7 +58,7 @@
     "vue-chartjs": "5.3.0",
     "vue-loader": "17.4.2",
     "vue3-calendar-heatmap": "2.0.5",
-    "webpack": "5.90.3",
+    "webpack": "5.91.0",
     "webpack-cli": "5.1.4",
     "wrap-ansi": "9.0.0"
   },
@@ -76,24 +76,24 @@
     "eslint-plugin-jquery": "1.5.1",
     "eslint-plugin-no-jquery": "2.7.0",
     "eslint-plugin-no-use-extend-native": "0.5.0",
-    "eslint-plugin-regexp": "2.3.0",
+    "eslint-plugin-regexp": "2.4.0",
     "eslint-plugin-sonarjs": "0.24.0",
     "eslint-plugin-unicorn": "51.0.1",
-    "eslint-plugin-vitest": "0.3.26",
-    "eslint-plugin-vitest-globals": "1.4.0",
-    "eslint-plugin-vue": "9.23.0",
-    "eslint-plugin-vue-scoped-css": "2.7.2",
+    "eslint-plugin-vitest": "0.4.0",
+    "eslint-plugin-vitest-globals": "1.5.0",
+    "eslint-plugin-vue": "9.24.0",
+    "eslint-plugin-vue-scoped-css": "2.8.0",
     "eslint-plugin-wc": "2.0.4",
-    "happy-dom": "14.2.0",
+    "happy-dom": "14.3.7",
     "markdownlint-cli": "0.39.0",
     "postcss-html": "1.6.0",
-    "stylelint": "16.2.1",
+    "stylelint": "16.3.0",
     "stylelint-declaration-block-no-ignored-properties": "2.8.0",
     "stylelint-declaration-strict-value": "1.10.4",
     "svgo": "3.2.0",
-    "updates": "15.3.1",
+    "updates": "16.0.0",
     "vite-string-plugin": "1.1.5",
-    "vitest": "1.3.1"
+    "vitest": "1.4.0"
   },
   "browserslist": [
     "defaults"
diff --git a/poetry.lock b/poetry.lock
index 46520fba3c..951a0fa7a8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -113,18 +113,15 @@ six = ">=1.13.0"
 
 [[package]]
 name = "json5"
-version = "0.9.18"
+version = "0.9.24"
 description = "A Python implementation of the JSON5 data format."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "json5-0.9.18-py2.py3-none-any.whl", hash = "sha256:3f20193ff8dfdec6ab114b344e7ac5d76fac453c8bab9bdfe1460d1d528ec393"},
-    {file = "json5-0.9.18.tar.gz", hash = "sha256:ecb8ac357004e3522fb989da1bf08b146011edbd14fdffae6caad3bd68493467"},
+    {file = "json5-0.9.24-py3-none-any.whl", hash = "sha256:4ca101fd5c7cb47960c055ef8f4d0e31e15a7c6c48c3b6f1473fc83b6c462a13"},
+    {file = "json5-0.9.24.tar.gz", hash = "sha256:0c638399421da959a20952782800e5c1a78c14e08e1dc9738fa10d8ec14d58c8"},
 ]
 
-[package.extras]
-dev = ["hypothesis"]
-
 [[package]]
 name = "pathspec"
 version = "0.12.1"
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 20babb331e..913d26779f 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -205,7 +205,7 @@ export const SvgIcon = {
 
     // make the <SvgIcon class="foo" class-name="bar"> classes work together
     const classes = [];
-    for (const cls of svgOuter.classList.values()) {
+    for (const cls of svgOuter.classList) {
       classes.push(cls);
     }
     // TODO: drop the `className/class-name` prop in the future, only use "class" prop
diff --git a/webpack.config.js b/webpack.config.js
index 00952f90b4..0b0e7403e8 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -171,7 +171,6 @@ export default {
             loader: 'postcss-loader',
             options: {
               postcssOptions: {
-                map: false, // https://github.com/postcss/postcss/issues/1914
                 plugins: [
                   tailwindcssNesting(postcssNesting({edition: '2024-02'})),
                   tailwindcss(tailwindConfig),

From 274bc00ca2c3bf5a734d74e47cc28fa8c41e3875 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 26 Mar 2024 19:20:26 +0900
Subject: [PATCH 529/679] Fix duplicate migrated milestones (#30102)

Fix #17567
---
 services/migrations/migrate.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index 0b83f3b4a3..5bb3056161 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -250,14 +250,13 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba
 			}
 			log.Warn("migrating milestones is not supported, ignored")
 		}
-
 		msBatchSize := uploader.MaxBatchInsertSize("milestone")
 		for len(milestones) > 0 {
 			if len(milestones) < msBatchSize {
 				msBatchSize = len(milestones)
 			}
 
-			if err := uploader.CreateMilestones(milestones...); err != nil {
+			if err := uploader.CreateMilestones(milestones[:msBatchSize]...); err != nil {
 				return err
 			}
 			milestones = milestones[msBatchSize:]

From 2ab5f05f40d93224f73e211e84de50a88a6ecf03 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 26 Mar 2024 12:19:15 +0100
Subject: [PATCH 530/679] Add svg linter and fix incorrect svgs (#30086)

Fixes https://github.com/go-gitea/gitea/issues/30082.

Adds a new linter that searches for non-existant SVG images in
templates. Output before the fix was:

```
$ make lint-templates
SVG "octicon-warning" not found, used in templates/devtest/flex-list.tmpl
SVG "octicon-warning" not found, used in templates/devtest/flex-list.tmpl
SVG "octicon-markup" not found, used in templates/repo/diff/comment_form.tmpl
make: *** [Makefile:438: lint-templates] Error 1
```

<img width="306" alt="Screenshot 2024-03-25 at 23 31 05"
src="https://github.com/go-gitea/gitea/assets/115237/1052d1a9-bfec-4d5a-9cae-f895f78f7c93">
---
 .github/workflows/files-changed.yml   |  1 +
 .github/workflows/pull-compliance.yml |  4 ++++
 Makefile                              |  3 ++-
 templates/devtest/flex-list.tmpl      |  4 ++--
 templates/repo/diff/comment_form.tmpl |  2 +-
 tools/lint-templates-svg.js           | 26 ++++++++++++++++++++++++++
 6 files changed, 36 insertions(+), 4 deletions(-)
 create mode 100755 tools/lint-templates-svg.js

diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index f9b6b1ec49..b8535cb42b 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -73,6 +73,7 @@ jobs:
               - "Makefile"
 
             templates:
+              - "tools/lint-templates-*.js"
               - "templates/**/*.tmpl"
               - "pyproject.toml"
               - "poetry.lock"
diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 02a265b1ff..99a69ab174 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -35,8 +35,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: "3.12"
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
       - run: pip install poetry
       - run: make deps-py
+      - run: make deps-frontend
       - run: make lint-templates
 
   lint-yaml:
diff --git a/Makefile b/Makefile
index 236f115a2f..a7e175e76b 100644
--- a/Makefile
+++ b/Makefile
@@ -434,7 +434,8 @@ lint-actions:
 	$(GO) run $(ACTIONLINT_PACKAGE)
 
 .PHONY: lint-templates
-lint-templates: .venv
+lint-templates: .venv node_modules
+	@node tools/lint-templates-svg.js
 	@poetry run djlint $(shell find templates -type f -iname '*.tmpl')
 
 .PHONY: lint-yaml
diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl
index d5678566d8..015ab1e154 100644
--- a/templates/devtest/flex-list.tmpl
+++ b/templates/devtest/flex-list.tmpl
@@ -25,7 +25,7 @@
 				</div>
 				<div class="flex-item-trailing">
 					<button class="ui tiny red button">
-						{{svg "octicon-warning" 14}} CJK文本测试
+						{{svg "octicon-alert" 14}} CJK文本测试
 					</button>
 					<button class="ui tiny primary button">
 						{{svg "octicon-info" 14}} Button
@@ -54,7 +54,7 @@
 				</div>
 				<div class="flex-item-trailing">
 					<button class="ui tiny red button">
-						{{svg "octicon-warning" 12}} CJK文本测试 <!-- single CJK text test, it shouldn't be horizontal -->
+						{{svg "octicon-alert" 12}} CJK文本测试 <!-- single CJK text test, it shouldn't be horizontal -->
 					</button>
 				</div>
 			</div>
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl
index 6a5dec6c48..856b3da01a 100644
--- a/templates/repo/diff/comment_form.tmpl
+++ b/templates/repo/diff/comment_form.tmpl
@@ -26,7 +26,7 @@
 		{{end}}
 
 		<div class="field footer tw-mx-2">
-			<span class="markup-info">{{svg "octicon-markup"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
+			<span class="markup-info">{{svg "octicon-markdown"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
 			<div class="tw-text-right">
 				{{if $.reply}}
 					<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>
diff --git a/tools/lint-templates-svg.js b/tools/lint-templates-svg.js
new file mode 100755
index 0000000000..72f756400d
--- /dev/null
+++ b/tools/lint-templates-svg.js
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+import {readdirSync, readFileSync} from 'node:fs';
+import {parse, relative} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import {exit} from 'node:process';
+import fastGlob from 'fast-glob';
+
+const knownSvgs = new Set();
+for (const file of readdirSync(new URL('../public/assets/img/svg', import.meta.url))) {
+  knownSvgs.add(parse(file).name);
+}
+
+const rootPath = fileURLToPath(new URL('..', import.meta.url));
+let hadErrors = false;
+
+for (const file of fastGlob.sync(fileURLToPath(new URL('../templates/**/*.tmpl', import.meta.url)))) {
+  const content = readFileSync(file, 'utf8');
+  for (const [_, name] of content.matchAll(/svg ["'`]([^"'`]+)["'`]/g)) {
+    if (!knownSvgs.has(name)) {
+      console.info(`SVG "${name}" not found, used in ${relative(rootPath, file)}`);
+      hadErrors = true;
+    }
+  }
+}
+
+exit(hadErrors ? 1 : 0);

From 0c8b828f5d5ac7eb8e251edfb1f2536ce1c30336 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 26 Mar 2024 22:08:30 +0800
Subject: [PATCH 531/679] Fix possible data race on tests (#30093)

---
 services/webhook/deliver_test.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index bb8092831f..d0cfc1598f 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -107,7 +107,6 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
 	err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
 	assert.NoError(t, err)
 	assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
-	db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true)
 
 	hookTask := &webhook_model.HookTask{
 		HookID:         hook.ID,

From 30a561ce569fcb1dfc12edf658104a126281494d Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 26 Mar 2024 16:37:14 +0100
Subject: [PATCH 532/679] Restore aligned grid column CSS (#30106)

Fixes #30097, regression from #29894.
---
 web_src/css/modules/grid.css | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/web_src/css/modules/grid.css b/web_src/css/modules/grid.css
index 5a80576c8a..4aaa452372 100644
--- a/web_src/css/modules/grid.css
+++ b/web_src/css/modules/grid.css
@@ -406,6 +406,15 @@
   align-self: center !important;
 }
 
+.ui[class*="left aligned"].grid > .column,
+.ui[class*="left aligned"].grid > .row > .column,
+.ui.grid > [class*="left aligned"].row > .column,
+.ui.grid > [class*="left aligned"].column.column,
+.ui.grid > .row > [class*="left aligned"].column.column {
+  text-align: left;
+  align-self: inherit;
+}
+
 .ui[class*="center aligned"].grid > .column,
 .ui[class*="center aligned"].grid > .row > .column,
 .ui.grid > [class*="center aligned"].row > .column,
@@ -418,6 +427,15 @@
   justify-content: center;
 }
 
+.ui[class*="right aligned"].grid > .column,
+.ui[class*="right aligned"].grid > .row > .column,
+.ui.grid > [class*="right aligned"].row > .column,
+.ui.grid > [class*="right aligned"].column.column,
+.ui.grid > .row > [class*="right aligned"].column.column {
+  text-align: right;
+  align-self: inherit;
+}
+
 .ui[class*="equal width"].grid > .column:not(.row),
 .ui[class*="equal width"].grid > .row > .column,
 .ui.grid > [class*="equal width"].row > .column {

From e0b018706fa7703ef1759d9a75a1399383715808 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 26 Mar 2024 21:33:32 +0200
Subject: [PATCH 533/679] Remove jQuery `.attr` from the common issue page
 functions (#30083)

- Switched from jQuery `attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested most of the functions and they work as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: delvh <dev.lh@web.de>
---
 web_src/js/features/repo-issue.js | 156 ++++++++++++++++--------------
 1 file changed, 83 insertions(+), 73 deletions(-)

diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 20a854fb47..47b4d5f71c 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -42,23 +42,23 @@ export function initRepoIssueTimeTracking() {
 }
 
 async function updateDeadline(deadlineString) {
-  hideElem($('#deadline-err-invalid-date'));
-  $('#deadline-loader').addClass('is-loading');
+  hideElem('#deadline-err-invalid-date');
+  document.getElementById('deadline-loader')?.classList.add('is-loading');
 
   let realDeadline = null;
   if (deadlineString !== '') {
     const newDate = Date.parse(deadlineString);
 
     if (Number.isNaN(newDate)) {
-      $('#deadline-loader').removeClass('is-loading');
-      showElem($('#deadline-err-invalid-date'));
+      document.getElementById('deadline-loader')?.classList.remove('is-loading');
+      showElem('#deadline-err-invalid-date');
       return false;
     }
     realDeadline = new Date(newDate);
   }
 
   try {
-    const response = await POST($('#update-issue-deadline-form').attr('action'), {
+    const response = await POST(document.getElementById('update-issue-deadline-form').getAttribute('action'), {
       data: {due_date: realDeadline},
     });
 
@@ -69,8 +69,8 @@ async function updateDeadline(deadlineString) {
     }
   } catch (error) {
     console.error(error);
-    $('#deadline-loader').removeClass('is-loading');
-    showElem($('#deadline-err-invalid-date'));
+    document.getElementById('deadline-loader').classList.remove('is-loading');
+    showElem('#deadline-err-invalid-date');
   }
 }
 
@@ -87,6 +87,19 @@ export function initRepoIssueDue() {
   });
 }
 
+/**
+ * @param {HTMLElement} item
+ */
+function excludeLabel(item) {
+  const href = item.getAttribute('href');
+  const id = item.getAttribute('data-label-id');
+
+  const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
+  const newStr = 'labels=$1-$2$3&';
+
+  window.location = href.replace(new RegExp(regStr), newStr);
+}
+
 export function initRepoIssueSidebarList() {
   const repolink = $('#repolink').val();
   const repoId = $('#repoId').val();
@@ -123,16 +136,6 @@ export function initRepoIssueSidebarList() {
       fullTextSearch: true,
     });
 
-  function excludeLabel(item) {
-    const href = $(item).attr('href');
-    const id = $(item).data('label-id');
-
-    const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
-    const newStr = 'labels=$1-$2$3&';
-
-    window.location = href.replace(new RegExp(regStr), newStr);
-  }
-
   $('.menu a.label-filter-item').each(function () {
     $(this).on('click', function (e) {
       if (e.altKey) {
@@ -144,9 +147,9 @@ export function initRepoIssueSidebarList() {
 
   $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
     if (e.altKey && e.keyCode === 13) {
-      const $selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected');
-      if ($selectedItems.length > 0) {
-        excludeLabel($($selectedItems[0]));
+      const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
+      if (selectedItem) {
+        excludeLabel(selectedItem);
       }
     }
   });
@@ -166,11 +169,11 @@ export function initRepoIssueCommentDelete() {
         const $parentTimelineGroup = $this.closest('.timeline-item-group');
         // Check if this was a pending comment.
         if ($conversationHolder.find('.pending-label').length) {
-          const $counter = $('#review-box .review-comments-counter');
-          let num = parseInt($counter.attr('data-pending-comment-number')) - 1 || 0;
+          const counter = document.querySelector('#review-box .review-comments-counter');
+          let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
           num = Math.max(num, 0);
-          $counter.attr('data-pending-comment-number', num);
-          $counter.text(num);
+          counter.setAttribute('data-pending-comment-number', num);
+          counter.textContent = String(num);
         }
 
         $(`#${$this.data('comment-id')}`).remove();
@@ -279,14 +282,16 @@ export function initRepoPullRequestMergeInstruction() {
 }
 
 export function initRepoPullRequestAllowMaintainerEdit() {
-  const $checkbox = $('#allow-edits-from-maintainers');
-  if (!$checkbox.length) return;
+  const checkbox = document.getElementById('allow-edits-from-maintainers');
+  if (!checkbox) return;
 
-  const promptError = $checkbox.attr('data-prompt-error');
+  const $checkbox = $(checkbox);
+
+  const promptError = checkbox.getAttribute('data-prompt-error');
   $checkbox.checkbox({
     'onChange': async () => {
       const checked = $checkbox.checkbox('is checked');
-      let url = $checkbox.attr('data-url');
+      let url = checkbox.getAttribute('data-url');
       url += '/set_allow_maintainer_edit';
       $checkbox.checkbox('set disabled');
       try {
@@ -298,7 +303,7 @@ export function initRepoPullRequestAllowMaintainerEdit() {
         }
       } catch (error) {
         console.error(error);
-        showTemporaryTooltip($checkbox[0], promptError);
+        showTemporaryTooltip(checkbox, promptError);
       } finally {
         $checkbox.checkbox('set enabled');
       }
@@ -325,7 +330,9 @@ export function initRepoIssueReferenceRepositorySearch() {
       },
       onChange(_value, _text, $choice) {
         const $form = $choice.closest('form');
-        $form.attr('action', `${appSubUrl}/${_text}/issues/new`);
+        if (!$form.length) return;
+
+        $form[0].setAttribute('action', `${appSubUrl}/${_text}/issues/new`);
       },
       fullTextSearch: true,
     });
@@ -375,17 +382,16 @@ export function initRepoIssueComments() {
     window.location.reload();
   });
 
-  $(document).on('click', (event) => {
-    const $urlTarget = $(':target');
-    if (!$urlTarget.length) return;
+  document.addEventListener('click', (e) => {
+    const urlTarget = document.querySelector(':target');
+    if (!urlTarget) return;
 
-    const urlTargetId = $urlTarget.attr('id');
+    const urlTargetId = urlTarget.id;
     if (!urlTargetId) return;
+
     if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
 
-    const $target = $(event.target);
-
-    if (!$target.closest(`#${urlTargetId}`).length) {
+    if (!e.target.closest(`#${urlTargetId}`)) {
       const scrollPosition = $(window).scrollTop();
       window.location.hash = '';
       $(window).scrollTop(scrollPosition);
@@ -419,30 +425,33 @@ export function initRepoPullRequestReview() {
     if (window.history.scrollRestoration !== 'manual') {
       window.history.scrollRestoration = 'manual';
     }
-    const $commentDiv = $(window.location.hash);
-    if ($commentDiv) {
+    const commentDiv = document.querySelector(window.location.hash);
+    if (commentDiv) {
       // get the name of the parent id
-      const groupID = $commentDiv.closest('div[id^="code-comments-"]').attr('id');
+      const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id');
       if (groupID && groupID.startsWith('code-comments-')) {
         const id = groupID.slice(14);
-        const $ancestorDiffBox = $commentDiv.closest('.diff-file-box');
+        const ancestorDiffBox = commentDiv.closest('.diff-file-box');
         // on pages like conversation, there is no diff header
-        const $diffHeader = $ancestorDiffBox.find('.diff-file-header');
+        const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header');
+
         // offset is for scrolling
         let offset = 30;
-        if ($diffHeader[0]) {
-          offset += $('.diff-detail-box').outerHeight() + $diffHeader.outerHeight();
+        if (diffHeader) {
+          offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight();
         }
-        $(`#show-outdated-${id}`).addClass('tw-hidden');
-        $(`#code-comments-${id}`).removeClass('tw-hidden');
-        $(`#code-preview-${id}`).removeClass('tw-hidden');
-        $(`#hide-outdated-${id}`).removeClass('tw-hidden');
+
+        document.getElementById(`show-outdated-${id}`).classList.add('tw-hidden');
+        document.getElementById(`code-comments-${id}`).classList.remove('tw-hidden');
+        document.getElementById(`code-preview-${id}`).classList.remove('tw-hidden');
+        document.getElementById(`hide-outdated-${id}`).classList.remove('tw-hidden');
         // if the comment box is folded, expand it
-        if ($ancestorDiffBox.attr('data-folded') && $ancestorDiffBox.attr('data-folded') === 'true') {
-          setFileFolding($ancestorDiffBox[0], $ancestorDiffBox.find('.fold-file')[0], false);
+        if (ancestorDiffBox.getAttribute('data-folded') === 'true') {
+          setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false);
         }
+
         window.scrollTo({
-          top: $commentDiv.offset().top - offset,
+          top: $(commentDiv).offset().top - offset,
           behavior: 'instant',
         });
       }
@@ -529,7 +538,7 @@ export function initRepoPullRequestReview() {
     const $commentCloud = $td.find('.comment-code-cloud');
     if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
       try {
-        const response = await GET($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
+        const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
         const html = await response.text();
         $td.html(html);
         $td.find("input[name='line']").val(idx);
@@ -585,6 +594,22 @@ export function initRepoIssueWipToggle() {
   });
 }
 
+async function pullrequest_targetbranch_change(update_url) {
+  const targetBranch = $('#pull-target-branch').data('branch');
+  const $branchTarget = $('#branch_target');
+  if (targetBranch === $branchTarget.text()) {
+    window.location.reload();
+    return false;
+  }
+  try {
+    await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
+  } catch (error) {
+    console.error(error);
+  } finally {
+    window.location.reload();
+  }
+}
+
 export function initRepoIssueTitleEdit() {
   // Edit issue title
   const $issueTitle = $('#issue-title');
@@ -607,23 +632,7 @@ export function initRepoIssueTitleEdit() {
   $('#edit-title').on('click', editTitleToggle);
   $('#cancel-edit-title').on('click', editTitleToggle);
   $('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
-    const pullrequest_targetbranch_change = async function (update_url) {
-      const targetBranch = $('#pull-target-branch').data('branch');
-      const $branchTarget = $('#branch_target');
-      if (targetBranch === $branchTarget.text()) {
-        window.location.reload();
-        return false;
-      }
-      try {
-        await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
-      } catch (error) {
-        console.error(error);
-      } finally {
-        window.location.reload();
-      }
-    };
-
-    const pullrequest_target_update_url = $(this).attr('data-target-update-url');
+    const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
     if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
       $editInput.val($issueTitle.text());
       await pullrequest_targetbranch_change(pullrequest_target_update_url);
@@ -631,7 +640,7 @@ export function initRepoIssueTitleEdit() {
       try {
         const params = new URLSearchParams();
         params.append('title', $editInput.val());
-        const response = await POST($(this).attr('data-update-url'), {data: params});
+        const response = await POST(this.getAttribute('data-update-url'), {data: params});
         const data = await response.json();
         $editInput.val(data.title);
         $issueTitle.text(data.title);
@@ -671,10 +680,11 @@ export function initSingleCommentEditor($commentForm) {
   // * normal new issue/pr page, no status-button
   // * issue/pr view page, with comment form, has status-button
   const opts = {};
-  const $statusButton = $('#status-button');
-  if ($statusButton.length) {
+  const statusButton = document.getElementById('status-button');
+  if (statusButton) {
     opts.onContentChanged = (editor) => {
-      $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
+      const statusText = statusButton.getAttribute(editor.value().trim() ? 'data-status-and-comment' : 'data-status');
+      statusButton.textContent = statusText;
     };
   }
   initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);

From a1f11e2e33f20409eac65b2d0e9a7cd7c767eb72 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 26 Mar 2024 21:38:37 +0200
Subject: [PATCH 534/679] Remove jQuery calls that have no effect on `showElem`
 and `hideElem` (#30110)

There's no need to initialize a jQuery object with a CSS selector when
we can pass the CSS selector directly.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/admin/common.js | 36 ++++++++++++++---------------
 web_src/js/features/org-team.js     |  4 ++--
 web_src/js/features/repo-editor.js  |  4 ++--
 3 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 4e64bff330..59edba11c5 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -17,8 +17,8 @@ export function initAdminCommon() {
       if ($(this).val().substring(0, 1) === '0') {
         $('#user_name').removeAttr('disabled');
         $('#login_name').removeAttr('required');
-        hideElem($('.non-local'));
-        showElem($('.local'));
+        hideElem('.non-local');
+        showElem('.local');
         $('#user_name').trigger('focus');
 
         if ($(this).data('password') === 'required') {
@@ -29,8 +29,8 @@ export function initAdminCommon() {
           $('#user_name').attr('disabled', 'disabled');
         }
         $('#login_name').attr('required', 'required');
-        showElem($('.non-local'));
-        hideElem($('.local'));
+        showElem('.non-local');
+        hideElem('.local');
         $('#login_name').trigger('focus');
 
         $('#password').removeAttr('required');
@@ -40,9 +40,9 @@ export function initAdminCommon() {
 
   function onSecurityProtocolChange() {
     if ($('#security_protocol').val() > 0) {
-      showElem($('.has-tls'));
+      showElem('.has-tls');
     } else {
-      hideElem($('.has-tls'));
+      hideElem('.has-tls');
     }
   }
 
@@ -57,21 +57,21 @@ export function initAdminCommon() {
   }
 
   function onOAuth2Change(applyDefaultValues) {
-    hideElem($('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url'));
+    hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url');
     $('.open_id_connect_auto_discovery_url input[required]').removeAttr('required');
 
     const provider = $('#oauth2_provider').val();
     switch (provider) {
       case 'openidConnect':
         $('.open_id_connect_auto_discovery_url input').attr('required', 'required');
-        showElem($('.open_id_connect_auto_discovery_url'));
+        showElem('.open_id_connect_auto_discovery_url');
         break;
       default:
         if ($(`#${provider}_customURLSettings`).data('required')) {
           $('#oauth2_use_custom_url').attr('checked', 'checked');
         }
         if ($(`#${provider}_customURLSettings`).data('available')) {
-          showElem($('.oauth2_use_custom_url'));
+          showElem('.oauth2_use_custom_url');
         }
     }
     onOAuth2UseCustomURLChange(applyDefaultValues);
@@ -79,7 +79,7 @@ export function initAdminCommon() {
 
   function onOAuth2UseCustomURLChange(applyDefaultValues) {
     const provider = $('#oauth2_provider').val();
-    hideElem($('.oauth2_use_custom_url_field'));
+    hideElem('.oauth2_use_custom_url_field');
     $('.oauth2_use_custom_url_field input[required]').removeAttr('required');
 
     if (document.getElementById('oauth2_use_custom_url')?.checked) {
@@ -102,7 +102,7 @@ export function initAdminCommon() {
   // New authentication
   if ($('.admin.new.authentication').length > 0) {
     $('#auth_type').on('change', function () {
-      hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi'));
+      hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi');
 
       $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]').removeAttr('required');
       $('.binddnrequired').removeClass('required');
@@ -110,30 +110,30 @@ export function initAdminCommon() {
       const authType = $(this).val();
       switch (authType) {
         case '2': // LDAP
-          showElem($('.ldap'));
+          showElem('.ldap');
           $('.binddnrequired input, .ldap div.required:not(.dldap) input').attr('required', 'required');
           $('.binddnrequired').addClass('required');
           break;
         case '3': // SMTP
-          showElem($('.smtp'));
-          showElem($('.has-tls'));
+          showElem('.smtp');
+          showElem('.has-tls');
           $('.smtp div.required input, .has-tls').attr('required', 'required');
           break;
         case '4': // PAM
-          showElem($('.pam'));
+          showElem('.pam');
           $('.pam input').attr('required', 'required');
           break;
         case '5': // LDAP
-          showElem($('.dldap'));
+          showElem('.dldap');
           $('.dldap div.required:not(.ldap) input').attr('required', 'required');
           break;
         case '6': // OAuth2
-          showElem($('.oauth2'));
+          showElem('.oauth2');
           $('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input').attr('required', 'required');
           onOAuth2Change(true);
           break;
         case '7': // SSPI
-          showElem($('.sspi'));
+          showElem('.sspi');
           $('.sspi div.required input').attr('required', 'required');
           break;
       }
diff --git a/web_src/js/features/org-team.js b/web_src/js/features/org-team.js
index 2236bc58bc..c216fdf6a2 100644
--- a/web_src/js/features/org-team.js
+++ b/web_src/js/features/org-team.js
@@ -8,9 +8,9 @@ export function initOrgTeamSettings() {
   $('.organization.new.team input[name=permission]').on('change', () => {
     const val = $('input[name=permission]:checked', '.organization.new.team').val();
     if (val === 'admin') {
-      hideElem($('.organization.new.team .team-units'));
+      hideElem('.organization.new.team .team-units');
     } else {
-      showElem($('.organization.new.team .team-units'));
+      showElem('.organization.new.team .team-units');
     }
   });
 }
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index da3bda8c1d..fc951750a9 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -64,10 +64,10 @@ export function initRepoEditor() {
 
   $('.js-quick-pull-choice-option').on('change', function () {
     if ($(this).val() === 'commit-to-new-branch') {
-      showElem($('.quick-pull-branch-name'));
+      showElem('.quick-pull-branch-name');
       document.querySelector('.quick-pull-branch-name input').required = true;
     } else {
-      hideElem($('.quick-pull-branch-name'));
+      hideElem('.quick-pull-branch-name');
       document.querySelector('.quick-pull-branch-name input').required = false;
     }
     $('#commit-button').text(this.getAttribute('button_text'));

From 5687aca4fc9aef44e1fb5af655b2320d18583dbd Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 26 Mar 2024 21:49:38 +0200
Subject: [PATCH 535/679] Remove jQuery `.attr` from the code comments (#30112)

- Switched from jQuery `attr` to plain javascript `getAttribute`
- Tested the code comments and they work as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-legacy.js | 131 ++++++++++++++++-------------
 1 file changed, 71 insertions(+), 60 deletions(-)

diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index e96afe484e..34320de1de 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -74,11 +74,11 @@ export function initRepoCommentForm() {
       }
 
       if (editMode === 'true') {
-        const $form = $('#update_issueref_form');
+        const form = document.getElementById('update_issueref_form');
         const params = new URLSearchParams();
         params.append('ref', selectedValue);
         try {
-          await POST($form.attr('action'), {data: params});
+          await POST(form.getAttribute('action'), {data: params});
           window.location.reload();
         } catch (error) {
           console.error(error);
@@ -138,12 +138,12 @@ export function initRepoCommentForm() {
       hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
 
       const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment
-      const scope = $(this).attr('data-scope');
+      const scope = this.getAttribute('data-scope');
 
       $(this).parent().find('.item').each(function () {
         if (scope) {
           // Enable only clicked item for scoped labels
-          if ($(this).attr('data-scope') !== scope) {
+          if (this.getAttribute('data-scope') !== scope) {
             return true;
           }
           if (this !== clickedItem && !$(this).hasClass('checked')) {
@@ -319,29 +319,32 @@ export function initRepoCommentForm() {
 async function onEditContent(event) {
   event.preventDefault();
 
-  const $segment = $(this).closest('.header').next();
-  const $editContentZone = $segment.find('.edit-content-zone');
-  const $renderContent = $segment.find('.render-content');
-  const $rawContent = $segment.find('.raw-content');
+  const segment = this.closest('.header').nextElementSibling;
+  const editContentZone = segment.querySelector('.edit-content-zone');
+  const renderContent = segment.querySelector('.render-content');
+  const rawContent = segment.querySelector('.raw-content');
 
   let comboMarkdownEditor;
 
-  const setupDropzone = async ($dropzone) => {
-    if (!$dropzone.length) return null;
+  /**
+   * @param {HTMLElement} dropzone
+   */
+  const setupDropzone = async (dropzone) => {
+    if (!dropzone) return null;
 
     let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
     let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
-    const dz = await createDropzone($dropzone[0], {
-      url: $dropzone.attr('data-upload-url'),
+    const dz = await createDropzone(dropzone, {
+      url: dropzone.getAttribute('data-upload-url'),
       headers: {'X-Csrf-Token': csrfToken},
-      maxFiles: $dropzone.attr('data-max-file'),
-      maxFilesize: $dropzone.attr('data-max-size'),
-      acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'),
+      maxFiles: dropzone.getAttribute('data-max-file'),
+      maxFilesize: dropzone.getAttribute('data-max-size'),
+      acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
       addRemoveLinks: true,
-      dictDefaultMessage: $dropzone.attr('data-default-message'),
-      dictInvalidFileType: $dropzone.attr('data-invalid-input-type'),
-      dictFileTooBig: $dropzone.attr('data-file-too-big'),
-      dictRemoveFile: $dropzone.attr('data-remove-file'),
+      dictDefaultMessage: dropzone.getAttribute('data-default-message'),
+      dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
+      dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
+      dictRemoveFile: dropzone.getAttribute('data-remove-file'),
       timeout: 0,
       thumbnailMethod: 'contain',
       thumbnailWidth: 480,
@@ -350,46 +353,54 @@ async function onEditContent(event) {
         this.on('success', (file, data) => {
           file.uuid = data.uuid;
           fileUuidDict[file.uuid] = {submitted: false};
-          const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
-          $dropzone.find('.files').append($input);
+          const input = document.createElement('input');
+          input.id = data.uuid;
+          input.name = 'files';
+          input.type = 'hidden';
+          input.value = data.uuid;
+          dropzone.querySelector('.files').insertAdjacentHTML('beforeend', input.outerHTML);
         });
         this.on('removedfile', async (file) => {
           if (disableRemovedfileEvent) return;
-          $(`#${file.uuid}`).remove();
-          if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
+          document.getElementById(file.uuid)?.remove();
+          if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
             try {
-              await POST($dropzone.attr('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
+              await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
             } catch (error) {
               console.error(error);
             }
           }
         });
         this.on('submit', () => {
-          $.each(fileUuidDict, (fileUuid) => {
+          for (const fileUuid of Object.keys(fileUuidDict)) {
             fileUuidDict[fileUuid].submitted = true;
-          });
+          }
         });
         this.on('reload', async () => {
           try {
-            const response = await GET($editContentZone.attr('data-attachment-url'));
+            const response = await GET(editContentZone.getAttribute('data-attachment-url'));
             const data = await response.json();
             // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
             disableRemovedfileEvent = true;
             dz.removeAllFiles(true);
-            $dropzone.find('.files').empty();
+            dropzone.querySelector('.files').innerHTML = '';
             fileUuidDict = {};
             disableRemovedfileEvent = false;
 
             for (const attachment of data) {
-              const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`;
+              const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
               dz.emit('addedfile', attachment);
               dz.emit('thumbnail', attachment, imgSrc);
               dz.emit('complete', attachment);
               dz.files.push(attachment);
               fileUuidDict[attachment.uuid] = {submitted: true};
-              $dropzone.find(`img[src='${imgSrc}']`)[0].style.maxWidth = '100%';
-              const $input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
-              $dropzone.find('.files').append($input);
+              dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
+              const input = document.createElement('input');
+              input.id = attachment.uuid;
+              input.name = 'files';
+              input.type = 'hidden';
+              input.value = attachment.uuid;
+              dropzone.querySelector('.files').insertAdjacentHTML('beforeend', input.outerHTML);
             }
           } catch (error) {
             console.error(error);
@@ -402,44 +413,44 @@ async function onEditContent(event) {
   };
 
   const cancelAndReset = (dz) => {
-    showElem($renderContent);
-    hideElem($editContentZone);
+    showElem(renderContent);
+    hideElem(editContentZone);
     if (dz) {
       dz.emit('reload');
     }
   };
 
   const saveAndRefresh = async (dz) => {
-    showElem($renderContent);
-    hideElem($editContentZone);
+    showElem(renderContent);
+    hideElem(editContentZone);
 
     try {
       const params = new URLSearchParams({
         content: comboMarkdownEditor.value(),
-        context: $editContentZone.attr('data-context'),
+        context: editContentZone.getAttribute('data-context'),
       });
       for (const file of dz.files) params.append('files[]', file.uuid);
 
-      const response = await POST($editContentZone.attr('data-update-url'), {data: params});
+      const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
       const data = await response.json();
       if (!data.content) {
-        $renderContent.html($('#no-content').html());
-        $rawContent.text('');
+        renderContent.innerHTML = document.getElementById('no-content').innerHTML;
+        rawContent.textContent = '';
       } else {
-        $renderContent.html(data.content);
-        $rawContent.text(comboMarkdownEditor.value());
-        const $refIssues = $renderContent.find('p .ref-issue');
-        attachRefIssueContextPopup($refIssues);
+        renderContent.innerHTML = data.content;
+        rawContent.textContent = comboMarkdownEditor.value();
+        const refIssues = renderContent.querySelectorAll('p .ref-issue');
+        attachRefIssueContextPopup(refIssues);
       }
-      const $content = $segment;
-      if (!$content.find('.dropzone-attachments').length) {
+      const content = segment;
+      if (!content.querySelector('.dropzone-attachments')) {
         if (data.attachments !== '') {
-          $content[0].insertAdjacentHTML('beforeend', data.attachments);
+          content.insertAdjacentHTML('beforeend', data.attachments);
         }
       } else if (data.attachments === '') {
-        $content.find('.dropzone-attachments').remove();
+        content.querySelector('.dropzone-attachments').remove();
       } else {
-        $content.find('.dropzone-attachments')[0].outerHTML = data.attachments;
+        content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
       }
       if (dz) {
         dz.emit('submit');
@@ -452,29 +463,29 @@ async function onEditContent(event) {
     }
   };
 
-  if (!$editContentZone.html()) {
-    $editContentZone.html($('#issue-comment-editor-template').html());
-    comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
+  if (!editContentZone.innerHTML) {
+    editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
+    comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
 
-    const $dropzone = $editContentZone.find('.dropzone');
-    const dz = await setupDropzone($dropzone);
-    $editContentZone.find('.cancel.button').on('click', (e) => {
+    const dropzone = editContentZone.querySelector('.dropzone');
+    const dz = await setupDropzone(dropzone);
+    editContentZone.querySelector('.cancel.button').addEventListener('click', (e) => {
       e.preventDefault();
       cancelAndReset(dz);
     });
-    $editContentZone.find('.save.button').on('click', (e) => {
+    editContentZone.querySelector('.save.button').addEventListener('click', (e) => {
       e.preventDefault();
       saveAndRefresh(dz);
     });
   } else {
-    comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
+    comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
   }
 
   // Show write/preview tab and copy raw content as needed
-  showElem($editContentZone);
-  hideElem($renderContent);
+  showElem(editContentZone);
+  hideElem(renderContent);
   if (!comboMarkdownEditor.value()) {
-    comboMarkdownEditor.value($rawContent.text());
+    comboMarkdownEditor.value(rawContent.textContent);
   }
   comboMarkdownEditor.focus();
 }

From f47e00d9d3c3bd58b5944a29c4ff5cec0357520a Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 26 Mar 2024 21:57:57 +0200
Subject: [PATCH 536/679] Remove jQuery `.attr` from the Fomantic modal cancel
 buttons (#30113)

- Switched from jQuery `attr` to plain javascript `setAttribute`
- Tested the modals and they work as before

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/modules/fomantic/modal.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/web_src/js/modules/fomantic/modal.js b/web_src/js/modules/fomantic/modal.js
index 7c9aade790..8b455cf4de 100644
--- a/web_src/js/modules/fomantic/modal.js
+++ b/web_src/js/modules/fomantic/modal.js
@@ -19,7 +19,9 @@ function ariaModalFn(...args) {
       // In such case, the "Enter" key will trigger the "cancel" button instead of "ok" button, then the dialog will be closed.
       // It breaks the user experience - the "Enter" key should confirm the dialog and submit the form.
       // So, all "cancel" buttons without "[type]" must be marked as "type=button".
-      $(el).find('form button.cancel:not([type])').attr('type', 'button');
+      for (const button of el.querySelectorAll('form button.cancel:not([type])')) {
+        button.setAttribute('type', 'button');
+      }
     }
   }
   return ret;

From 538790ad1db8bf66942ff2ead4ef78df5ab8b702 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 27 Mar 2024 10:34:10 +0800
Subject: [PATCH 537/679] Put an edit file button on pull request files to
 allow a quick operation (#29697)

Resolve #23848

This PR put an edit file button on pull request files to allow a quick
edit for a file. After the edit finished, it will return back to the
viewed file position on pull request files tab.

It also use a branch view file link instead of commit link when it's a
non-commit pull request files view.

<img width="1532" alt="image"
src="https://github.com/go-gitea/gitea/assets/81045/3637ca4c-89d5-4621-847b-79702a44f617">

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 routers/private/hook_post_receive.go   |  4 +++
 routers/web/repo/editor.go             | 12 ++++++--
 routers/web/repo/pull.go               | 26 ++++++++++++++++
 services/pull/pull.go                  | 19 ++++++++++++
 templates/repo/diff/box.tmpl           |  3 ++
 templates/repo/editor/commit_form.tmpl | 42 +++++++++++++-------------
 templates/repo/editor/edit.tmpl        |  2 +-
 tests/integration/pull_compare_test.go |  7 +++++
 8 files changed, 91 insertions(+), 24 deletions(-)

diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index a09956f738..101ae92302 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	gitea_context "code.gitea.io/gitea/services/context"
+	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
@@ -109,6 +110,9 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 				}
 			} else {
 				branchesToSync = append(branchesToSync, update)
+
+				// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
+				pull_service.UpdatePullsRefs(ctx, repo, update)
 			}
 		}
 		if len(branchesToSync) > 0 {
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 082666276a..474f7ff1da 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -80,8 +80,12 @@ func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName,
 		}
 	}
 
-	// Redirect to viewing file or folder
-	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(newBranchName) + "/" + util.PathEscapeSegments(treePath))
+	returnURI := ctx.FormString("return_uri")
+
+	ctx.RedirectToCurrentSite(
+		returnURI,
+		ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath),
+	)
 }
 
 // getParentTreeFields returns list of parent tree names and corresponding tree paths
@@ -100,6 +104,7 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
 }
 
 func editFile(ctx *context.Context, isNewFile bool) {
+	ctx.Data["PageIsViewCode"] = true
 	ctx.Data["PageIsEdit"] = true
 	ctx.Data["IsNewFile"] = isNewFile
 	canCommit := renderCommitRights(ctx)
@@ -190,6 +195,9 @@ func editFile(ctx *context.Context, isNewFile bool) {
 	ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
 	ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
 
+	ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
+	ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
+
 	ctx.HTML(http.StatusOK, tplEditFile)
 }
 
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 2422be39b8..a0a8e5410c 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -857,6 +857,32 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
 		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
 	}
+	if !willShowSpecifiedCommit && !willShowSpecifiedCommitRange && pull.Flow == issues_model.PullRequestFlowGithub {
+		if err := pull.LoadHeadRepo(ctx); err != nil {
+			ctx.ServerError("LoadHeadRepo", err)
+			return
+		}
+
+		if pull.HeadRepo != nil {
+			ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch)
+		}
+
+		if !pull.HasMerged && ctx.Doer != nil {
+			perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
+			if err != nil {
+				ctx.ServerError("GetUserRepoPermission", err)
+				return
+			}
+
+			if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) {
+				ctx.Data["CanEditFile"] = true
+				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
+				ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link()
+				ctx.Data["HeadBranchName"] = pull.HeadBranch
+				ctx.Data["BackToLink"] = setting.AppSubURL + ctx.Req.URL.RequestURI()
+			}
+		}
+	}
 
 	ctx.HTML(http.StatusOK, tplPullFiles)
 }
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 4289e2e6e1..c091b8608a 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -526,6 +526,25 @@ func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, pre
 	return nil
 }
 
+// UpdatePullsRefs update all the PRs head file pointers like /refs/pull/1/head so that it will be dependent by other operations
+func UpdatePullsRefs(ctx context.Context, repo *repo_model.Repository, update *repo_module.PushUpdateOptions) {
+	branch := update.RefFullName.BranchName()
+	// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
+	prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch)
+	if err != nil {
+		log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repo.ID, branch, err)
+	} else {
+		for _, pr := range prs {
+			log.Trace("Updating PR[%d]: composing new test task", pr.ID)
+			if pr.Flow == issues_model.PullRequestFlowGithub {
+				if err := PushToBaseRepo(ctx, pr); err != nil {
+					log.Error("PushToBaseRepo: %v", err)
+				}
+			}
+		}
+	}
+}
+
 // UpdateRef update refs/pull/id/head directly for agit flow pull request
 func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
 	log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 37a4e1e323..555ffafc62 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -166,6 +166,9 @@
 										<a class="ui basic tiny button" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
 									{{else}}
 										<a class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
+										{{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}}
+											<a class="ui basic tiny button" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a>
+										{{end}}
 									{{end}}
 								{{end}}
 								{{if $isReviewFile}}
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index 0ddbec0e8e..21ef63288f 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -39,36 +39,36 @@
 					</label>
 				</div>
 			</div>
-			{{if not .Repository.IsEmpty}}
-			<div class="field">
-				<div class="ui radio checkbox">
-					{{if .CanCreatePullRequest}}
-						<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
-					{{else}}
-						<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
-					{{end}}
-					<label>
-						{{svg "octicon-git-pull-request"}}
+			{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
+				<div class="field">
+					<div class="ui radio checkbox">
 						{{if .CanCreatePullRequest}}
-							{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
+							<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
 						{{else}}
-							{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
+							<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
 						{{end}}
-					</label>
+						<label>
+							{{svg "octicon-git-pull-request"}}
+							{{if .CanCreatePullRequest}}
+								{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
+							{{else}}
+								{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
+							{{end}}
+						</label>
+					</div>
 				</div>
-			</div>
-			<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
-				<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
-					{{svg "octicon-git-branch"}}
-					<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
-					<span class="text-muted js-quick-pull-normalization-info"></span>
+				<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
+					<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
+						{{svg "octicon-git-branch"}}
+						<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
+						<span class="text-muted js-quick-pull-normalization-info"></span>
+					</div>
 				</div>
-			</div>
 			{{end}}
 		</div>
 	</div>
 	<button id="commit-button" type="submit" class="ui primary button">
 		{{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}}
 	</button>
-	<a class="ui button red" href="{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
+	<a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
 </div>
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index 1f5652f6b5..46f82c47d4 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -21,7 +21,7 @@
 							<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
 						{{end}}
 					{{end}}
-					<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
+					<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
 					<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
 				</div>
 			</div>
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
index f5baf05965..5ce8ea3031 100644
--- a/tests/integration/pull_compare_test.go
+++ b/tests/integration/pull_compare_test.go
@@ -25,4 +25,11 @@ func TestPullCompare(t *testing.T) {
 	req = NewRequest(t, "GET", link)
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	assert.EqualValues(t, http.StatusOK, resp.Code)
+
+	// test the edit button in the PR diff view
+	req = NewRequest(t, "GET", "/user2/repo1/pulls/3/files")
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	doc := NewHTMLParser(t, resp.Body)
+	editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
+	assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none")
 }

From 57539bcdc024110c890320e3e785bf3d6ad6df55 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 27 Mar 2024 04:50:24 +0100
Subject: [PATCH 538/679] Fix click handler in job-step-summary (#30122)

Fix mistake from https://github.com/go-gitea/gitea/pull/29977 where the
click handler wasn't updated for the change with the `isExpandable`
function.
---
 web_src/js/components/RepoActionView.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index d56192526e..75cd1db70a 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -463,7 +463,7 @@ export function initRepositoryActionView() {
         </div>
         <div class="job-step-container" ref="steps" v-if="currentJob.steps.length">
           <div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
-            <div class="job-step-summary" @click.stop="jobStep.status !== 'skipped' && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
+            <div class="job-step-summary" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
               <!-- If the job is done and the job step log is loaded for the first time, show the loading icon
                 currentJobStepsStates[i].cursor === null means the log is loaded for the first time
               -->

From a9e5706696f7d593e281d33783877b7772e48e19 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 27 Mar 2024 05:17:14 +0100
Subject: [PATCH 539/679] Upgrade fabric to 6.0.0-beta20 (#30121)

Fixes https://github.com/go-gitea/gitea/issues/29326 because it includes
https://github.com/fabricjs/fabric.js/pull/9707.
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index a7e175e76b..b4fa62e05e 100644
--- a/Makefile
+++ b/Makefile
@@ -959,7 +959,7 @@ generate-gitignore:
 
 .PHONY: generate-images
 generate-images: | node_modules
-	npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7
+	npm install --no-save fabric@6.0.0-beta20 imagemin-zopfli@7
 	node tools/generate-images.js $(TAGS)
 
 .PHONY: generate-manpage

From ce3c3512265df3b4940672be40065c4fb415ef95 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Wed, 27 Mar 2024 13:44:26 +0900
Subject: [PATCH 540/679] Load attachments for code comments (#30124)

Fix #30103

ps: comments has `LoadAttributes`, but maybe considering performance
problem, we don't call it.
---
 models/issues/comment_code.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index 74a7a86f26..f860dacfac 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -74,6 +74,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 		return nil, err
 	}
 
+	if err := comments.LoadAttachments(ctx); err != nil {
+		return nil, err
+	}
+
 	// Find all reviews by ReviewID
 	reviews := make(map[int64]*Review)
 	ids := make([]int64, 0, len(comments))

From 1261dd6742fb7095e51c173ca4641477d81a3634 Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Wed, 27 Mar 2024 15:20:10 +0800
Subject: [PATCH 541/679] When the title in the issue has a value, set the text
 cursor at the end of the text. (#30090)

Fix:  [#25055](https://github.com/go-gitea/gitea/issues/25055)

Before

![image](https://github.com/go-gitea/gitea/assets/37935145/1b89cd7b-4fa3-49aa-9b5e-a8413add436e)

After

![image](https://github.com/go-gitea/gitea/assets/37935145/fa808f8d-d3ce-4245-a4fe-dd0282ba3fdf)

ps: I've noticed that we are gradually replacing jQuery, so I didn't use jQuery here.
---
 templates/repo/issue/new_form.tmpl   | 2 +-
 web_src/js/features/autofocus-end.js | 6 ++++++
 web_src/js/index.js                  | 2 ++
 3 files changed, 9 insertions(+), 1 deletion(-)
 create mode 100644 web_src/js/features/autofocus-end.js

diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 058ea8d73e..88a6c39e52 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -9,7 +9,7 @@
 				{{ctx.AvatarUtils.Avatar .SignedUser 40}}
 				<div class="ui segment content tw-my-0">
 					<div class="field">
-						<input name="title" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" autofocus required maxlength="255" autocomplete="off">
+						<input name="title" class="js-autofocus-end" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" required maxlength="255" autocomplete="off">
 						{{if .PageIsComparePull}}
 							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}</div>
 						{{end}}
diff --git a/web_src/js/features/autofocus-end.js b/web_src/js/features/autofocus-end.js
new file mode 100644
index 0000000000..da71ce9536
--- /dev/null
+++ b/web_src/js/features/autofocus-end.js
@@ -0,0 +1,6 @@
+export function initAutoFocusEnd() {
+  for (const el of document.querySelectorAll('.js-autofocus-end')) {
+    el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus.
+    el.setSelectionRange(el.value.length, el.value.length);
+  }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index abf0d469d1..4c707486bd 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -13,6 +13,7 @@ import {initImageDiff} from './features/imagediff.js';
 import {initRepoMigration} from './features/repo-migration.js';
 import {initRepoProject} from './features/repo-projects.js';
 import {initTableSort} from './features/tablesort.js';
+import {initAutoFocusEnd} from './features/autofocus-end.js';
 import {initAdminUserListSearchForm} from './features/admin/users.js';
 import {initAdminConfigs} from './features/admin/config.js';
 import {initMarkupAnchors} from './markup/anchors.js';
@@ -122,6 +123,7 @@ onDomReady(() => {
   initSshKeyFormParser();
   initStopwatch();
   initTableSort();
+  initAutoFocusEnd();
   initFindFileInRepo();
   initCopyContent();
 

From 4640441a0e23e40bc9ad73ca60f8ade0f29950ee Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Wed, 27 Mar 2024 16:13:12 +0800
Subject: [PATCH 542/679] Fix: The interface is broken when modifying  code
 comments under mobile devices  (#30125)

**Fix**: [#30123](https://github.com/go-gitea/gitea/issues/30123)

**Before**

![image](https://github.com/go-gitea/gitea/assets/37935145/2a186399-85b0-480a-b2f9-f4feffd9a8e2)


**After**

![image](https://github.com/go-gitea/gitea/assets/37935145/ce1ce3e4-3bbb-4a4b-b0e7-e7943a0774f2)
---
 web_src/css/review.css | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/web_src/css/review.css b/web_src/css/review.css
index cf3a4d48f7..7534500e6f 100644
--- a/web_src/css/review.css
+++ b/web_src/css/review.css
@@ -96,9 +96,6 @@
 }
 
 @media (max-width: 767.98px) {
-  .comment-code-cloud .comments .comment {
-    display: flex;
-  }
   .comment-code-cloud .comments .comment .comment-header-right.actions .ui.basic.label {
     display: none;
   }

From 400bb7ced48eb344d75512a1f7f51dc4c69471df Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 27 Mar 2024 17:09:25 +0800
Subject: [PATCH 543/679] Fix bug for markdown rendering of blockquote (#30130)

Caused by #29984

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/markup/markdown/transform_blockquote.go | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index d685cfd1c5..65b735e83b 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -22,10 +22,16 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
 	if firstParagraph.ChildCount() < 3 {
 		return ast.WalkContinue, nil
 	}
-	node1, ok1 := firstParagraph.FirstChild().(*ast.Text)
-	node2, ok2 := node1.NextSibling().(*ast.Text)
-	node3, ok3 := node2.NextSibling().(*ast.Text)
-	if !ok1 || !ok2 || !ok3 {
+	node1, ok := firstParagraph.FirstChild().(*ast.Text)
+	if !ok {
+		return ast.WalkContinue, nil
+	}
+	node2, ok := node1.NextSibling().(*ast.Text)
+	if !ok {
+		return ast.WalkContinue, nil
+	}
+	node3, ok := node2.NextSibling().(*ast.Text)
+	if !ok {
 		return ast.WalkContinue, nil
 	}
 	val1 := string(node1.Segment.Value(reader.Source()))

From 643e6b09587a89dba1f6b58ae21e5d0e7cfd9776 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 27 Mar 2024 10:58:02 +0100
Subject: [PATCH 544/679] Remove fomantic label module (#30081)

Of note is the CSS has references to "floating label" and "transparent
label" but I could not find those anywhere in the code. They are related
to https://github.com/go-gitea/gitea/pull/3939, but I think these have
long been removed.

---------

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/base.css                |  200 +----
 web_src/css/dashboard.css           |   17 -
 web_src/css/index.css               |    1 +
 web_src/css/modules/label.css       |  294 +++++++
 web_src/fomantic/build/semantic.css | 1114 ---------------------------
 web_src/fomantic/semantic.json      |    1 -
 6 files changed, 296 insertions(+), 1331 deletions(-)
 create mode 100644 web_src/css/modules/label.css

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 7431f1dbd1..07f15cac2b 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -835,14 +835,6 @@ input:-webkit-autofill:active,
   font-weight: var(--font-weight-normal);
 }
 
-.ui.floating.label {
-  z-index: 10;
-}
-
-.ui.transparent.label {
-  background-color: transparent;
-}
-
 /* replace fomantic popover box shadows */
 .ui.dropdown .menu,
 .ui.upward.dropdown > .menu,
@@ -877,14 +869,6 @@ input:-webkit-autofill:active,
   width: 100%;
 }
 
-.ui.dropdown .menu > .item > .floating.label {
-  z-index: 11;
-}
-
-.ui.dropdown .menu .menu > .item > .floating.label {
-  z-index: 21;
-}
-
 .ui.dropdown .menu > .header {
   font-size: 0.8em;
 }
@@ -1214,44 +1198,11 @@ overflow-menu .ui.label {
   margin-top: 1px;
 }
 
-.ui.label {
-  padding: 0.3em 0.5em;
-  transition: none;
-  white-space: nowrap;
-}
-
-.ui.label,
-.ui.menu .item > .label,
-.ui.grey.labels .label,
-.ui.ui.ui.grey.label {
+.ui.menu .item > .label {
   background: var(--color-label-bg);
   color: var(--color-label-text);
 }
 
-.ui.label > a {
-  opacity: .75; /* increase contrast over default fomantic .5 */
-}
-
-.ui.active.label {
-  background: var(--color-label-active-bg);
-  border-color: var(--color-label-active-bg);
-  color: var(--color-label-text);
-}
-
-.ui.labels a.label:hover,
-a.ui.label:hover {
-  background: var(--color-label-hover-bg);
-  border-color: var(--color-label-hover-bg);
-  color: var(--color-label-text);
-}
-
-.ui.labels a.active.label:hover,
-a.ui.active.label:hover {
-  background: var(--color-label-active-bg);
-  border-color: var(--color-label-active-bg);
-  color: var(--color-label-text);
-}
-
 .lines-blame-btn {
   padding: 0 0 0 5px;
   display: flex;
@@ -1417,146 +1368,6 @@ a.ui.active.label:hover {
   width: 100%;
 }
 
-.ui.primary.label,
-.ui.primary.labels .label,
-.ui.ui.ui.primary.label {
-  background-color: var(--color-primary);
-  border-color: var(--color-primary-dark-2);
-}
-
-.ui.basic.labels .primary.label,
-.ui.ui.ui.basic.primary.label {
-  background: transparent;
-  border-color: var(--color-primary);
-  color: var(--color-primary);
-}
-
-.ui.basic.labels a.primary.label:hover,
-a.ui.ui.ui.basic.primary.label:hover {
-  background: var(--color-hover);
-  border-color: var(--color-primary-dark-1);
-  color: var(--color-primary-dark-1);
-}
-
-.ui.basic.labels .secondary.label,
-.ui.ui.ui.basic.secondary.label {
-  background: transparent;
-  border-color: var(--color-secondary);
-  color: var(--color-secondary);
-}
-
-.ui.basic.labels .orange.label,
-.ui.ui.ui.basic.orange.label {
-  background: transparent;
-  border-color: var(--color-orange);
-  color: var(--color-orange);
-}
-
-.ui.basic.labels .green.label,
-.ui.ui.ui.basic.green.label {
-  background: transparent;
-  border-color: var(--color-green);
-  color: var(--color-green);
-}
-
-.ui.basic.labels .olive.label,
-.ui.ui.ui.basic.olive.label {
-  background: transparent;
-  border-color: var(--color-olive);
-  color: var(--color-olive);
-}
-
-.ui.basic.labels .teal.label,
-.ui.ui.ui.basic.teal.label {
-  background: transparent;
-  border-color: var(--color-teal);
-  color: var(--color-teal);
-}
-
-.ui.basic.labels .blue.label,
-.ui.ui.ui.basic.blue.label {
-  background: transparent;
-  border-color: var(--color-blue);
-  color: var(--color-blue);
-}
-
-.ui.basic.labels .violet.label,
-.ui.ui.ui.basic.violet.label {
-  background: transparent;
-  border-color: var(--color-violet);
-  color: var(--color-violet);
-}
-
-.ui.basic.labels .purple.label,
-.ui.ui.ui.basic.purple.label {
-  background: transparent;
-  border-color: var(--color-purple);
-  color: var(--color-purple);
-}
-
-.ui.basic.labels .pink.label,
-.ui.ui.ui.basic.pink.label {
-  background: transparent;
-  border-color: var(--color-pink);
-  color: var(--color-pink);
-}
-
-.ui.basic.labels .red.label,
-.ui.ui.ui.basic.red.label {
-  background: transparent;
-  border-color: var(--color-red);
-  color: var(--color-red);
-}
-
-.ui.basic.labels .brown.label,
-.ui.ui.ui.basic.brown.label {
-  background: transparent;
-  border-color: var(--color-brown);
-  color: var(--color-brown);
-}
-
-.ui.basic.labels .yellow.label,
-.ui.ui.ui.basic.yellow.label {
-  background: transparent;
-  border-color: var(--color-yellow);
-  color: var(--color-yellow);
-}
-
-.ui.basic.labels .grey.label,
-.ui.ui.ui.basic.grey.label {
-  background: transparent;
-  border-color: var(--color-grey);
-  color: var(--color-grey);
-}
-
-.ui.basic.labels .black.label,
-.ui.ui.ui.basic.black.label {
-  background: transparent;
-  border-color: var(--color-black);
-  color: var(--color-black);
-}
-
-.ui.basic.labels .label,
-.ui.basic.label,
-.ui.secondary.labels .ui.basic.label {
-  background: var(--color-button);
-  border-color: var(--color-light-border);
-  color: var(--color-text-light);
-}
-
-.ui.basic.labels a.label:hover,
-a.ui.basic.label:hover {
-  color: var(--color-text);
-  border-color: var(--color-light-border);
-  background: var(--color-hover);
-}
-
-.ui.label > img {
-  width: auto !important;
-  vertical-align: middle;
-  height: 2.1666em !important;
-}
-
 .migrate .svg.gitea-git {
   color: var(--color-git);
 }
@@ -1568,10 +1379,6 @@ a.ui.basic.label:hover {
   width: 14px;
 }
 
-.ui.label > .color-icon {
-  margin-left: 0;
-}
-
 .rss-icon {
   display: inline-flex;
   color: var(--color-text-light-1);
@@ -1769,7 +1576,6 @@ table th[data-sortt-desc] .svg {
 .btn,
 .ui.ui.button,
 .ui.ui.dropdown,
-.ui.ui.label,
 .flex-text-inline {
   display: inline-flex;
   align-items: center;
@@ -1785,10 +1591,6 @@ table th[data-sortt-desc] .svg {
   vertical-align: middle;
 }
 
-.ui.ui.circular.label {
-  justify-content: center;
-}
-
 .ui.ui.labeled.button {
   gap: 0;
   align-items: stretch;
diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css
index e50f1abf42..d61e0c1cf2 100644
--- a/web_src/css/dashboard.css
+++ b/web_src/css/dashboard.css
@@ -28,23 +28,6 @@
   width: 75%;
 }
 
-.dashboard.feeds .filter.menu .item .floating.label,
-.dashboard.issues .filter.menu .item .floating.label {
-  top: 7px;
-  left: 90%;
-  width: 15%;
-}
-
-@media (max-width: 767.98px) {
-  .dashboard.feeds .filter.menu .item .floating.label,
-  .dashboard.issues .filter.menu .item .floating.label {
-    top: 10px;
-    left: auto;
-    width: auto;
-    right: 13px;
-  }
-}
-
 /* Sort */
 .dashboard.feeds .filter.menu .jump.item,
 .dashboard.issues .filter.menu .jump.item {
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 74b5617e1c..aa3f6ac48e 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -6,6 +6,7 @@
 @import "./modules/container.css";
 @import "./modules/divider.css";
 @import "./modules/header.css";
+@import "./modules/label.css";
 @import "./modules/segment.css";
 @import "./modules/grid.css";
 @import "./modules/message.css";
diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css
new file mode 100644
index 0000000000..0512c5fddb
--- /dev/null
+++ b/web_src/css/modules/label.css
@@ -0,0 +1,294 @@
+/* based on Fomantic UI label module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.label {
+  display: inline-flex;
+  align-items: center;
+  gap: .25rem;
+  vertical-align: middle;
+  line-height: 1;
+  background: var(--color-label-bg);
+  color: var(--color-label-text);
+  padding: 0.3em 0.5em;
+  text-transform: none;
+  font-size: 0.85714286rem;
+  font-weight: var(--font-weight-medium);
+  border: 0 solid transparent;
+  border-radius: 0.28571429rem;
+  white-space: nowrap;
+}
+
+.ui.label:first-child {
+  margin-left: 0;
+}
+.ui.label:last-child {
+  margin-right: 0;
+}
+
+a.ui.label {
+  cursor: pointer;
+}
+
+.ui.label > a {
+  cursor: pointer;
+  color: inherit;
+  opacity: 0.75;
+}
+.ui.label > a:hover {
+  opacity: 1;
+}
+
+.ui.label > img {
+  width: auto;
+  vertical-align: middle;
+  height: 2.1666em;
+}
+
+.ui.label > .color-icon {
+  margin-left: 0;
+}
+
+.ui.label > .icon {
+  width: auto;
+  margin: 0 0.75em 0 0;
+}
+
+.ui.label > .detail {
+  display: inline-block;
+  vertical-align: top;
+  font-weight: var(--font-weight-medium);
+  margin-left: 1em;
+  opacity: 0.8;
+}
+.ui.label > .detail .icon {
+  margin: 0 0.25em 0 0;
+}
+
+.ui.label > .close.icon,
+.ui.label > .delete.icon {
+  cursor: pointer;
+  font-size: 0.92857143em;
+  opacity: 0.5;
+}
+.ui.label > .close.icon:hover,
+.ui.label > .delete.icon:hover {
+  opacity: 1;
+}
+
+.ui.label.left.icon > .close.icon,
+.ui.label.left.icon > .delete.icon {
+  margin: 0 0.5em 0 0;
+}
+.ui.label:not(.icon) > .close.icon,
+.ui.label:not(.icon) > .delete.icon {
+  margin: 0 0 0 0.5em;
+}
+
+.ui.header > .ui.label {
+  margin-top: -0.29165em;
+}
+
+a.ui.label:hover {
+  background: var(--color-label-hover-bg);
+  border-color: var(--color-label-hover-bg);
+  color: var(--color-label-text);
+}
+
+.ui.label.visible:not(.dropdown) {
+  display: inline-block !important;
+}
+
+.ui.basic.label {
+  background: var(--color-button);
+  border: 1px solid var(--color-light-border);
+  color: var(--color-text-light);
+  padding: calc(0.5833em - 1px) calc(0.833em - 1px);
+}
+a.ui.basic.label:hover {
+  text-decoration: none;
+  color: var(--color-text);
+  border-color: var(--color-light-border);
+  background: var(--color-hover);
+}
+
+.ui.ui.ui.primary.label {
+  background: var(--color-primary);
+  border-color: var(--color-primary-dark-2);
+  color: var(--color-primary-contrast);
+}
+a.ui.ui.ui.primary.label:hover {
+  background: var(--color-primary-dark-3);
+  border-color: var(--color-primary-dark-3);
+  color: var(--color-primary-contrast);
+}
+.ui.ui.ui.basic.primary.label {
+  background: transparent;
+  border-color: var(--color-primary);
+  color: var(--color-primary);
+}
+a.ui.ui.ui.basic.primary.label:hover {
+  background: var(--color-hover);
+  border-color: var(--color-primary-dark-1);
+  color: var(--color-primary-dark-1);
+}
+
+.ui.ui.ui.red.label {
+  background: var(--color-red);
+  border-color: var(--color-red);
+  color: var(--color-white);
+}
+a.ui.ui.ui.red.label:hover {
+  background: var(--color-red-dark-1);
+  border-color: var(--color-red-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.red.label {
+  background: transparent;
+  border-color: var(--color-red);
+  color: var(--color-red);
+}
+a.ui.ui.ui.basic.red.label:hover {
+  background: transparent;
+  border-color: var(--color-red-dark-1);
+  color: var(--color-red-dark-1);
+}
+
+.ui.ui.ui.orange.label {
+  background: var(--color-orange);
+  border-color: var(--color-orange);
+  color: var(--color-white);
+}
+a.ui.ui.ui.orange.label:hover {
+  background: var(--color-orange-dark-1);
+  border-color: var(--color-orange-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.orange.label {
+  background: transparent;
+  border-color: var(--color-orange);
+  color: var(--color-orange);
+}
+a.ui.ui.ui.basic.orange.label:hover {
+  background: transparent;
+  border-color: var(--color-orange-dark-1);
+  color: var(--color-orange-dark-1);
+}
+
+.ui.ui.ui.yellow.label {
+  background: var(--color-yellow);
+  border-color: var(--color-yellow);
+  color: var(--color-white);
+}
+a.ui.ui.ui.yellow.label:hover {
+  background: var(--color-yellow-dark-1);
+  border-color: var(--color-yellow-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.yellow.label {
+  background: transparent;
+  border-color: var(--color-yellow);
+  color: var(--color-yellow);
+}
+a.ui.ui.ui.basic.yellow.label:hover {
+  background: transparent;
+  border-color: var(--color-yellow-dark-1);
+  color: var(--color-yellow-dark-1);
+}
+.ui.ui.ui.olive.label {
+  background: var(--color-olive);
+  border-color: var(--color-olive);
+  color: var(--color-white);
+}
+
+.ui.ui.ui.green.label {
+  background: var(--color-green);
+  border-color: var(--color-green);
+  color: var(--color-white);
+}
+a.ui.ui.ui.green.label:hover {
+  background: var(--color-green-dark-1);
+  border-color: var(--color-green-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.green.label {
+  background: transparent;
+  border-color: var(--color-green);
+  color: var(--color-green);
+}
+a.ui.ui.ui.basic.green.label:hover {
+  background: transparent;
+  border-color: var(--color-green-dark-1);
+  color: var(--color-green-dark-1);
+}
+
+.ui.ui.ui.purple.label {
+  background: var(--color-purple);
+  border-color: var(--color-purple);
+  color: var(--color-white);
+}
+a.ui.ui.ui.purple.label:hover {
+  background: var(--color-purple-dark-1);
+  border-color: var(--color-purple-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.purple.label {
+  background: transparent;
+  border-color: var(--color-purple);
+  color: var(--color-purple);
+}
+a.ui.ui.ui.basic.purple.label:hover {
+  background: transparent;
+  border-color: var(--color-purple-dark-1);
+  color: var(--color-purple-dark-1);
+}
+
+.ui.ui.ui.grey.label {
+  background: var(--color-label-bg);
+  border-color: var(--color-label-bg);
+  color: var(--color-label-text);
+}
+a.ui.ui.ui.grey.label:hover {
+  background: var(--color-label-hover-bg);
+  border-color: var(--color-label-hover-bg);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.grey.label {
+  background: transparent;
+  border-color: var(--color-label-bg);
+  color: var(--color-label-text);
+}
+a.ui.ui.ui.basic.grey.label:hover {
+  background: transparent;
+  border-color: var(--color-label-hover-bg);
+  color: var(--color-label-hover-bg);
+}
+
+.ui.horizontal.label {
+  margin: 0 0.5em 0 0;
+  padding: 0.4em 0.833em;
+  min-width: 3em;
+  text-align: center;
+}
+
+.ui.circular.label {
+  min-width: 2em;
+  min-height: 2em;
+  padding: 0.5em !important;
+  line-height: 1;
+  text-align: center;
+  border-radius: 500rem;
+  justify-content: center;
+}
+
+.ui.mini.label {
+  font-size: 0.64285714rem;
+}
+.ui.tiny.label {
+  font-size: 0.71428571rem;
+}
+.ui.small.label {
+  font-size: 0.78571429rem;
+}
+.ui.large.label {
+  font-size: 1rem;
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 05a3387563..21c41a6161 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -7927,1120 +7927,6 @@ select.ui.dropdown {
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Label
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Label
-*******************************/
-
-.ui.label {
-  display: inline-block;
-  line-height: 1;
-  vertical-align: baseline;
-  margin: 0 0.14285714em;
-  background-color: #E8E8E8;
-  background-image: none;
-  padding: 0.5833em 0.833em;
-  color: rgba(0, 0, 0, 0.6);
-  text-transform: none;
-  font-weight: 500;
-  border: 0 solid transparent;
-  border-radius: 0.28571429rem;
-  transition: background 0.1s ease;
-}
-
-.ui.label:first-child {
-  margin-left: 0;
-}
-
-.ui.label:last-child {
-  margin-right: 0;
-}
-
-/* Link */
-
-a.ui.label {
-  cursor: pointer;
-}
-
-/* Inside Link */
-
-.ui.label > a {
-  cursor: pointer;
-  color: inherit;
-  opacity: 0.5;
-  transition: 0.1s opacity ease;
-}
-
-.ui.label > a:hover {
-  opacity: 1;
-}
-
-/* Image */
-
-.ui.label > img {
-  width: auto !important;
-  vertical-align: middle;
-  height: 2.1666em;
-}
-
-/* Icon */
-
-.ui.left.icon.label > .icon,
-.ui.label > .icon {
-  width: auto;
-  margin: 0 0.75em 0 0;
-}
-
-/* Detail */
-
-.ui.label > .detail {
-  display: inline-block;
-  vertical-align: top;
-  font-weight: 500;
-  margin-left: 1em;
-  opacity: 0.8;
-}
-
-.ui.label > .detail .icon {
-  margin: 0 0.25em 0 0;
-}
-
-/* Removable label */
-
-.ui.label > .close.icon,
-.ui.label > .delete.icon {
-  cursor: pointer;
-  font-size: 0.92857143em;
-  opacity: 0.5;
-  transition: background 0.1s ease;
-}
-
-.ui.label > .close.icon:hover,
-.ui.label > .delete.icon:hover {
-  opacity: 1;
-}
-
-/* Backward compatible positioning */
-
-.ui.label.left.icon > .close.icon,
-.ui.label.left.icon > .delete.icon {
-  margin: 0 0.5em 0 0;
-}
-
-.ui.label:not(.icon) > .close.icon,
-.ui.label:not(.icon) > .delete.icon {
-  margin: 0 0 0 0.5em;
-}
-
-/* Label for only an icon */
-
-.ui.icon.label > .icon {
-  margin: 0 auto;
-}
-
-/* Right Side Icon */
-
-.ui.right.icon.label > .icon {
-  margin: 0 0 0 0.75em;
-}
-
-/*-------------------
-       Group
---------------------*/
-
-.ui.labels > .label {
-  margin: 0 0.5em 0.5em 0;
-}
-
-/*-------------------
-       Coupling
---------------------*/
-
-.ui.header > .ui.label {
-  margin-top: -0.29165em;
-}
-
-/* Remove border radius on attached segment */
-
-.ui.attached.segment > .ui.top.left.attached.label,
-.ui.bottom.attached.segment > .ui.top.left.attached.label {
-  border-top-left-radius: 0;
-}
-
-.ui.attached.segment > .ui.top.right.attached.label,
-.ui.bottom.attached.segment > .ui.top.right.attached.label {
-  border-top-right-radius: 0;
-}
-
-.ui.top.attached.segment > .ui.bottom.left.attached.label {
-  border-bottom-left-radius: 0;
-}
-
-.ui.top.attached.segment > .ui.bottom.right.attached.label {
-  border-bottom-right-radius: 0;
-}
-
-/* Padding on next content after a label */
-
-.ui.top.attached.label ~ .ui.bottom.attached.label + :not(.attached),
-.ui.top.attached.label + :not(.attached) {
-  margin-top: 2rem !important;
-}
-
-.ui.bottom.attached.label ~ :last-child:not(.attached) {
-  margin-top: 0;
-  margin-bottom: 2rem !important;
-}
-
-.ui.segment:not(.basic) > .ui.top.attached.label {
-  margin-top: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.bottom.attached.label {
-  margin-bottom: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.attached.label:not(.right) {
-  margin-left: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.right.attached.label {
-  margin-right: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.attached.label:not(.left):not(.right) {
-  width: calc(100% + 2px);
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*-------------------
-        Attached
-  --------------------*/
-
-.ui[class*="top attached"].label,
-.ui.attached.label {
-  width: 100%;
-  position: absolute;
-  margin: 0;
-  top: 0;
-  left: 0;
-  padding: 0.75em 1em;
-  border-radius: 0.21428571rem 0.21428571rem 0 0;
-}
-
-.ui[class*="bottom attached"].label {
-  top: auto;
-  bottom: 0;
-  border-radius: 0 0 0.21428571rem 0.21428571rem;
-}
-
-.ui[class*="top left attached"].label {
-  width: auto;
-  margin-top: 0;
-  border-radius: 0.21428571rem 0 0.28571429rem 0;
-}
-
-.ui[class*="top right attached"].label {
-  width: auto;
-  left: auto;
-  right: 0;
-  border-radius: 0 0.21428571rem 0 0.28571429rem;
-}
-
-.ui[class*="bottom left attached"].label {
-  width: auto;
-  top: auto;
-  bottom: 0;
-  border-radius: 0 0.28571429rem 0 0.21428571rem;
-}
-
-.ui[class*="bottom right attached"].label {
-  top: auto;
-  bottom: 0;
-  left: auto;
-  right: 0;
-  width: auto;
-  border-radius: 0.28571429rem 0 0.21428571rem 0;
-}
-
-/*******************************
-             States
-*******************************/
-
-/*-------------------
-      Disabled
---------------------*/
-
-.ui.label.disabled {
-  opacity: 0.5;
-}
-
-/*-------------------
-        Hover
---------------------*/
-
-.ui.labels a.label:hover,
-a.ui.label:hover {
-  background-color: #E0E0E0;
-  border-color: #E0E0E0;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.labels a.label:hover:before,
-a.ui.label:hover:before {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-/*-------------------
-        Active
---------------------*/
-
-.ui.active.label {
-  background-color: #D0D0D0;
-  border-color: #D0D0D0;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.active.label:before {
-  background-color: #D0D0D0;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*-------------------
-     Active Hover
---------------------*/
-
-.ui.labels a.active.label:hover,
-a.ui.active.label:hover {
-  background-color: #C8C8C8;
-  border-color: #C8C8C8;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.labels a.active.label:hover:before,
-a.ui.active.label:hover:before {
-  background-color: #C8C8C8;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*-------------------
-      Visible
---------------------*/
-
-.ui.labels.visible .label,
-.ui.label.visible:not(.dropdown) {
-  display: inline-block !important;
-}
-
-/*-------------------
-      Hidden
---------------------*/
-
-.ui.labels.hidden .label,
-.ui.label.hidden {
-  display: none !important;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-          Basic
-  --------------------*/
-
-.ui.basic.labels .label,
-.ui.basic.label {
-  background: none #FFFFFF;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  color: rgba(0, 0, 0, 0.87);
-  box-shadow: none;
-  padding-top: calc(0.5833em - 1px);
-  padding-bottom: calc(0.5833em - 1px);
-  padding-right: calc(0.833em - 1px);
-}
-
-.ui.basic.labels:not(.tag):not(.image):not(.ribbon) .label,
-.ui.basic.label:not(.tag):not(.image):not(.ribbon) {
-  padding-left: calc(0.833em - 1px);
-}
-
-/* Link */
-
-.ui.basic.labels a.label:hover,
-a.ui.basic.label:hover {
-  text-decoration: none;
-  background: none #FFFFFF;
-  color: #1e70bf;
-  box-shadow: none;
-}
-
-/* Pointing */
-
-.ui.basic.pointing.label:before {
-  border-color: inherit;
-}
-
-/*-------------------
-         Fluid
-  --------------------*/
-
-.ui.label.fluid,
-.ui.fluid.labels > .label {
-  width: 100%;
-  box-sizing: border-box;
-}
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.primary.labels .label,
-.ui.ui.ui.primary.label {
-  background-color: #2185D0;
-  border-color: #2185D0;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-/* Link */
-
-.ui.primary.labels a.label:hover,
-a.ui.ui.ui.primary.label:hover {
-  background-color: #1678c2;
-  border-color: #1678c2;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .primary.label,
-.ui.ui.ui.basic.primary.label {
-  background: none #FFFFFF;
-  border-color: #2185D0;
-  color: #2185D0;
-}
-
-.ui.basic.labels a.primary.label:hover,
-a.ui.ui.ui.basic.primary.label:hover {
-  background: none #FFFFFF;
-  border-color: #1678c2;
-  color: #1678c2;
-}
-
-.ui.secondary.labels .label,
-.ui.ui.ui.secondary.label {
-  background-color: #1B1C1D;
-  border-color: #1B1C1D;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-/* Link */
-
-.ui.secondary.labels a.label:hover,
-a.ui.ui.ui.secondary.label:hover {
-  background-color: #27292a;
-  border-color: #27292a;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .secondary.label,
-.ui.ui.ui.basic.secondary.label {
-  background: none #FFFFFF;
-  border-color: #1B1C1D;
-  color: #1B1C1D;
-}
-
-.ui.basic.labels a.secondary.label:hover,
-a.ui.ui.ui.basic.secondary.label:hover {
-  background: none #FFFFFF;
-  border-color: #27292a;
-  color: #27292a;
-}
-
-.ui.red.labels .label,
-.ui.ui.ui.red.label {
-  background-color: #DB2828;
-  border-color: #DB2828;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.red.labels a.label:hover,
-a.ui.ui.ui.red.label:hover {
-  background-color: #d01919;
-  border-color: #d01919;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .red.label,
-.ui.ui.ui.basic.red.label {
-  background: none #FFFFFF;
-  border-color: #DB2828;
-  color: #DB2828;
-}
-
-.ui.basic.labels a.red.label:hover,
-a.ui.ui.ui.basic.red.label:hover {
-  background: none #FFFFFF;
-  border-color: #d01919;
-  color: #d01919;
-}
-
-.ui.orange.labels .label,
-.ui.ui.ui.orange.label {
-  background-color: #F2711C;
-  border-color: #F2711C;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.orange.labels a.label:hover,
-a.ui.ui.ui.orange.label:hover {
-  background-color: #f26202;
-  border-color: #f26202;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .orange.label,
-.ui.ui.ui.basic.orange.label {
-  background: none #FFFFFF;
-  border-color: #F2711C;
-  color: #F2711C;
-}
-
-.ui.basic.labels a.orange.label:hover,
-a.ui.ui.ui.basic.orange.label:hover {
-  background: none #FFFFFF;
-  border-color: #f26202;
-  color: #f26202;
-}
-
-.ui.yellow.labels .label,
-.ui.ui.ui.yellow.label {
-  background-color: #FBBD08;
-  border-color: #FBBD08;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.yellow.labels a.label:hover,
-a.ui.ui.ui.yellow.label:hover {
-  background-color: #eaae00;
-  border-color: #eaae00;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .yellow.label,
-.ui.ui.ui.basic.yellow.label {
-  background: none #FFFFFF;
-  border-color: #FBBD08;
-  color: #FBBD08;
-}
-
-.ui.basic.labels a.yellow.label:hover,
-a.ui.ui.ui.basic.yellow.label:hover {
-  background: none #FFFFFF;
-  border-color: #eaae00;
-  color: #eaae00;
-}
-
-.ui.olive.labels .label,
-.ui.ui.ui.olive.label {
-  background-color: #B5CC18;
-  border-color: #B5CC18;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.olive.labels a.label:hover,
-a.ui.ui.ui.olive.label:hover {
-  background-color: #a7bd0d;
-  border-color: #a7bd0d;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .olive.label,
-.ui.ui.ui.basic.olive.label {
-  background: none #FFFFFF;
-  border-color: #B5CC18;
-  color: #B5CC18;
-}
-
-.ui.basic.labels a.olive.label:hover,
-a.ui.ui.ui.basic.olive.label:hover {
-  background: none #FFFFFF;
-  border-color: #a7bd0d;
-  color: #a7bd0d;
-}
-
-.ui.green.labels .label,
-.ui.ui.ui.green.label {
-  background-color: #21BA45;
-  border-color: #21BA45;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.green.labels a.label:hover,
-a.ui.ui.ui.green.label:hover {
-  background-color: #16ab39;
-  border-color: #16ab39;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .green.label,
-.ui.ui.ui.basic.green.label {
-  background: none #FFFFFF;
-  border-color: #21BA45;
-  color: #21BA45;
-}
-
-.ui.basic.labels a.green.label:hover,
-a.ui.ui.ui.basic.green.label:hover {
-  background: none #FFFFFF;
-  border-color: #16ab39;
-  color: #16ab39;
-}
-
-.ui.teal.labels .label,
-.ui.ui.ui.teal.label {
-  background-color: #00B5AD;
-  border-color: #00B5AD;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.teal.labels a.label:hover,
-a.ui.ui.ui.teal.label:hover {
-  background-color: #009c95;
-  border-color: #009c95;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .teal.label,
-.ui.ui.ui.basic.teal.label {
-  background: none #FFFFFF;
-  border-color: #00B5AD;
-  color: #00B5AD;
-}
-
-.ui.basic.labels a.teal.label:hover,
-a.ui.ui.ui.basic.teal.label:hover {
-  background: none #FFFFFF;
-  border-color: #009c95;
-  color: #009c95;
-}
-
-.ui.blue.labels .label,
-.ui.ui.ui.blue.label {
-  background-color: #2185D0;
-  border-color: #2185D0;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.blue.labels a.label:hover,
-a.ui.ui.ui.blue.label:hover {
-  background-color: #1678c2;
-  border-color: #1678c2;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .blue.label,
-.ui.ui.ui.basic.blue.label {
-  background: none #FFFFFF;
-  border-color: #2185D0;
-  color: #2185D0;
-}
-
-.ui.basic.labels a.blue.label:hover,
-a.ui.ui.ui.basic.blue.label:hover {
-  background: none #FFFFFF;
-  border-color: #1678c2;
-  color: #1678c2;
-}
-
-.ui.violet.labels .label,
-.ui.ui.ui.violet.label {
-  background-color: #6435C9;
-  border-color: #6435C9;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.violet.labels a.label:hover,
-a.ui.ui.ui.violet.label:hover {
-  background-color: #5829bb;
-  border-color: #5829bb;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .violet.label,
-.ui.ui.ui.basic.violet.label {
-  background: none #FFFFFF;
-  border-color: #6435C9;
-  color: #6435C9;
-}
-
-.ui.basic.labels a.violet.label:hover,
-a.ui.ui.ui.basic.violet.label:hover {
-  background: none #FFFFFF;
-  border-color: #5829bb;
-  color: #5829bb;
-}
-
-.ui.purple.labels .label,
-.ui.ui.ui.purple.label {
-  background-color: #A333C8;
-  border-color: #A333C8;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.purple.labels a.label:hover,
-a.ui.ui.ui.purple.label:hover {
-  background-color: #9627ba;
-  border-color: #9627ba;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .purple.label,
-.ui.ui.ui.basic.purple.label {
-  background: none #FFFFFF;
-  border-color: #A333C8;
-  color: #A333C8;
-}
-
-.ui.basic.labels a.purple.label:hover,
-a.ui.ui.ui.basic.purple.label:hover {
-  background: none #FFFFFF;
-  border-color: #9627ba;
-  color: #9627ba;
-}
-
-.ui.pink.labels .label,
-.ui.ui.ui.pink.label {
-  background-color: #E03997;
-  border-color: #E03997;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.pink.labels a.label:hover,
-a.ui.ui.ui.pink.label:hover {
-  background-color: #e61a8d;
-  border-color: #e61a8d;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .pink.label,
-.ui.ui.ui.basic.pink.label {
-  background: none #FFFFFF;
-  border-color: #E03997;
-  color: #E03997;
-}
-
-.ui.basic.labels a.pink.label:hover,
-a.ui.ui.ui.basic.pink.label:hover {
-  background: none #FFFFFF;
-  border-color: #e61a8d;
-  color: #e61a8d;
-}
-
-.ui.brown.labels .label,
-.ui.ui.ui.brown.label {
-  background-color: #A5673F;
-  border-color: #A5673F;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.brown.labels a.label:hover,
-a.ui.ui.ui.brown.label:hover {
-  background-color: #975b33;
-  border-color: #975b33;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .brown.label,
-.ui.ui.ui.basic.brown.label {
-  background: none #FFFFFF;
-  border-color: #A5673F;
-  color: #A5673F;
-}
-
-.ui.basic.labels a.brown.label:hover,
-a.ui.ui.ui.basic.brown.label:hover {
-  background: none #FFFFFF;
-  border-color: #975b33;
-  color: #975b33;
-}
-
-.ui.grey.labels .label,
-.ui.ui.ui.grey.label {
-  background-color: #767676;
-  border-color: #767676;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.grey.labels a.label:hover,
-a.ui.ui.ui.grey.label:hover {
-  background-color: #838383;
-  border-color: #838383;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .grey.label,
-.ui.ui.ui.basic.grey.label {
-  background: none #FFFFFF;
-  border-color: #767676;
-  color: #767676;
-}
-
-.ui.basic.labels a.grey.label:hover,
-a.ui.ui.ui.basic.grey.label:hover {
-  background: none #FFFFFF;
-  border-color: #838383;
-  color: #838383;
-}
-
-.ui.black.labels .label,
-.ui.ui.ui.black.label {
-  background-color: #1B1C1D;
-  border-color: #1B1C1D;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.black.labels a.label:hover,
-a.ui.ui.ui.black.label:hover {
-  background-color: #27292a;
-  border-color: #27292a;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .black.label,
-.ui.ui.ui.basic.black.label {
-  background: none #FFFFFF;
-  border-color: #1B1C1D;
-  color: #1B1C1D;
-}
-
-.ui.basic.labels a.black.label:hover,
-a.ui.ui.ui.basic.black.label:hover {
-  background: none #FFFFFF;
-  border-color: #27292a;
-  color: #27292a;
-}
-
-/*-------------------
-     Horizontal
---------------------*/
-
-.ui.horizontal.labels .label,
-.ui.horizontal.label {
-  margin: 0 0.5em 0 0;
-  padding: 0.4em 0.833em;
-  min-width: 3em;
-  text-align: center;
-}
-
-/*-------------------
-         Circular
-  --------------------*/
-
-.ui.circular.labels .label,
-.ui.circular.label {
-  min-width: 2em;
-  min-height: 2em;
-  padding: 0.5em !important;
-  line-height: 1em;
-  text-align: center;
-  border-radius: 500rem;
-}
-
-.ui.empty.circular.labels .label,
-.ui.empty.circular.label {
-  min-width: 0;
-  min-height: 0;
-  overflow: hidden;
-  width: 0.5em;
-  height: 0.5em;
-  vertical-align: baseline;
-}
-
-/*-------------------
-         Pointing
-  --------------------*/
-
-.ui.pointing.label {
-  position: relative;
-}
-
-.ui.attached.pointing.label {
-  position: absolute;
-}
-
-.ui.pointing.label:before {
-  background-color: inherit;
-  background-image: inherit;
-  border-width: 0;
-  border-style: solid;
-  border-color: inherit;
-}
-
-/* Arrow */
-
-.ui.pointing.label:before {
-  position: absolute;
-  content: '';
-  transform: rotate(45deg);
-  background-image: none;
-  z-index: 2;
-  width: 0.6666em;
-  height: 0.6666em;
-  transition: none;
-}
-
-/*--- Above ---*/
-
-.ui.pointing.label,
-.ui[class*="pointing above"].label {
-  margin-top: 1em;
-}
-
-.ui.pointing.label:before,
-.ui[class*="pointing above"].label:before {
-  border-width: 1px 0 0 1px;
-  transform: translateX(-50%) translateY(-50%) rotate(45deg);
-  top: 0;
-  left: 50%;
-}
-
-/*--- Below ---*/
-
-.ui[class*="bottom pointing"].label,
-.ui[class*="pointing below"].label {
-  margin-top: 0;
-  margin-bottom: 1em;
-}
-
-.ui[class*="bottom pointing"].label:before,
-.ui[class*="pointing below"].label:before {
-  border-width: 0 1px 1px 0;
-  top: auto;
-  right: auto;
-  transform: translateX(-50%) translateY(-50%) rotate(45deg);
-  top: 100%;
-  left: 50%;
-}
-
-/*--- Left ---*/
-
-.ui[class*="left pointing"].label {
-  margin-top: 0;
-  margin-left: 0.6666em;
-}
-
-.ui[class*="left pointing"].label:before {
-  border-width: 0 0 1px 1px;
-  transform: translateX(-50%) translateY(-50%) rotate(45deg);
-  bottom: auto;
-  right: auto;
-  top: 50%;
-  left: 0;
-}
-
-/*--- Right ---*/
-
-.ui[class*="right pointing"].label {
-  margin-top: 0;
-  margin-right: 0.6666em;
-}
-
-.ui[class*="right pointing"].label:before {
-  border-width: 1px 1px 0 0;
-  transform: translateX(50%) translateY(-50%) rotate(45deg);
-  top: 50%;
-  right: 0;
-  bottom: auto;
-  left: auto;
-}
-
-/* Basic Pointing */
-
-/*--- Above ---*/
-
-.ui.basic.pointing.label:before,
-.ui.basic[class*="pointing above"].label:before {
-  margin-top: -1px;
-}
-
-/*--- Below ---*/
-
-.ui.basic[class*="bottom pointing"].label:before,
-.ui.basic[class*="pointing below"].label:before {
-  bottom: auto;
-  top: 100%;
-  margin-top: 1px;
-}
-
-/*--- Left ---*/
-
-.ui.basic[class*="left pointing"].label:before {
-  top: 50%;
-  left: -1px;
-}
-
-/*--- Right ---*/
-
-.ui.basic[class*="right pointing"].label:before {
-  top: 50%;
-  right: -1px;
-}
-
-/*------------------
-     Floating Label
-  -------------------*/
-
-.ui.floating.label {
-  position: absolute;
-  z-index: 100;
-  top: -1em;
-  right: 0;
-  white-space: nowrap;
-  transform: translateX(50%);
-}
-
-.ui.right.aligned.floating.label {
-  transform: translateX(1.2em);
-}
-
-.ui.left.floating.label {
-  left: 0;
-  right: auto;
-  transform: translateX(-50%);
-}
-
-.ui.left.aligned.floating.label {
-  transform: translateX(-1.2em);
-}
-
-.ui.bottom.floating.label {
-  top: auto;
-  bottom: -1em;
-}
-
-/*-------------------
-        Sizes
---------------------*/
-
-.ui.labels .label,
-.ui.label {
-  font-size: 0.85714286rem;
-}
-
-.ui.mini.labels .label,
-.ui.mini.label {
-  font-size: 0.64285714rem;
-}
-
-.ui.tiny.labels .label,
-.ui.tiny.label {
-  font-size: 0.71428571rem;
-}
-
-.ui.small.labels .label,
-.ui.small.label {
-  font-size: 0.78571429rem;
-}
-
-.ui.large.labels .label,
-.ui.large.label {
-  font-size: 1rem;
-}
-
-.ui.big.labels .label,
-.ui.big.label {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.labels .label,
-.ui.huge.label {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.labels .label,
-.ui.massive.label {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 6fbb0e7b97..b916af6922 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -28,7 +28,6 @@
     "dropdown",
     "form",
     "input",
-    "label",
     "list",
     "menu",
     "modal",

From 4efe7884a3c99235b39998472ea430bffe0799e5 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 27 Mar 2024 12:40:21 +0200
Subject: [PATCH 545/679] Remove jQuery from the create/rename branch modals
 (except Fomantic) (#30109)

- Switched to plain JavaScript
- Tested the create/rename branch modals' functionality and they work as
before

# Demo using JavaScript without jQuery

![demo](https://github.com/go-gitea/gitea/assets/20454870/ca53155e-856e-44ca-9852-12ff60065735)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-branch.js | 50 +++++++++++++++---------------
 1 file changed, 25 insertions(+), 25 deletions(-)

diff --git a/web_src/js/features/repo-branch.js b/web_src/js/features/repo-branch.js
index e6da9661b6..b9ffc6127f 100644
--- a/web_src/js/features/repo-branch.js
+++ b/web_src/js/features/repo-branch.js
@@ -8,35 +8,35 @@ export function initRepoBranchButton() {
 
 function initRepoCreateBranchButton() {
   // 2 pages share this code, one is the branch list page, the other is the commit view page: create branch/tag from current commit (dirty code)
-  $('.show-create-branch-modal').on('click', function () {
-    let modalFormName = $(this).attr('data-modal-form');
-    if (!modalFormName) {
-      modalFormName = '#create-branch-form';
-    }
-    $(modalFormName)[0].action = $(modalFormName).attr('data-base-action') + $(this).attr('data-branch-from-urlcomponent');
-    let fromSpanName = $(this).attr('data-modal-from-span');
-    if (!fromSpanName) {
-      fromSpanName = '#modal-create-branch-from-span';
-    }
+  for (const el of document.querySelectorAll('.show-create-branch-modal')) {
+    el.addEventListener('click', () => {
+      const modalFormName = el.getAttribute('data-modal-form') || '#create-branch-form';
+      const modalForm = document.querySelector(modalFormName);
+      if (!modalForm) return;
+      modalForm.action = `${modalForm.getAttribute('data-base-action')}${el.getAttribute('data-branch-from-urlcomponent')}`;
 
-    $(fromSpanName).text($(this).attr('data-branch-from'));
-    $($(this).attr('data-modal')).modal('show');
-  });
+      const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
+      document.querySelector(fromSpanName).textContent = el.getAttribute('data-branch-from');
+
+      $(el.getAttribute('data-modal')).modal('show');
+    });
+  }
 }
 
 function initRepoRenameBranchButton() {
-  $('.show-rename-branch-modal').on('click', function () {
-    const target = $(this).attr('data-modal');
-    const $modal = $(target);
+  for (const el of document.querySelectorAll('.show-rename-branch-modal')) {
+    el.addEventListener('click', () => {
+      const target = el.getAttribute('data-modal');
+      const modal = document.querySelector(target);
+      const oldBranchName = el.getAttribute('data-old-branch-name');
+      modal.querySelector('input[name=from]').value = oldBranchName;
 
-    const oldBranchName = $(this).attr('data-old-branch-name');
-    $modal.find('input[name=from]').val(oldBranchName);
+      // display the warning that the branch which is chosen is the default branch
+      const warn = modal.querySelector('.default-branch-warning');
+      toggleElem(warn, el.getAttribute('data-is-default-branch') === 'true');
 
-    // display the warning that the branch which is chosen is the default branch
-    const $warn = $modal.find('.default-branch-warning');
-    toggleElem($warn, $(this).attr('data-is-default-branch') === 'true');
-
-    const $text = $modal.find('[data-rename-branch-to]');
-    $text.text($text.attr('data-rename-branch-to').replace('%s', oldBranchName));
-  });
+      const text = modal.querySelector('[data-rename-branch-to]');
+      text.textContent = text.getAttribute('data-rename-branch-to').replace('%s', oldBranchName);
+    });
+  }
 }

From a190f68f1bf92554923a4adde50e5cbc637a2a2e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 27 Mar 2024 12:45:05 +0200
Subject: [PATCH 546/679] Remove jQuery `.attr` from the common admin functions
 (#30115)

- Switched from jQuery `attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested most of the functions and they work as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/admin/common.js | 153 +++++++++++++++++-----------
 1 file changed, 91 insertions(+), 62 deletions(-)

diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 59edba11c5..ac8bfe8b34 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -5,72 +5,81 @@ import {POST} from '../../modules/fetch.js';
 
 const {appSubUrl} = window.config;
 
+function onSecurityProtocolChange() {
+  if (Number(document.getElementById('security_protocol')?.value) > 0) {
+    showElem('.has-tls');
+  } else {
+    hideElem('.has-tls');
+  }
+}
+
 export function initAdminCommon() {
-  if (!$('.page-content.admin').length) return;
+  if (!document.querySelector('.page-content.admin')) return;
 
   // check whether appUrl(ROOT_URL) is correct, if not, show an error message
   checkAppUrl();
 
   // New user
   if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) {
-    $('#login_type').on('change', function () {
-      if ($(this).val().substring(0, 1) === '0') {
-        $('#user_name').removeAttr('disabled');
-        $('#login_name').removeAttr('required');
+    document.getElementById('login_type')?.addEventListener('change', function () {
+      if (this.value?.substring(0, 1) === '0') {
+        document.getElementById('user_name')?.removeAttribute('disabled');
+        document.getElementById('login_name')?.removeAttribute('required');
         hideElem('.non-local');
         showElem('.local');
-        $('#user_name').trigger('focus');
+        document.getElementById('user_name')?.focus();
 
-        if ($(this).data('password') === 'required') {
-          $('#password').attr('required', 'required');
+        if (this.getAttribute('data-password') === 'required') {
+          document.getElementById('password')?.setAttribute('required', 'required');
         }
       } else {
-        if ($('.admin.edit.user').length > 0) {
-          $('#user_name').attr('disabled', 'disabled');
+        if (document.querySelector('.admin.edit.user')) {
+          document.getElementById('user_name')?.setAttribute('disabled', 'disabled');
         }
-        $('#login_name').attr('required', 'required');
+        document.getElementById('login_name')?.setAttribute('required', 'required');
         showElem('.non-local');
         hideElem('.local');
-        $('#login_name').trigger('focus');
+        document.getElementById('login_name')?.focus();
 
-        $('#password').removeAttr('required');
+        document.getElementById('password')?.removeAttribute('required');
       }
     });
   }
 
-  function onSecurityProtocolChange() {
-    if ($('#security_protocol').val() > 0) {
-      showElem('.has-tls');
-    } else {
-      hideElem('.has-tls');
-    }
-  }
-
   function onUsePagedSearchChange() {
+    const searchPageSizeElements = document.querySelectorAll('.search-page-size');
     if (document.getElementById('use_paged_search').checked) {
       showElem('.search-page-size');
-      $('.search-page-size').find('input').attr('required', 'required');
+      for (const el of searchPageSizeElements) {
+        el.querySelector('input')?.setAttribute('required', 'required');
+      }
     } else {
       hideElem('.search-page-size');
-      $('.search-page-size').find('input').removeAttr('required');
+      for (const el of searchPageSizeElements) {
+        el.querySelector('input')?.removeAttribute('required');
+      }
     }
   }
 
   function onOAuth2Change(applyDefaultValues) {
     hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url');
-    $('.open_id_connect_auto_discovery_url input[required]').removeAttr('required');
+    for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) {
+      input.removeAttribute('required');
+    }
 
-    const provider = $('#oauth2_provider').val();
+    const provider = document.getElementById('oauth2_provider')?.value;
     switch (provider) {
       case 'openidConnect':
-        $('.open_id_connect_auto_discovery_url input').attr('required', 'required');
+        for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input')) {
+          input.setAttribute('required', 'required');
+        }
         showElem('.open_id_connect_auto_discovery_url');
         break;
       default:
-        if ($(`#${provider}_customURLSettings`).data('required')) {
-          $('#oauth2_use_custom_url').attr('checked', 'checked');
+        if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-required')) {
+          document.getElementById('oauth2_use_custom_url')?.setAttribute('checked', 'checked');
         }
-        if ($(`#${provider}_customURLSettings`).data('available')) {
+        if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-available')) {
           showElem('.oauth2_use_custom_url');
         }
     }
@@ -78,63 +87,83 @@ export function initAdminCommon() {
   }
 
   function onOAuth2UseCustomURLChange(applyDefaultValues) {
-    const provider = $('#oauth2_provider').val();
+    const provider = document.getElementById('oauth2_provider')?.value;
     hideElem('.oauth2_use_custom_url_field');
-    $('.oauth2_use_custom_url_field input[required]').removeAttr('required');
+    for (const input of document.querySelectorAll('.oauth2_use_custom_url_field input[required]')) {
+      input.removeAttribute('required');
+    }
 
     if (document.getElementById('oauth2_use_custom_url')?.checked) {
       for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
         if (applyDefaultValues) {
-          $(`#oauth2_${custom}`).val($(`#${provider}_${custom}`).val());
+          document.getElementById(`oauth2_${custom}`).value = document.getElementById(`${provider}_${custom}`).value;
         }
-        if ($(`#${provider}_${custom}`).data('available')) {
-          $(`.oauth2_${custom} input`).attr('required', 'required');
-          showElem($(`.oauth2_${custom}`));
+        const customInput = document.getElementById(`${provider}_${custom}`);
+        if (customInput && customInput.getAttribute('data-available')) {
+          for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) {
+            input.setAttribute('required', 'required');
+          }
+          showElem(`.oauth2_${custom}`);
         }
       }
     }
   }
 
   function onEnableLdapGroupsChange() {
-    toggleElem($('#ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
+    toggleElem(document.getElementById('ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
   }
 
   // New authentication
-  if ($('.admin.new.authentication').length > 0) {
-    $('#auth_type').on('change', function () {
+  if (document.querySelector('.admin.new.authentication')) {
+    document.getElementById('auth_type')?.addEventListener('change', function () {
       hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi');
 
-      $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]').removeAttr('required');
+      for (const input of document.querySelectorAll('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) {
+        input.removeAttribute('required');
+      }
+
       $('.binddnrequired').removeClass('required');
 
-      const authType = $(this).val();
+      const authType = this.value;
       switch (authType) {
         case '2': // LDAP
           showElem('.ldap');
-          $('.binddnrequired input, .ldap div.required:not(.dldap) input').attr('required', 'required');
+          for (const input of document.querySelectorAll('.binddnrequired input, .ldap div.required:not(.dldap) input')) {
+            input.setAttribute('required', 'required');
+          }
           $('.binddnrequired').addClass('required');
           break;
         case '3': // SMTP
           showElem('.smtp');
           showElem('.has-tls');
-          $('.smtp div.required input, .has-tls').attr('required', 'required');
+          for (const input of document.querySelectorAll('.smtp div.required input, .has-tls')) {
+            input.setAttribute('required', 'required');
+          }
           break;
         case '4': // PAM
           showElem('.pam');
-          $('.pam input').attr('required', 'required');
+          for (const input of document.querySelectorAll('.pam input')) {
+            input.setAttribute('required', 'required');
+          }
           break;
         case '5': // LDAP
           showElem('.dldap');
-          $('.dldap div.required:not(.ldap) input').attr('required', 'required');
+          for (const input of document.querySelectorAll('.dldap div.required:not(.ldap) input')) {
+            input.setAttribute('required', 'required');
+          }
           break;
         case '6': // OAuth2
           showElem('.oauth2');
-          $('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input').attr('required', 'required');
+          for (const input of document.querySelectorAll('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input')) {
+            input.setAttribute('required', 'required');
+          }
           onOAuth2Change(true);
           break;
         case '7': // SSPI
           showElem('.sspi');
-          $('.sspi div.required input').attr('required', 'required');
+          for (const input of document.querySelectorAll('.sspi div.required input')) {
+            input.setAttribute('required', 'required');
+          }
           break;
       }
       if (authType === '2' || authType === '5') {
@@ -146,44 +175,44 @@ export function initAdminCommon() {
       }
     });
     $('#auth_type').trigger('change');
-    $('#security_protocol').on('change', onSecurityProtocolChange);
-    $('#use_paged_search').on('change', onUsePagedSearchChange);
-    $('#oauth2_provider').on('change', () => onOAuth2Change(true));
-    $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(true));
+    document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange);
+    document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
+    document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+    document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true));
     $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
   }
   // Edit authentication
-  if ($('.admin.edit.authentication').length > 0) {
-    const authType = $('#auth_type').val();
+  if (document.querySelector('.admin.edit.authentication')) {
+    const authType = document.getElementById('auth_type')?.value;
     if (authType === '2' || authType === '5') {
-      $('#security_protocol').on('change', onSecurityProtocolChange);
+      document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange);
       $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
       onEnableLdapGroupsChange();
       if (authType === '2') {
-        $('#use_paged_search').on('change', onUsePagedSearchChange);
+        document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
       }
     } else if (authType === '6') {
-      $('#oauth2_provider').on('change', () => onOAuth2Change(true));
-      $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(false));
+      document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+      document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
       onOAuth2Change(false);
     }
   }
 
-  if ($('.admin.authentication').length > 0) {
+  if (document.querySelector('.admin.authentication')) {
     $('#auth_name').on('input', function () {
       // appSubUrl is either empty or is a path that starts with `/` and doesn't have a trailing slash.
-      $('#oauth2-callback-url').text(`${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent($(this).val())}/callback`);
+      document.getElementById('oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent(this.value)}/callback`;
     }).trigger('input');
   }
 
   // Notice
-  if ($('.admin.notice')) {
-    const $detailModal = $('#detail-modal');
+  if (document.querySelector('.admin.notice')) {
+    const $detailModal = document.getElementById('detail-modal');
 
     // Attach view detail modals
     $('.view-detail').on('click', function () {
       $detailModal.find('.content pre').text($(this).parents('tr').find('.notice-description').text());
-      $detailModal.find('.sub.header').text($(this).parents('tr').find('relative-time').attr('title'));
+      $detailModal.find('.sub.header').text(this.closest('tr')?.querySelector('relative-time')?.getAttribute('title'));
       $detailModal.modal('show');
       return false;
     });
@@ -203,7 +232,7 @@ export function initAdminCommon() {
           break;
       }
     });
-    $('#delete-selection').on('click', async function (e) {
+    document.getElementById('delete-selection')?.addEventListener('click', async function (e) {
       e.preventDefault();
       const $this = $(this);
       $this.addClass('is-loading disabled');

From 0922ce8191ae83834b89b59c5c504209a8a0558e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 27 Mar 2024 12:50:07 +0200
Subject: [PATCH 547/679] Remove jQuery `.attr` from the Fomantic dropdowns
 (#30114)

- Switched from jQuery `attr` to plain javascript `getAttribute` and
`setAttribute`
- Tested the dropdowns and they work as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/modules/fomantic/dropdown.js | 120 +++++++++++++-----------
 1 file changed, 64 insertions(+), 56 deletions(-)

diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
index 97aabb44b6..e795e8e2c8 100644
--- a/web_src/js/modules/fomantic/dropdown.js
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -21,12 +21,11 @@ function ariaDropdownFn(...args) {
   // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
   const needDelegate = (!args.length || typeof args[0] !== 'string');
   for (const el of this) {
-    const $dropdown = $(el);
     if (!el[ariaPatchKey]) {
-      attachInit($dropdown);
+      attachInit(el);
     }
     if (needDelegate) {
-      delegateOne($dropdown);
+      delegateOne($(el));
     }
   }
   return ret;
@@ -40,17 +39,23 @@ function updateMenuItem(dropdown, item) {
   item.setAttribute('tabindex', '-1');
   for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1');
 }
-
-// make the label item and its "delete icon" has correct aria attributes
-function updateSelectionLabel($label) {
+/**
+ * make the label item and its "delete icon" have correct aria attributes
+ * @param {HTMLElement} label
+ */
+function updateSelectionLabel(label) {
   // the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
-  if (!$label.attr('id')) $label.attr('id', generateAriaId());
-  $label.attr('tabindex', '-1');
-  $label.find('.delete.icon').attr({
-    'aria-hidden': 'false',
-    'aria-label': window.config.i18n.remove_label_str.replace('%s', $label.attr('data-value')),
-    'role': 'button',
-  });
+  if (!label.id) {
+    label.id = generateAriaId();
+  }
+  label.tabIndex = -1;
+
+  const deleteIcon = label.querySelector('.delete.icon');
+  if (deleteIcon) {
+    deleteIcon.setAttribute('aria-hidden', 'false');
+    deleteIcon.setAttribute('aria-label', window.config.i18n.remove_label_str.replace('%s', label.getAttribute('data-value')));
+    deleteIcon.setAttribute('role', 'button');
+  }
 }
 
 // delegate the dropdown's template functions and callback functions to add aria attributes.
@@ -86,43 +91,44 @@ function delegateOne($dropdown) {
   const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate');
   dropdownCall('setting', 'onLabelCreate', function(value, text) {
     const $label = dropdownOnLabelCreateOld.call(this, value, text);
-    updateSelectionLabel($label);
+    updateSelectionLabel($label[0]);
     return $label;
   });
 }
 
 // for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes
-function attachStaticElements($dropdown, $focusable, $menu) {
-  const dropdown = $dropdown[0];
-
+function attachStaticElements(dropdown, focusable, menu) {
   // prepare static dropdown menu list popup
-  if (!$menu.attr('id')) $menu.attr('id', generateAriaId());
-  $menu.find('> .item').each((_, item) => updateMenuItem(dropdown, item));
+  if (!menu.id) {
+    menu.id = generateAriaId();
+  }
+
+  $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item));
+
   // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash
-  $menu.attr('role', dropdown[ariaPatchKey].listPopupRole);
+  menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole);
 
   // prepare selection label items
-  $dropdown.find('.ui.label').each((_, label) => updateSelectionLabel($(label)));
+  for (const label of dropdown.querySelectorAll('.ui.label')) {
+    updateSelectionLabel(label);
+  }
 
   // make the primary element (focusable) aria-friendly
-  $focusable.attr({
-    'role': $focusable.attr('role') ?? dropdown[ariaPatchKey].focusableRole,
-    'aria-haspopup': dropdown[ariaPatchKey].listPopupRole,
-    'aria-controls': $menu.attr('id'),
-    'aria-expanded': 'false',
-  });
+  focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole);
+  focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole);
+  focusable.setAttribute('aria-controls', menu.id);
+  focusable.setAttribute('aria-expanded', 'false');
 
   // use tooltip's content as aria-label if there is no aria-label
-  const tooltipContent = $dropdown.attr('data-tooltip-content');
-  if (tooltipContent && !$dropdown.attr('aria-label')) {
-    $dropdown.attr('aria-label', tooltipContent);
+  const tooltipContent = dropdown.getAttribute('data-tooltip-content');
+  if (tooltipContent && !dropdown.getAttribute('aria-label')) {
+    dropdown.setAttribute('aria-label', tooltipContent);
   }
 }
 
-function attachInit($dropdown) {
-  const dropdown = $dropdown[0];
+function attachInit(dropdown) {
   dropdown[ariaPatchKey] = {};
-  if ($dropdown.hasClass('custom')) return;
+  if (dropdown.classList.contains('custom')) return;
 
   // Dropdown has 2 different focusing behaviors
   // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -139,64 +145,66 @@ function attachInit($dropdown) {
 
   // TODO: multiple selection is only partially supported. Check and test them one by one in the future.
 
-  const $textSearch = $dropdown.find('input.search').eq(0);
-  const $focusable = $textSearch.length ? $textSearch : $dropdown; // the primary element for focus, see comment above
-  if (!$focusable.length) return;
+  const textSearch = dropdown.querySelector('input.search');
+  const focusable = textSearch || dropdown; // the primary element for focus, see comment above
+  if (!focusable) return;
 
   // as a combobox, the input should not have autocomplete by default
-  if ($textSearch.length && !$textSearch.attr('autocomplete')) {
-    $textSearch.attr('autocomplete', 'off');
+  if (textSearch && !textSearch.getAttribute('autocomplete')) {
+    textSearch.setAttribute('autocomplete', 'off');
   }
 
-  let $menu = $dropdown.find('> .menu');
-  if (!$menu.length) {
+  let menu = $(dropdown).find('> .menu')[0];
+  if (!menu) {
     // some "multiple selection" dropdowns don't have a static menu element in HTML, we need to pre-create it to make it have correct aria attributes
-    $menu = $('<div class="menu"></div>').appendTo($dropdown);
+    menu = document.createElement('div');
+    menu.classList.add('menu');
+    dropdown.append(menu);
   }
 
   // There are 2 possible solutions about the role: combobox or menu.
   // The idea is that if there is an input, then it's a combobox, otherwise it's a menu.
   // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before.
-  const isComboBox = $dropdown.find('input').length > 0;
+  const isComboBox = dropdown.querySelectorAll('input').length > 0;
 
   dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu';
   dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : '';
   dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem';
 
-  attachDomEvents($dropdown, $focusable, $menu);
-  attachStaticElements($dropdown, $focusable, $menu);
+  attachDomEvents(dropdown, focusable, menu);
+  attachStaticElements(dropdown, focusable, menu);
 }
 
-function attachDomEvents($dropdown, $focusable, $menu) {
-  const dropdown = $dropdown[0];
+function attachDomEvents(dropdown, focusable, menu) {
   // when showing, it has class: ".animating.in"
   // when hiding, it has class: ".visible.animating.out"
-  const isMenuVisible = () => ($menu.hasClass('visible') && !$menu.hasClass('out')) || $menu.hasClass('in');
+  const isMenuVisible = () => (menu.classList.contains('visible') && !menu.classList.contains('out')) || menu.classList.contains('in');
 
   // update aria attributes according to current active/selected item
   const refreshAriaActiveItem = () => {
     const menuVisible = isMenuVisible();
-    $focusable.attr('aria-expanded', menuVisible ? 'true' : 'false');
+    focusable.setAttribute('aria-expanded', menuVisible ? 'true' : 'false');
 
     // if there is an active item, use it (the user is navigating between items)
     // otherwise use the "selected" for combobox (for the last selected item)
-    const $active = $menu.find('> .item.active, > .item.selected');
+    const active = $(menu).find('> .item.active, > .item.selected')[0];
+    if (!active) return;
     // if the popup is visible and has an active/selected item, use its id as aria-activedescendant
     if (menuVisible) {
-      $focusable.attr('aria-activedescendant', $active.attr('id'));
+      focusable.setAttribute('aria-activedescendant', active.id);
     } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') {
       // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item
-      $focusable.removeAttr('aria-activedescendant');
-      $active.removeClass('active').removeClass('selected');
+      focusable.removeAttribute('aria-activedescendant');
+      active.classList.remove('active', 'selected');
     }
   };
 
-  $dropdown.on('keydown', (e) => {
+  dropdown.addEventListener('keydown', (e) => {
     // here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
     if (e.key === 'Enter') {
-      const dropdownCall = fomanticDropdownFn.bind($dropdown);
+      const dropdownCall = fomanticDropdownFn.bind($(dropdown));
       let $item = dropdownCall('get item', dropdownCall('get value'));
-      if (!$item) $item = $menu.find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
+      if (!$item) $item = $(menu).find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
       // if the selected item is clickable, then trigger the click event.
       // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
       if ($item && ($item[0].matches('a') || $item.hasClass('js-aria-clickable'))) $item[0].click();
@@ -209,7 +217,7 @@ function attachDomEvents($dropdown, $focusable, $menu) {
   // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation.
   const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) };
   dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem;
-  $dropdown.on('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); });
+  dropdown.addEventListener('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); });
 
   // if the dropdown has been opened by focus, do not trigger the next click event again.
   // otherwise the dropdown will be closed immediately, especially on Android with TalkBack

From 0262c66ba6c1d7488456269b2e56220bf6cf0b6f Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Wed, 27 Mar 2024 20:48:09 +0800
Subject: [PATCH 548/679] Fix: Organization Interface Display Issue (#30133)

**Before**

![image](https://github.com/go-gitea/gitea/assets/37935145/88d04a4b-6dc5-4399-9813-2c339eae3722)

**After**

![image](https://github.com/go-gitea/gitea/assets/37935145/e97a64b8-ea24-4de7-992d-5928888872d0)
---
 templates/org/home.tmpl | 2 +-
 templates/org/menu.tmpl | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index 1277665804..4851b69979 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -16,7 +16,7 @@
 			{{if .ShowMemberAndTeamTab}}
 			<div class="ui five wide column">
 				{{if .CanCreateOrgRepo}}
-					<div class="center aligned">
+					<div class="center aligned tw-mb-4">
 						<a class="ui primary button" href="{{AppSubUrl}}/repo/create?org={{.Org.ID}}">{{ctx.Locale.Tr "new_repo"}}</a>
 						{{if not .DisableNewPullMirrors}}
 							<a class="ui primary button" href="{{AppSubUrl}}/repo/migrate?org={{.Org.ID}}&mirror=1">{{ctx.Locale.Tr "new_migrate"}}</a>
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index 8eacc17e82..c519606d1f 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -1,5 +1,5 @@
 <div class="ui container">
-	<overflow-menu class="ui secondary pointing tabular borderless menu">
+	<overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4">
 		<div class="overflow-menu-items">
 			<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
 				{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}

From f1707f4562158853552d57394b8b1fea6df645b0 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 27 Mar 2024 21:14:34 +0800
Subject: [PATCH 549/679] Refactor render (#30136)

---
 routers/web/repo/render.go | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index 10fa21c60e..e64db03e20 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/modules/charset"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
@@ -44,20 +45,17 @@ func RenderFile(ctx *context.Context) {
 	isTextFile := st.IsText()
 
 	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
+	ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
 
 	if markupType := markup.Type(blob.Name()); markupType == "" {
 		if isTextFile {
-			_, err = io.Copy(ctx.Resp, rd)
-			if err != nil {
-				ctx.ServerError("Copy", err)
-			}
-			return
+			_, _ = io.Copy(ctx.Resp, rd)
+		} else {
+			http.Error(ctx.Resp, "Unsupported file type render", http.StatusInternalServerError)
 		}
-		ctx.Error(http.StatusInternalServerError, "Unsupported file type render")
 		return
 	}
 
-	ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
 	err = markup.Render(&markup.RenderContext{
 		Ctx:          ctx,
 		RelativePath: ctx.Repo.TreePath,
@@ -71,7 +69,8 @@ func RenderFile(ctx *context.Context) {
 		InStandalonePage: true,
 	}, rd, ctx.Resp)
 	if err != nil {
-		ctx.ServerError("Render", err)
+		log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err)
+		http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError)
 		return
 	}
 }

From 34acd8e3767ec0898f90a74b64ac738d0ce05f0a Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 27 Mar 2024 15:49:54 +0200
Subject: [PATCH 550/679] Forbid jQuery `.attr` (#30116)

Use `.getAttribute`, `.setAttribute`, or `.removeAttribute` instead

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 .eslintrc.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 50b3ca05a0..99ce2e97d6 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -281,7 +281,7 @@ rules:
   jquery/no-ajax-events: [2]
   jquery/no-ajax: [2]
   jquery/no-animate: [2]
-  jquery/no-attr: [0]
+  jquery/no-attr: [2]
   jquery/no-bind: [2]
   jquery/no-class: [0]
   jquery/no-clone: [2]
@@ -397,7 +397,7 @@ rules:
   no-jquery/no-animate-toggle: [2]
   no-jquery/no-animate: [2]
   no-jquery/no-append-html: [2]
-  no-jquery/no-attr: [0]
+  no-jquery/no-attr: [2]
   no-jquery/no-bind: [2]
   no-jquery/no-box-model: [2]
   no-jquery/no-browser: [2]

From 1a71dbfb7881f65d39b689a5be26cc94afefb10f Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 27 Mar 2024 18:09:34 +0200
Subject: [PATCH 551/679] Remove jQuery class from the reaction selector
 (#30138)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the reaction selector and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/comp/ReactionSelector.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js
index fc966c3985..2def3db51a 100644
--- a/web_src/js/features/comp/ReactionSelector.js
+++ b/web_src/js/features/comp/ReactionSelector.js
@@ -5,7 +5,7 @@ export function initCompReactionSelector($parent) {
   $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) {
     e.preventDefault();
 
-    if ($(this).hasClass('disabled')) return;
+    if (this.classList.contains('disabled')) return;
 
     const actionUrl = this.closest('[data-action-url]')?.getAttribute('data-action-url');
     const reactionContent = this.getAttribute('data-reaction-content');

From 1551d73d3f95284965675b828e1eeceafa378437 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 27 Mar 2024 18:14:18 +0200
Subject: [PATCH 552/679] Remove jQuery class from the common admin functions
 (#30137)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the new authentication source form and the deletion of system
notices. They work as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/admin/common.js | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index ac8bfe8b34..8a88996742 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -122,7 +122,7 @@ export function initAdminCommon() {
         input.removeAttribute('required');
       }
 
-      $('.binddnrequired').removeClass('required');
+      document.querySelector('.binddnrequired')?.classList.remove('required');
 
       const authType = this.value;
       switch (authType) {
@@ -131,7 +131,7 @@ export function initAdminCommon() {
           for (const input of document.querySelectorAll('.binddnrequired input, .ldap div.required:not(.dldap) input')) {
             input.setAttribute('required', 'required');
           }
-          $('.binddnrequired').addClass('required');
+          document.querySelector('.binddnrequired')?.classList.add('required');
           break;
         case '3': // SMTP
           showElem('.smtp');
@@ -234,16 +234,15 @@ export function initAdminCommon() {
     });
     document.getElementById('delete-selection')?.addEventListener('click', async function (e) {
       e.preventDefault();
-      const $this = $(this);
-      $this.addClass('is-loading disabled');
+      this.classList.add('is-loading', 'disabled');
       const data = new FormData();
       $checkboxes.each(function () {
         if ($(this).checkbox('is checked')) {
-          data.append('ids[]', $(this).data('id'));
+          data.append('ids[]', this.getAttribute('data-id'));
         }
       });
-      await POST($this.data('link'), {data});
-      window.location.href = $this.data('redirect');
+      await POST(this.getAttribute('data-link'), {data});
+      window.location.href = this.getAttribute('data-redirect');
     });
   }
 }

From 1ad48f781eb0681561b083b49dfeff84ba51f2fe Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 28 Mar 2024 00:55:05 +0800
Subject: [PATCH 553/679] Relax generic package filename restrictions (#30135)

Now, the chars `=:;()[]{}~!@#$%^ &` are possible as well
Fixes #30134

---------

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 routers/api/packages/generic/generic.go       | 29 +++++++--
 routers/api/packages/generic/generic_test.go  | 65 +++++++++++++++++++
 .../integration/api_packages_generic_test.go  |  4 +-
 3 files changed, 91 insertions(+), 7 deletions(-)
 create mode 100644 routers/api/packages/generic/generic_test.go

diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go
index b65870a8d0..8232931134 100644
--- a/routers/api/packages/generic/generic.go
+++ b/routers/api/packages/generic/generic.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"regexp"
 	"strings"
+	"unicode"
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/modules/log"
@@ -18,8 +19,8 @@ import (
 )
 
 var (
-	packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`)
-	filenameRegex    = packageNameRegex
+	packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
+	filenameRegex    = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
 )
 
 func apiError(ctx *context.Context, status int, obj any) {
@@ -54,20 +55,38 @@ func DownloadPackageFile(ctx *context.Context) {
 	helper.ServePackageFile(ctx, s, u, pf)
 }
 
+func isValidPackageName(packageName string) bool {
+	if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
+		return false
+	}
+	return packageNameRegex.MatchString(packageName) && packageName != ".."
+}
+
+func isValidFileName(filename string) bool {
+	return filenameRegex.MatchString(filename) &&
+		strings.TrimSpace(filename) == filename &&
+		filename != "." && filename != ".."
+}
+
 // UploadPackage uploads the specific generic package.
 // Duplicated packages get rejected.
 func UploadPackage(ctx *context.Context) {
 	packageName := ctx.Params("packagename")
 	filename := ctx.Params("filename")
 
-	if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) {
-		apiError(ctx, http.StatusBadRequest, errors.New("Invalid package name or filename"))
+	if !isValidPackageName(packageName) {
+		apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
+		return
+	}
+
+	if !isValidFileName(filename) {
+		apiError(ctx, http.StatusBadRequest, errors.New("invalid filename"))
 		return
 	}
 
 	packageVersion := ctx.Params("packageversion")
 	if packageVersion != strings.TrimSpace(packageVersion) {
-		apiError(ctx, http.StatusBadRequest, errors.New("Invalid package version"))
+		apiError(ctx, http.StatusBadRequest, errors.New("invalid package version"))
 		return
 	}
 
diff --git a/routers/api/packages/generic/generic_test.go b/routers/api/packages/generic/generic_test.go
new file mode 100644
index 0000000000..1acaafe576
--- /dev/null
+++ b/routers/api/packages/generic/generic_test.go
@@ -0,0 +1,65 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package generic
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidatePackageName(t *testing.T) {
+	bad := []string{
+		"",
+		".",
+		"..",
+		"-",
+		"a?b",
+		"a b",
+		"a/b",
+	}
+	for _, name := range bad {
+		assert.False(t, isValidPackageName(name), "bad=%q", name)
+	}
+
+	good := []string{
+		"a",
+		"1",
+		"a-",
+		"a_b",
+		"c.d+",
+	}
+	for _, name := range good {
+		assert.True(t, isValidPackageName(name), "good=%q", name)
+	}
+}
+
+func TestValidateFileName(t *testing.T) {
+	bad := []string{
+		"",
+		".",
+		"..",
+		"a?b",
+		"a/b",
+		" a",
+		"a ",
+	}
+	for _, name := range bad {
+		assert.False(t, isValidFileName(name), "bad=%q", name)
+	}
+
+	good := []string{
+		"-",
+		"a",
+		"1",
+		"a-",
+		"a_b",
+		"a b",
+		"c.d+",
+		`-_+=:;.()[]{}~!@#$%^& aA1`,
+	}
+	for _, name := range good {
+		assert.True(t, isValidFileName(name), "good=%q", name)
+	}
+}
diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go
index 93525ac4b1..1cbae599af 100644
--- a/tests/integration/api_packages_generic_test.go
+++ b/tests/integration/api_packages_generic_test.go
@@ -84,7 +84,7 @@ func TestPackageGeneric(t *testing.T) {
 		t.Run("InvalidParameter", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
 
-			req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid+package name", packageVersion, filename), bytes.NewReader(content)).
+			req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid package name", packageVersion, filename), bytes.NewReader(content)).
 				AddBasicAuth(user.Name)
 			MakeRequest(t, req, http.StatusBadRequest)
 
@@ -92,7 +92,7 @@ func TestPackageGeneric(t *testing.T) {
 				AddBasicAuth(user.Name)
 			MakeRequest(t, req, http.StatusBadRequest)
 
-			req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inval+id.na me"), bytes.NewReader(content)).
+			req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inva|id.name"), bytes.NewReader(content)).
 				AddBasicAuth(user.Name)
 			MakeRequest(t, req, http.StatusBadRequest)
 		})

From c85619b82d19a928cb219eba3f38473928b29b0c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 27 Mar 2024 21:05:49 +0100
Subject: [PATCH 554/679] Fix download buttons on branches page (#30147)

Fixes https://github.com/go-gitea/gitea/issues/30143, regression from
https://github.com/go-gitea/gitea/pull/29920.

We have `.button` on the repo page, but on the branch page it's a
`.btn`. Eventually we should find a solution to have a single button
class but until then this solution should be acceptable.
---
 web_src/css/modules/animations.css | 1 +
 web_src/js/features/repo-common.js | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index 5bfc090773..788a4ed6ed 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -13,6 +13,7 @@
   opacity: 0.3;
 }
 
+.btn.is-loading > *,
 .button.is-loading > * {
   opacity: 0;
 }
diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js
index 2c5746c738..b750addb07 100644
--- a/web_src/js/features/repo-common.js
+++ b/web_src/js/features/repo-common.js
@@ -3,7 +3,7 @@ import {hideElem, showElem} from '../utils/dom.js';
 import {POST} from '../modules/fetch.js';
 
 async function getArchive($target, url, first) {
-  const dropdownBtn = $target[0].closest('.ui.dropdown.button');
+  const dropdownBtn = $target[0].closest('.ui.dropdown.button') ?? $target[0].closest('.ui.dropdown.btn');
 
   try {
     dropdownBtn.classList.add('is-loading');

From 4eb86d68233241d53cff1009ecff17ac35efccd4 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 27 Mar 2024 21:18:04 +0100
Subject: [PATCH 555/679] Fix loading spinner on ContextPopup (#30145)

Fix regression from https://github.com/go-gitea/gitea/pull/26670. Here
with simulated delay:


![](https://github.com/go-gitea/gitea/assets/115237/9de5a136-c8a6-4d69-adc7-07e1184e3311)
---
 web_src/js/components/ContextPopup.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue
index 149cabd41e..d87eb1a180 100644
--- a/web_src/js/components/ContextPopup.vue
+++ b/web_src/js/components/ContextPopup.vue
@@ -103,7 +103,7 @@ export default {
 </script>
 <template>
   <div ref="root">
-    <div v-if="loading" class="ui active centered inline loader"/>
+    <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
     <div v-if="!loading && issue !== null">
       <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
       <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>

From e5160185ed65fd1c2bcb2fc7dc7e0b5514ddb299 Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Wed, 27 Mar 2024 21:54:32 +0100
Subject: [PATCH 556/679] Add default board to new projects, remove
 uncategorized pseudo-board (#29874)

On creation of an empty project (no template) a default board will be
created instead of falling back to the uneditable pseudo-board.

Every project now has to have exactly one default boards. As a
consequence, you cannot unset a board as default, instead you have to
set another board as default. Existing projects will be modified using a
cron job, additionally this check will run every midnight by default.

Deleting the default board is not allowed, you have to set another board
as default to do it.

Fixes #29873
Fixes #14679 along the way
Fixes #29853

Co-authored-by: delvh <dev.lh@web.de>
---
 models/fixtures/project.yml                   |  24 ++++
 models/fixtures/project_board.yml             |  46 ++++++++
 models/issues/issue_project.go                |  19 ++-
 .../project.yml                               |  23 ++++
 .../project_board.yml                         |  26 +++++
 models/migrations/migrations.go               |   2 +
 models/migrations/v1_22/v292.go               |  85 ++++++++++++++
 models/migrations/v1_22/v292_test.go          |  44 +++++++
 models/project/board.go                       |  76 ++++++++----
 models/project/board_test.go                  |  40 +++++++
 models/project/project_test.go                |  12 +-
 options/locale/locale_en-US.ini               |   5 +-
 routers/web/org/projects.go                   | 108 +++---------------
 routers/web/repo/projects.go                  |  52 ++-------
 routers/web/web.go                            |   2 -
 templates/projects/view.tmpl                  |  31 ++---
 web_src/js/features/repo-projects.js          |   1 -
 17 files changed, 400 insertions(+), 196 deletions(-)
 create mode 100644 models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml
 create mode 100644 models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml
 create mode 100644 models/migrations/v1_22/v292.go
 create mode 100644 models/migrations/v1_22/v292_test.go
 create mode 100644 models/project/board_test.go

diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml
index 1bf8030f6a..44d87bce04 100644
--- a/models/fixtures/project.yml
+++ b/models/fixtures/project.yml
@@ -45,3 +45,27 @@
   type: 2
   created_unix: 1688973000
   updated_unix: 1688973000
+
+-
+  id: 5
+  title: project without default column
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
+
+-
+  id: 6
+  title: project with multiple default columns
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
diff --git a/models/fixtures/project_board.yml b/models/fixtures/project_board.yml
index dc4f9cf565..3293dea6ed 100644
--- a/models/fixtures/project_board.yml
+++ b/models/fixtures/project_board.yml
@@ -3,6 +3,7 @@
   project_id: 1
   title: To Do
   creator_id: 2
+  default: true
   created_unix: 1588117528
   updated_unix: 1588117528
 
@@ -29,3 +30,48 @@
   creator_id: 2
   created_unix: 1588117528
   updated_unix: 1588117528
+
+-
+  id: 5
+  project_id: 2
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 6
+  project_id: 4
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 7
+  project_id: 5
+  title: Done
+  creator_id: 2
+  default: false
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 8
+  project_id: 6
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 9
+  project_id: 6
+  title: Uncategorized
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index cc7ffb356a..907a5a17b9 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -49,18 +49,13 @@ func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
 
 // LoadIssuesFromBoard load issues assigned to this board
 func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
-	issueList := make(IssueList, 0, 10)
-
-	if b.ID > 0 {
-		issues, err := Issues(ctx, &IssuesOptions{
-			ProjectBoardID: b.ID,
-			ProjectID:      b.ProjectID,
-			SortType:       "project-column-sorting",
-		})
-		if err != nil {
-			return nil, err
-		}
-		issueList = issues
+	issueList, err := Issues(ctx, &IssuesOptions{
+		ProjectBoardID: b.ID,
+		ProjectID:      b.ProjectID,
+		SortType:       "project-column-sorting",
+	})
+	if err != nil {
+		return nil, err
 	}
 
 	if b.Default {
diff --git a/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml
new file mode 100644
index 0000000000..2450d20beb
--- /dev/null
+++ b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml
@@ -0,0 +1,23 @@
+-
+  id: 1
+  title: project without default column
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
+
+-
+  id: 2
+  title: project with multiple default columns
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
diff --git a/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml
new file mode 100644
index 0000000000..2e1b1c7eee
--- /dev/null
+++ b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml
@@ -0,0 +1,26 @@
+-
+  id: 1
+  project_id: 1
+  title: Done
+  creator_id: 2
+  default: false
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 2
+  project_id: 2
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 3
+  project_id: 2
+  title: Uncategorized
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 87fddefb88..77895fba61 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -568,6 +568,8 @@ var migrations = []Migration{
 	NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
 	// v291 -> v292
 	NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
+	// v292 -> v293
+	NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v292.go b/models/migrations/v1_22/v292.go
new file mode 100644
index 0000000000..7c051a2b75
--- /dev/null
+++ b/models/migrations/v1_22/v292.go
@@ -0,0 +1,85 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"code.gitea.io/gitea/models/project"
+	"code.gitea.io/gitea/modules/setting"
+
+	"xorm.io/builder"
+	"xorm.io/xorm"
+)
+
+// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
+func CheckProjectColumnsConsistency(x *xorm.Engine) error {
+	sess := x.NewSession()
+	defer sess.Close()
+
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+
+	limit := setting.Database.IterateBufferSize
+	if limit <= 0 {
+		limit = 50
+	}
+
+	start := 0
+
+	for {
+		var projects []project.Project
+		if err := sess.SQL("SELECT DISTINCT `p`.`id`, `p`.`creator_id` FROM `project` `p` WHERE (SELECT COUNT(*) FROM `project_board` `pb` WHERE `pb`.`project_id` = `p`.`id` AND `pb`.`default` = ?) != 1", true).
+			Limit(limit, start).
+			Find(&projects); err != nil {
+			return err
+		}
+
+		if len(projects) == 0 {
+			break
+		}
+		start += len(projects)
+
+		for _, p := range projects {
+			var boards []project.Board
+			if err := sess.Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
+				return err
+			}
+
+			if len(boards) == 0 {
+				if _, err := sess.Insert(project.Board{
+					ProjectID: p.ID,
+					Default:   true,
+					Title:     "Uncategorized",
+					CreatorID: p.CreatorID,
+				}); err != nil {
+					return err
+				}
+				continue
+			}
+
+			var boardsToUpdate []int64
+			for id, b := range boards {
+				if id > 0 {
+					boardsToUpdate = append(boardsToUpdate, b.ID)
+				}
+			}
+
+			if _, err := sess.Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
+				Cols("`default`").Update(&project.Board{Default: false}); err != nil {
+				return err
+			}
+		}
+
+		if start%1000 == 0 {
+			if err := sess.Commit(); err != nil {
+				return err
+			}
+			if err := sess.Begin(); err != nil {
+				return err
+			}
+		}
+	}
+
+	return sess.Commit()
+}
diff --git a/models/migrations/v1_22/v292_test.go b/models/migrations/v1_22/v292_test.go
new file mode 100644
index 0000000000..5e32e0220f
--- /dev/null
+++ b/models/migrations/v1_22/v292_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/migrations/base"
+	"code.gitea.io/gitea/models/project"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_CheckProjectColumnsConsistency(t *testing.T) {
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	assert.NoError(t, CheckProjectColumnsConsistency(x))
+
+	// check if default board was added
+	var defaultBoard project.Board
+	has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard)
+	assert.NoError(t, err)
+	assert.True(t, has)
+	assert.Equal(t, int64(1), defaultBoard.ProjectID)
+	assert.True(t, defaultBoard.Default)
+
+	// check if multiple defaults were removed
+	expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
+	assert.True(t, expectDefaultBoard.Default)
+
+	expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
+	assert.False(t, expectNonDefaultBoard.Default)
+}
diff --git a/models/project/board.go b/models/project/board.go
index c0e6529880..5605f259b5 100644
--- a/models/project/board.go
+++ b/models/project/board.go
@@ -123,6 +123,17 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
 		return nil
 	}
 
+	board := Board{
+		CreatedUnix: timeutil.TimeStampNow(),
+		CreatorID:   project.CreatorID,
+		Title:       "Backlog",
+		ProjectID:   project.ID,
+		Default:     true,
+	}
+	if err := db.Insert(ctx, board); err != nil {
+		return err
+	}
+
 	if len(items) == 0 {
 		return nil
 	}
@@ -176,6 +187,10 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
 		return err
 	}
 
+	if board.Default {
+		return fmt.Errorf("deleteBoardByID: cannot delete default board")
+	}
+
 	if err = board.removeIssues(ctx); err != nil {
 		return err
 	}
@@ -228,7 +243,6 @@ func UpdateBoard(ctx context.Context, board *Board) error {
 }
 
 // GetBoards fetches all boards related to a project
-// if no default board set, first board is a temporary "Uncategorized" board
 func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
 	boards := make([]*Board, 0, 5)
 
@@ -244,41 +258,61 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
 	return append([]*Board{defaultB}, boards...), nil
 }
 
-// getDefaultBoard return default board and create a dummy if none exist
+// getDefaultBoard return default board and ensure only one exists
 func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
-	var board Board
-	exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board)
-	if err != nil {
+	var boards []Board
+	if err := db.GetEngine(ctx).Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
 		return nil, err
 	}
-	if exist {
+
+	// create a default board if none is found
+	if len(boards) == 0 {
+		board := Board{
+			ProjectID: p.ID,
+			Default:   true,
+			Title:     "Uncategorized",
+			CreatorID: p.CreatorID,
+		}
+		if _, err := db.GetEngine(ctx).Insert(); err != nil {
+			return nil, err
+		}
 		return &board, nil
 	}
 
-	// represents a board for issues not assigned to one
-	return &Board{
-		ProjectID: p.ID,
-		Title:     "Uncategorized",
-		Default:   true,
-	}, nil
+	// unset default boards where too many default boards exist
+	if len(boards) > 1 {
+		var boardsToUpdate []int64
+		for id, b := range boards {
+			if id > 0 {
+				boardsToUpdate = append(boardsToUpdate, b.ID)
+			}
+		}
+
+		if _, err := db.GetEngine(ctx).Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
+			Cols("`default`").Update(&Board{Default: false}); err != nil {
+			return nil, err
+		}
+	}
+
+	return &boards[0], nil
 }
 
 // SetDefaultBoard represents a board for issues not assigned to one
-// if boardID is 0 unset default
 func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
-	_, err := db.GetEngine(ctx).Where(builder.Eq{
-		"project_id": projectID,
-		"`default`":  true,
-	}).Cols("`default`").Update(&Board{Default: false})
-	if err != nil {
+	if _, err := GetBoard(ctx, boardID); err != nil {
 		return err
 	}
 
-	if boardID > 0 {
-		_, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}).
-			Cols("`default`").Update(&Board{Default: true})
+	if _, err := db.GetEngine(ctx).Where(builder.Eq{
+		"project_id": projectID,
+		"`default`":  true,
+	}).Cols("`default`").Update(&Board{Default: false}); err != nil {
+		return err
 	}
 
+	_, err := db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}).
+		Cols("`default`").Update(&Board{Default: true})
+
 	return err
 }
 
diff --git a/models/project/board_test.go b/models/project/board_test.go
new file mode 100644
index 0000000000..c1c6f0180b
--- /dev/null
+++ b/models/project/board_test.go
@@ -0,0 +1,40 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package project
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetDefaultBoard(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
+	assert.NoError(t, err)
+
+	// check if default board was added
+	board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(5), board.ProjectID)
+	assert.Equal(t, "Uncategorized", board.Title)
+
+	projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
+	assert.NoError(t, err)
+
+	// check if multiple defaults were removed
+	board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(6), board.ProjectID)
+	assert.Equal(t, int64(8), board.ID)
+
+	board, err = GetBoard(db.DefaultContext, 9)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(6), board.ProjectID)
+	assert.False(t, board.Default)
+}
diff --git a/models/project/project_test.go b/models/project/project_test.go
index 7a37c1faf2..8fbbdedecf 100644
--- a/models/project/project_test.go
+++ b/models/project/project_test.go
@@ -92,19 +92,19 @@ func TestProjectsSort(t *testing.T) {
 	}{
 		{
 			sortType: "default",
-			wants:    []int64{1, 3, 2, 4},
+			wants:    []int64{1, 3, 2, 6, 5, 4},
 		},
 		{
 			sortType: "oldest",
-			wants:    []int64{4, 2, 3, 1},
+			wants:    []int64{4, 5, 6, 2, 3, 1},
 		},
 		{
 			sortType: "recentupdate",
-			wants:    []int64{1, 3, 2, 4},
+			wants:    []int64{1, 3, 2, 6, 5, 4},
 		},
 		{
 			sortType: "leastupdate",
-			wants:    []int64{4, 2, 3, 1},
+			wants:    []int64{4, 5, 6, 2, 3, 1},
 		},
 	}
 
@@ -113,8 +113,8 @@ func TestProjectsSort(t *testing.T) {
 			OrderBy: GetSearchOrderByBySortType(tt.sortType),
 		})
 		assert.NoError(t, err)
-		assert.EqualValues(t, int64(4), count)
-		if assert.Len(t, projects, 4) {
+		assert.EqualValues(t, int64(6), count)
+		if assert.Len(t, projects, 6) {
 			for i := range projects {
 				assert.EqualValues(t, tt.wants[i], projects[i].ID)
 			}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 07082f99ae..b7bcf20d30 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1392,7 +1392,6 @@ projects.type.basic_kanban = "Basic Kanban"
 projects.type.bug_triage = "Bug Triage"
 projects.template.desc = "Template"
 projects.template.desc_helper = "Select a project template to get started"
-projects.type.uncategorized = Uncategorized
 projects.column.edit = "Edit Column"
 projects.column.edit_title = "Name"
 projects.column.new_title = "Name"
@@ -1400,10 +1399,8 @@ projects.column.new_submit = "Create Column"
 projects.column.new = "New Column"
 projects.column.set_default = "Set Default"
 projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls"
-projects.column.unset_default = "Unset Default"
-projects.column.unset_default_desc = "Unset this column as default"
 projects.column.delete = "Delete Column"
-projects.column.deletion_desc = "Deleting a project column moves all related issues to 'Uncategorized'. Continue?"
+projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?"
 projects.column.color = "Color"
 projects.open = Open
 projects.close = Close
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 928676a52b..596a370d2e 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -207,11 +207,7 @@ func ChangeProjectStatus(ctx *context.Context) {
 	id := ctx.ParamsInt64(":id")
 
 	if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", err)
-		} else {
-			ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err)
-		}
+		ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action")))
@@ -221,11 +217,7 @@ func ChangeProjectStatus(ctx *context.Context) {
 func DeleteProject(ctx *context.Context) {
 	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if p.OwnerID != ctx.ContextUser.ID {
@@ -254,11 +246,7 @@ func RenderEditProject(ctx *context.Context) {
 
 	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if p.OwnerID != ctx.ContextUser.ID {
@@ -303,11 +291,7 @@ func EditProjectPost(ctx *context.Context) {
 
 	p, err := project_model.GetProjectByID(ctx, projectID)
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if p.OwnerID != ctx.ContextUser.ID {
@@ -335,11 +319,7 @@ func EditProjectPost(ctx *context.Context) {
 func ViewProject(ctx *context.Context) {
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if project.OwnerID != ctx.ContextUser.ID {
@@ -353,10 +333,6 @@ func ViewProject(ctx *context.Context) {
 		return
 	}
 
-	if boards[0].ID == 0 {
-		boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
-	}
-
 	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
 	if err != nil {
 		ctx.ServerError("LoadIssuesOfBoards", err)
@@ -493,11 +469,7 @@ func DeleteProjectBoard(ctx *context.Context) {
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 
@@ -534,11 +506,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 
@@ -566,11 +534,7 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return nil, nil
 	}
 
@@ -636,21 +600,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
 	ctx.JSONOK()
 }
 
-// UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls
-func UnsetDefaultProjectBoard(ctx *context.Context) {
-	project, _ := CheckProjectBoardChangePermissions(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
-		ctx.ServerError("SetDefaultBoard", err)
-		return
-	}
-
-	ctx.JSONOK()
-}
-
 // MoveIssues moves or keeps issues in a column and sorts them inside that column
 func MoveIssues(ctx *context.Context) {
 	if ctx.Doer == nil {
@@ -662,11 +611,7 @@ func MoveIssues(ctx *context.Context) {
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("ProjectNotExist", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if project.OwnerID != ctx.ContextUser.ID {
@@ -674,28 +619,15 @@ func MoveIssues(ctx *context.Context) {
 		return
 	}
 
-	var board *project_model.Board
+	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	if err != nil {
+		ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
+		return
+	}
 
-	if ctx.ParamsInt64(":boardID") == 0 {
-		board = &project_model.Board{
-			ID:        0,
-			ProjectID: project.ID,
-			Title:     ctx.Locale.TrString("repo.projects.type.uncategorized"),
-		}
-	} else {
-		board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
-		if err != nil {
-			if project_model.IsErrProjectBoardNotExist(err) {
-				ctx.NotFound("ProjectBoardNotExist", nil)
-			} else {
-				ctx.ServerError("GetProjectBoard", err)
-			}
-			return
-		}
-		if board.ProjectID != project.ID {
-			ctx.NotFound("BoardNotInProject", nil)
-			return
-		}
+	if board.ProjectID != project.ID {
+		ctx.NotFound("BoardNotInProject", nil)
+		return
 	}
 
 	type movedIssuesForm struct {
@@ -718,11 +650,7 @@ func MoveIssues(ctx *context.Context) {
 	}
 	movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
 	if err != nil {
-		if issues_model.IsErrIssueNotExist(err) {
-			ctx.NotFound("IssueNotExisting", nil)
-		} else {
-			ctx.ServerError("GetIssueByID", err)
-		}
+		ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
 		return
 	}
 
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 2cba5c0970..a2db1fc770 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -315,10 +315,6 @@ func ViewProject(ctx *context.Context) {
 		return
 	}
 
-	if boards[0].ID == 0 {
-		boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
-	}
-
 	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
 	if err != nil {
 		ctx.ServerError("LoadIssuesOfBoards", err)
@@ -583,21 +579,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
 	ctx.JSONOK()
 }
 
-// UnSetDefaultProjectBoard unset default board for uncategorized issues/pulls
-func UnSetDefaultProjectBoard(ctx *context.Context) {
-	project, _ := checkProjectBoardChangePermissions(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
-		ctx.ServerError("SetDefaultBoard", err)
-		return
-	}
-
-	ctx.JSONOK()
-}
-
 // MoveIssues moves or keeps issues in a column and sorts them inside that column
 func MoveIssues(ctx *context.Context) {
 	if ctx.Doer == nil {
@@ -628,28 +609,19 @@ func MoveIssues(ctx *context.Context) {
 		return
 	}
 
-	var board *project_model.Board
+	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	if err != nil {
+		if project_model.IsErrProjectBoardNotExist(err) {
+			ctx.NotFound("ProjectBoardNotExist", nil)
+		} else {
+			ctx.ServerError("GetProjectBoard", err)
+		}
+		return
+	}
 
-	if ctx.ParamsInt64(":boardID") == 0 {
-		board = &project_model.Board{
-			ID:        0,
-			ProjectID: project.ID,
-			Title:     ctx.Locale.TrString("repo.projects.type.uncategorized"),
-		}
-	} else {
-		board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
-		if err != nil {
-			if project_model.IsErrProjectBoardNotExist(err) {
-				ctx.NotFound("ProjectBoardNotExist", nil)
-			} else {
-				ctx.ServerError("GetProjectBoard", err)
-			}
-			return
-		}
-		if board.ProjectID != project.ID {
-			ctx.NotFound("BoardNotInProject", nil)
-			return
-		}
+	if board.ProjectID != project.ID {
+		ctx.NotFound("BoardNotInProject", nil)
+		return
 	}
 
 	type movedIssuesForm struct {
diff --git a/routers/web/web.go b/routers/web/web.go
index 3d790d1621..4fff994e42 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1008,7 +1008,6 @@ func registerRoutes(m *web.Route) {
 						m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
 						m.Delete("", org.DeleteProjectBoard)
 						m.Post("/default", org.SetDefaultProjectBoard)
-						m.Post("/unsetdefault", org.UnsetDefaultProjectBoard)
 
 						m.Post("/move", org.MoveIssues)
 					})
@@ -1348,7 +1347,6 @@ func registerRoutes(m *web.Route) {
 						m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
 						m.Delete("", repo.DeleteProjectBoard)
 						m.Post("/default", repo.SetDefaultProjectBoard)
-						m.Post("/unsetdefault", repo.UnSetDefaultProjectBoard)
 
 						m.Post("/move", repo.MoveIssues)
 					})
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index ba5cbc3b45..e6c6c20497 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -74,7 +74,7 @@
 						</div>
 						{{.Title}}
 					</div>
-					{{if and $canWriteProject (ne .ID 0)}}
+					{{if $canWriteProject}}
 						<div class="ui dropdown jump item">
 							<div class="tw-px-2">
 								{{svg "octicon-kebab-horizontal"}}
@@ -86,29 +86,20 @@
 								</a>
 								{{if not .Default}}
 									<a class="item show-modal button default-project-column-show"
-									data-modal="#default-project-column-modal-{{.ID}}"
-									data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
-									data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
-									data-url="{{$.Link}}/{{.ID}}/default">
+										data-modal="#default-project-column-modal-{{.ID}}"
+										data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
+										data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
+										data-url="{{$.Link}}/{{.ID}}/default">
 										{{svg "octicon-pin"}}
 										{{ctx.Locale.Tr "repo.projects.column.set_default"}}
 									</a>
-								{{else}}
-									<a class="item show-modal button default-project-column-show"
-									data-modal="#default-project-column-modal-{{.ID}}"
-									data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}"
-									data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}"
-									data-url="{{$.Link}}/{{.ID}}/unsetdefault">
-										{{svg "octicon-pin-slash"}}
-										{{ctx.Locale.Tr "repo.projects.column.unset_default"}}
+									<a class="item show-modal button show-delete-project-column-modal"
+										data-modal="#delete-project-column-modal-{{.ID}}"
+										data-url="{{$.Link}}/{{.ID}}">
+										{{svg "octicon-trash"}}
+										{{ctx.Locale.Tr "repo.projects.column.delete"}}
 									</a>
 								{{end}}
-								<a class="item show-modal button show-delete-project-column-modal"
-									data-modal="#delete-project-column-modal-{{.ID}}"
-									data-url="{{$.Link}}/{{.ID}}">
-									{{svg "octicon-trash"}}
-									{{ctx.Locale.Tr "repo.projects.column.delete"}}
-								</a>
 
 								<div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}">
 									<div class="header">
@@ -165,7 +156,7 @@
 
 				<div class="divider"></div>
 
-				<div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
+				<div class="ui cards{{if $canWriteProject}} tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
 					{{range (index $.IssuesMap .ID)}}
 						<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
 							{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index fc688bb695..1747cb2b3a 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -58,7 +58,6 @@ async function initRepoProjectSortable() {
   createSortable(mainBoard, {
     group: 'project-column',
     draggable: '.project-column',
-    filter: '[data-id="0"]',
     animation: 150,
     ghostClass: 'card-ghost',
     delayOnTouchOnly: true,

From b08c7afe5f60075ed62a5ffe034b88624983d007 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 27 Mar 2024 22:47:40 +0100
Subject: [PATCH 557/679] Fix table alignment classes (#30144)

Fixes https://github.com/go-gitea/gitea/issues/30142, regression from
https://github.com/go-gitea/gitea/pull/30047. I searched the codebase
and only `bottom aligned` was definitely not in use so I removed it.
---
 web_src/css/modules/table.css | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css
index eabca31a17..4fb9d4214e 100644
--- a/web_src/css/modules/table.css
+++ b/web_src/css/modules/table.css
@@ -152,6 +152,31 @@
   }
 }
 
+.ui.table[class*="left aligned"],
+.ui.table [class*="left aligned"] {
+  text-align: left;
+}
+
+.ui.table[class*="center aligned"],
+.ui.table [class*="center aligned"] {
+  text-align: center;
+}
+
+.ui.table[class*="right aligned"],
+.ui.table [class*="right aligned"] {
+  text-align: right;
+}
+
+.ui.table[class*="top aligned"],
+.ui.table [class*="top aligned"] {
+  vertical-align: top;
+}
+
+.ui.table[class*="middle aligned"],
+.ui.table [class*="middle aligned"] {
+  vertical-align: middle;
+}
+
 .ui.table th.collapsing,
 .ui.table td.collapsing {
   width: 1px;

From 7fda109aba6cd077343edef086b2f2ff60124f78 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 28 Mar 2024 00:20:38 +0100
Subject: [PATCH 558/679] Drag-and-drop improvements for projects and issue
 pins (#29875)

1. Add "grabbing" cursor while dragging items:


![](https://github.com/go-gitea/gitea/assets/115237/c60845ff-7544-4215-aeaa-408e8c4ef03a)

2. Make project board only drag via their header, not via their whole
body.


![](https://github.com/go-gitea/gitea/assets/115237/62c27f3d-993a-481d-9cc3-b6226b4c5d61)

3. Fix some cursor problems in projects
4. Move shared options into `createSortable`.
---
 templates/projects/view.tmpl           |  4 ++--
 web_src/css/features/projects.css      |  3 +++
 web_src/css/repo/issue-card.css        |  4 ++++
 web_src/js/features/repo-issue-list.js |  2 --
 web_src/js/features/repo-projects.js   |  5 +----
 web_src/js/modules/sortable.js         | 19 +++++++++++++++++--
 6 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index e6c6c20497..b45174b086 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -67,7 +67,7 @@
 	<div class="board {{if .CanWriteProjects}}sortable{{end}}">
 		{{range .Columns}}
 			<div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
-				<div class="project-column-header">
+				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
 					<div class="ui large label project-column-title tw-py-1">
 						<div class="ui small circular grey label project-column-issue-count">
 							{{.NumIssues ctx}}
@@ -156,7 +156,7 @@
 
 				<div class="divider"></div>
 
-				<div class="ui cards{{if $canWriteProject}} tw-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
+				<div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
 					{{range (index $.IssuesMap .ID)}}
 						<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
 							{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index f85430a2a8..30df994c38 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -19,6 +19,7 @@
   overflow: visible;
   display: flex;
   flex-direction: column;
+  cursor: default;
 }
 
 .project-column-header {
@@ -46,6 +47,7 @@
 .project-column-title {
   background: none !important;
   line-height: 1.25 !important;
+  cursor: inherit;
 }
 
 .project-column > .cards {
@@ -92,6 +94,7 @@
 }
 
 .card-ghost {
+  border-color: var(--color-secondary-dark-4) !important;
   border-style: dashed !important;
   background: none !important;
 }
diff --git a/web_src/css/repo/issue-card.css b/web_src/css/repo/issue-card.css
index 5a70de47c2..b9368df4f6 100644
--- a/web_src/css/repo/issue-card.css
+++ b/web_src/css/repo/issue-card.css
@@ -19,3 +19,7 @@
   font-size: 14px;
   margin-left: 4px;
 }
+
+.issue-card.sortable-chosen .issue-card-title {
+  cursor: inherit;
+}
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 9681e648d5..4582f87425 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -188,8 +188,6 @@ async function initIssuePinSort() {
 
   createSortable(pinDiv, {
     group: 'shared',
-    animation: 150,
-    ghostClass: 'card-ghost',
     onEnd: pinMoveEnd,
   });
 }
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index 1747cb2b3a..d9ae85a8d2 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -58,8 +58,7 @@ async function initRepoProjectSortable() {
   createSortable(mainBoard, {
     group: 'project-column',
     draggable: '.project-column',
-    animation: 150,
-    ghostClass: 'card-ghost',
+    handle: '.project-column-header',
     delayOnTouchOnly: true,
     delay: 500,
     onSort: async () => {
@@ -86,8 +85,6 @@ async function initRepoProjectSortable() {
     const boardCardList = boardColumn.getElementsByClassName('cards')[0];
     createSortable(boardCardList, {
       group: 'shared',
-      animation: 150,
-      ghostClass: 'card-ghost',
       onAdd: moveIssue,
       onUpdate: moveIssue,
       delayOnTouchOnly: true,
diff --git a/web_src/js/modules/sortable.js b/web_src/js/modules/sortable.js
index cfe7c3bf30..1c9adb6d72 100644
--- a/web_src/js/modules/sortable.js
+++ b/web_src/js/modules/sortable.js
@@ -1,4 +1,19 @@
-export async function createSortable(...args) {
+export async function createSortable(el, opts = {}) {
   const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
-  return new Sortable(...args);
+
+  return new Sortable(el, {
+    animation: 150,
+    ghostClass: 'card-ghost',
+    onChoose: (e) => {
+      const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
+      handle.classList.add('tw-cursor-grabbing');
+      opts.onChoose?.(e);
+    },
+    onUnchoose: (e) => {
+      const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
+      handle.classList.remove('tw-cursor-grabbing');
+      opts.onUnchoose?.(e);
+    },
+    ...opts,
+  });
 }

From 71706126b56616750a65290460fd211b9b8449da Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 28 Mar 2024 10:26:13 +0800
Subject: [PATCH 559/679] Refactor markdown render (#30139)

Only split the file into small ones (and rename AttentionTypes to
attentionTypes)
---
 modules/markup/markdown/goldmark.go           | 263 +-----------------
 modules/markup/markdown/prefixed_id.go        |  59 ++++
 .../markup/markdown/transform_blockquote.go   |  27 +-
 modules/markup/markdown/transform_codespan.go |  57 ++++
 modules/markup/markdown/transform_heading.go  |  32 +++
 modules/markup/markdown/transform_image.go    |  66 +++++
 modules/markup/markdown/transform_link.go     |  31 +++
 modules/markup/markdown/transform_list.go     |  86 ++++++
 8 files changed, 364 insertions(+), 257 deletions(-)
 create mode 100644 modules/markup/markdown/prefixed_id.go
 create mode 100644 modules/markup/markdown/transform_codespan.go
 create mode 100644 modules/markup/markdown/transform_heading.go
 create mode 100644 modules/markup/markdown/transform_image.go
 create mode 100644 modules/markup/markdown/transform_link.go
 create mode 100644 modules/markup/markdown/transform_list.go

diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index b61299c480..b8b3aeaab0 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -4,19 +4,14 @@
 package markdown
 
 import (
-	"bytes"
 	"fmt"
 	"regexp"
 	"strings"
 
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/markup"
-	"code.gitea.io/gitea/modules/markup/common"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/svg"
-	giteautil "code.gitea.io/gitea/modules/util"
 
-	"github.com/microcosm-cc/bluemonday/css"
 	"github.com/yuin/goldmark/ast"
 	east "github.com/yuin/goldmark/extension/ast"
 	"github.com/yuin/goldmark/parser"
@@ -28,12 +23,12 @@ import (
 
 // ASTTransformer is a default transformer of the goldmark tree.
 type ASTTransformer struct {
-	AttentionTypes container.Set[string]
+	attentionTypes container.Set[string]
 }
 
 func NewASTTransformer() *ASTTransformer {
 	return &ASTTransformer{
-		AttentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
+		attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
 	}
 }
 
@@ -66,123 +61,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 
 		switch v := n.(type) {
 		case *ast.Heading:
-			for _, attr := range v.Attributes() {
-				if _, ok := attr.Value.([]byte); !ok {
-					v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
-				}
-			}
-			txt := n.Text(reader.Source())
-			header := markup.Header{
-				Text:  util.BytesToReadOnlyString(txt),
-				Level: v.Level,
-			}
-			if id, found := v.AttributeString("id"); found {
-				header.ID = util.BytesToReadOnlyString(id.([]byte))
-			}
-			tocList = append(tocList, header)
-			g.applyElementDir(v)
+			g.transformHeading(ctx, v, reader, &tocList)
 		case *ast.Paragraph:
 			g.applyElementDir(v)
 		case *ast.Image:
-			// Images need two things:
-			//
-			// 1. Their src needs to munged to be a real value
-			// 2. If they're not wrapped with a link they need a link wrapper
-
-			// Check if the destination is a real link
-			if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
-				v.Destination = []byte(giteautil.URLJoin(
-					ctx.Links.ResolveMediaLink(ctx.IsWiki),
-					strings.TrimLeft(string(v.Destination), "/"),
-				))
-			}
-
-			parent := n.Parent()
-			// Create a link around image only if parent is not already a link
-			if _, ok := parent.(*ast.Link); !ok && parent != nil {
-				next := n.NextSibling()
-
-				// Create a link wrapper
-				wrap := ast.NewLink()
-				wrap.Destination = v.Destination
-				wrap.Title = v.Title
-				wrap.SetAttributeString("target", []byte("_blank"))
-
-				// Duplicate the current image node
-				image := ast.NewImage(ast.NewLink())
-				image.Destination = v.Destination
-				image.Title = v.Title
-				for _, attr := range v.Attributes() {
-					image.SetAttribute(attr.Name, attr.Value)
-				}
-				for child := v.FirstChild(); child != nil; {
-					next := child.NextSibling()
-					image.AppendChild(image, child)
-					child = next
-				}
-
-				// Append our duplicate image to the wrapper link
-				wrap.AppendChild(wrap, image)
-
-				// Wire in the next sibling
-				wrap.SetNextSibling(next)
-
-				// Replace the current node with the wrapper link
-				parent.ReplaceChild(parent, n, wrap)
-
-				// But most importantly ensure the next sibling is still on the old image too
-				v.SetNextSibling(next)
-			}
+			g.transformImage(ctx, v, reader)
 		case *ast.Link:
-			// Links need their href to munged to be a real value
-			link := v.Destination
-			isAnchorFragment := len(link) > 0 && link[0] == '#'
-			if !isAnchorFragment && !markup.IsFullURLBytes(link) {
-				base := ctx.Links.Base
-				if ctx.IsWiki {
-					base = ctx.Links.WikiLink()
-				} else if ctx.Links.HasBranchInfo() {
-					base = ctx.Links.SrcLink()
-				}
-				link = []byte(giteautil.URLJoin(base, string(link)))
-			}
-			if isAnchorFragment {
-				link = []byte("#user-content-" + string(link)[1:])
-			}
-			v.Destination = link
+			g.transformLink(ctx, v, reader)
 		case *ast.List:
-			if v.HasChildren() {
-				children := make([]ast.Node, 0, v.ChildCount())
-				child := v.FirstChild()
-				for child != nil {
-					children = append(children, child)
-					child = child.NextSibling()
-				}
-				v.RemoveChildren(v)
-
-				for _, child := range children {
-					listItem := child.(*ast.ListItem)
-					if !child.HasChildren() || !child.FirstChild().HasChildren() {
-						v.AppendChild(v, child)
-						continue
-					}
-					taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
-					if !ok {
-						v.AppendChild(v, child)
-						continue
-					}
-					newChild := NewTaskCheckBoxListItem(listItem)
-					newChild.IsChecked = taskCheckBox.IsChecked
-					newChild.SetAttributeString("class", []byte("task-list-item"))
-					segments := newChild.FirstChild().Lines()
-					if segments.Len() > 0 {
-						segment := segments.At(0)
-						newChild.SourcePosition = rc.metaLength + segment.Start
-					}
-					v.AppendChild(v, newChild)
-				}
-			}
-			g.applyElementDir(v)
+			g.transformList(ctx, v, reader, rc)
 		case *ast.Text:
 			if v.SoftLineBreak() && !v.HardLineBreak() {
 				if ctx.Metas["mode"] != "document" {
@@ -192,10 +79,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 				}
 			}
 		case *ast.CodeSpan:
-			colorContent := n.Text(reader.Source())
-			if css.ColorHandler(strings.ToLower(string(colorContent))) {
-				v.AppendChild(v, NewColorPreview(colorContent))
-			}
+			g.transformCodeSpan(ctx, v, reader)
 		case *ast.Blockquote:
 			return g.transformBlockquote(v, reader)
 		}
@@ -219,50 +103,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 	}
 }
 
-type prefixedIDs struct {
-	values container.Set[string]
-}
-
-// Generate generates a new element id.
-func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
-	dft := []byte("id")
-	if kind == ast.KindHeading {
-		dft = []byte("heading")
-	}
-	return p.GenerateWithDefault(value, dft)
-}
-
-// GenerateWithDefault generates a new element id.
-func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
-	result := common.CleanValue(value)
-	if len(result) == 0 {
-		result = dft
-	}
-	if !bytes.HasPrefix(result, []byte("user-content-")) {
-		result = append([]byte("user-content-"), result...)
-	}
-	if p.values.Add(util.BytesToReadOnlyString(result)) {
-		return result
-	}
-	for i := 1; ; i++ {
-		newResult := fmt.Sprintf("%s-%d", result, i)
-		if p.values.Add(newResult) {
-			return []byte(newResult)
-		}
-	}
-}
-
-// Put puts a given element id to the used ids table.
-func (p *prefixedIDs) Put(value []byte) {
-	p.values.Add(util.BytesToReadOnlyString(value))
-}
-
-func newPrefixedIDs() *prefixedIDs {
-	return &prefixedIDs{
-		values: make(container.Set[string]),
-	}
-}
-
 // NewHTMLRenderer creates a HTMLRenderer to render
 // in the gitea form.
 func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
@@ -295,60 +135,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 }
 
-// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
-// See #21474 for reference
-func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
-	if entering {
-		if n.Attributes() != nil {
-			_, _ = w.WriteString("<code")
-			html.RenderAttributes(w, n, html.CodeAttributeFilter)
-			_ = w.WriteByte('>')
-		} else {
-			_, _ = w.WriteString("<code>")
-		}
-		for c := n.FirstChild(); c != nil; c = c.NextSibling() {
-			switch v := c.(type) {
-			case *ast.Text:
-				segment := v.Segment
-				value := segment.Value(source)
-				if bytes.HasSuffix(value, []byte("\n")) {
-					r.Writer.RawWrite(w, value[:len(value)-1])
-					r.Writer.RawWrite(w, []byte(" "))
-				} else {
-					r.Writer.RawWrite(w, value)
-				}
-			case *ColorPreview:
-				_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
-			}
-		}
-		return ast.WalkSkipChildren, nil
-	}
-	_, _ = w.WriteString("</code>")
-	return ast.WalkContinue, nil
-}
-
-// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
-func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	if entering {
-		n := node.(*Attention)
-		var octiconName string
-		switch n.AttentionType {
-		case "tip":
-			octiconName = "light-bulb"
-		case "important":
-			octiconName = "report"
-		case "warning":
-			octiconName = "alert"
-		case "caution":
-			octiconName = "stop"
-		default: // including "note"
-			octiconName = "info"
-		}
-		_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
-	}
-	return ast.WalkContinue, nil
-}
-
 func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	n := node.(*ast.Document)
 
@@ -435,38 +221,3 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
 
 	return ast.WalkContinue, nil
 }
-
-func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	n := node.(*TaskCheckBoxListItem)
-	if entering {
-		if n.Attributes() != nil {
-			_, _ = w.WriteString("<li")
-			html.RenderAttributes(w, n, html.ListItemAttributeFilter)
-			_ = w.WriteByte('>')
-		} else {
-			_, _ = w.WriteString("<li>")
-		}
-		fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
-		if n.IsChecked {
-			_, _ = w.WriteString(` checked=""`)
-		}
-		if r.XHTML {
-			_, _ = w.WriteString(` />`)
-		} else {
-			_ = w.WriteByte('>')
-		}
-		fc := n.FirstChild()
-		if fc != nil {
-			if _, ok := fc.(*ast.TextBlock); !ok {
-				_ = w.WriteByte('\n')
-			}
-		}
-	} else {
-		_, _ = w.WriteString("</li>\n")
-	}
-	return ast.WalkContinue, nil
-}
-
-func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	return ast.WalkContinue, nil
-}
diff --git a/modules/markup/markdown/prefixed_id.go b/modules/markup/markdown/prefixed_id.go
new file mode 100644
index 0000000000..9c60949202
--- /dev/null
+++ b/modules/markup/markdown/prefixed_id.go
@@ -0,0 +1,59 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"bytes"
+	"fmt"
+
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/markup/common"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/util"
+)
+
+type prefixedIDs struct {
+	values container.Set[string]
+}
+
+// Generate generates a new element id.
+func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
+	dft := []byte("id")
+	if kind == ast.KindHeading {
+		dft = []byte("heading")
+	}
+	return p.GenerateWithDefault(value, dft)
+}
+
+// GenerateWithDefault generates a new element id.
+func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
+	result := common.CleanValue(value)
+	if len(result) == 0 {
+		result = dft
+	}
+	if !bytes.HasPrefix(result, []byte("user-content-")) {
+		result = append([]byte("user-content-"), result...)
+	}
+	if p.values.Add(util.BytesToReadOnlyString(result)) {
+		return result
+	}
+	for i := 1; ; i++ {
+		newResult := fmt.Sprintf("%s-%d", result, i)
+		if p.values.Add(newResult) {
+			return []byte(newResult)
+		}
+	}
+}
+
+// Put puts a given element id to the used ids table.
+func (p *prefixedIDs) Put(value []byte) {
+	p.values.Add(util.BytesToReadOnlyString(value))
+}
+
+func newPrefixedIDs() *prefixedIDs {
+	return &prefixedIDs{
+		values: make(container.Set[string]),
+	}
+}
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index 65b735e83b..933f0e5c59 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -6,12 +6,37 @@ package markdown
 import (
 	"strings"
 
+	"code.gitea.io/gitea/modules/svg"
+
 	"github.com/yuin/goldmark/ast"
 	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 )
 
+// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
+func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	if entering {
+		n := node.(*Attention)
+		var octiconName string
+		switch n.AttentionType {
+		case "tip":
+			octiconName = "light-bulb"
+		case "important":
+			octiconName = "report"
+		case "warning":
+			octiconName = "alert"
+		case "caution":
+			octiconName = "stop"
+		default: // including "note"
+			octiconName = "info"
+		}
+		_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
+	}
+	return ast.WalkContinue, nil
+}
+
 func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
 	// We only want attention blockquotes when the AST looks like:
 	// > Text("[") Text("!TYPE") Text("]")
@@ -43,7 +68,7 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
 
 	// grab attention type from markdown source
 	attentionType := strings.ToLower(val2[1:])
-	if !g.AttentionTypes.Contains(attentionType) {
+	if !g.attentionTypes.Contains(attentionType) {
 		return ast.WalkContinue, nil
 	}
 
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
new file mode 100644
index 0000000000..bfff2897b0
--- /dev/null
+++ b/modules/markup/markdown/transform_codespan.go
@@ -0,0 +1,57 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/microcosm-cc/bluemonday/css"
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+)
+
+// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
+// See #21474 for reference
+func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+	if entering {
+		if n.Attributes() != nil {
+			_, _ = w.WriteString("<code")
+			html.RenderAttributes(w, n, html.CodeAttributeFilter)
+			_ = w.WriteByte('>')
+		} else {
+			_, _ = w.WriteString("<code>")
+		}
+		for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+			switch v := c.(type) {
+			case *ast.Text:
+				segment := v.Segment
+				value := segment.Value(source)
+				if bytes.HasSuffix(value, []byte("\n")) {
+					r.Writer.RawWrite(w, value[:len(value)-1])
+					r.Writer.RawWrite(w, []byte(" "))
+				} else {
+					r.Writer.RawWrite(w, value)
+				}
+			case *ColorPreview:
+				_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
+			}
+		}
+		return ast.WalkSkipChildren, nil
+	}
+	_, _ = w.WriteString("</code>")
+	return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
+	colorContent := v.Text(reader.Source())
+	if css.ColorHandler(strings.ToLower(string(colorContent))) {
+		v.AppendChild(v, NewColorPreview(colorContent))
+	}
+}
diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go
new file mode 100644
index 0000000000..ce585a37de
--- /dev/null
+++ b/modules/markup/markdown/transform_heading.go
@@ -0,0 +1,32 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"fmt"
+
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+)
+
+func (g *ASTTransformer) transformHeading(ctx *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
+	for _, attr := range v.Attributes() {
+		if _, ok := attr.Value.([]byte); !ok {
+			v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
+		}
+	}
+	txt := v.Text(reader.Source())
+	header := markup.Header{
+		Text:  util.BytesToReadOnlyString(txt),
+		Level: v.Level,
+	}
+	if id, found := v.AttributeString("id"); found {
+		header.ID = util.BytesToReadOnlyString(id.([]byte))
+	}
+	*tocList = append(*tocList, header)
+	g.applyElementDir(v)
+}
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go
new file mode 100644
index 0000000000..f290dc3721
--- /dev/null
+++ b/modules/markup/markdown/transform_image.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"strings"
+
+	"code.gitea.io/gitea/modules/markup"
+	giteautil "code.gitea.io/gitea/modules/util"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image, reader text.Reader) {
+	// Images need two things:
+	//
+	// 1. Their src needs to munged to be a real value
+	// 2. If they're not wrapped with a link they need a link wrapper
+
+	// Check if the destination is a real link
+	if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
+		v.Destination = []byte(giteautil.URLJoin(
+			ctx.Links.ResolveMediaLink(ctx.IsWiki),
+			strings.TrimLeft(string(v.Destination), "/"),
+		))
+	}
+
+	parent := v.Parent()
+	// Create a link around image only if parent is not already a link
+	if _, ok := parent.(*ast.Link); !ok && parent != nil {
+		next := v.NextSibling()
+
+		// Create a link wrapper
+		wrap := ast.NewLink()
+		wrap.Destination = v.Destination
+		wrap.Title = v.Title
+		wrap.SetAttributeString("target", []byte("_blank"))
+
+		// Duplicate the current image node
+		image := ast.NewImage(ast.NewLink())
+		image.Destination = v.Destination
+		image.Title = v.Title
+		for _, attr := range v.Attributes() {
+			image.SetAttribute(attr.Name, attr.Value)
+		}
+		for child := v.FirstChild(); child != nil; {
+			next := child.NextSibling()
+			image.AppendChild(image, child)
+			child = next
+		}
+
+		// Append our duplicate image to the wrapper link
+		wrap.AppendChild(wrap, image)
+
+		// Wire in the next sibling
+		wrap.SetNextSibling(next)
+
+		// Replace the current node with the wrapper link
+		parent.ReplaceChild(parent, v, wrap)
+
+		// But most importantly ensure the next sibling is still on the old image too
+		v.SetNextSibling(next)
+	}
+}
diff --git a/modules/markup/markdown/transform_link.go b/modules/markup/markdown/transform_link.go
new file mode 100644
index 0000000000..8bf19ea4ce
--- /dev/null
+++ b/modules/markup/markdown/transform_link.go
@@ -0,0 +1,31 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"code.gitea.io/gitea/modules/markup"
+	giteautil "code.gitea.io/gitea/modules/util"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
+	// Links need their href to munged to be a real value
+	link := v.Destination
+	isAnchorFragment := len(link) > 0 && link[0] == '#'
+	if !isAnchorFragment && !markup.IsFullURLBytes(link) {
+		base := ctx.Links.Base
+		if ctx.IsWiki {
+			base = ctx.Links.WikiLink()
+		} else if ctx.Links.HasBranchInfo() {
+			base = ctx.Links.SrcLink()
+		}
+		link = []byte(giteautil.URLJoin(base, string(link)))
+	}
+	if isAnchorFragment {
+		link = []byte("#user-content-" + string(link)[1:])
+	}
+	v.Destination = link
+}
diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go
new file mode 100644
index 0000000000..6563e2dd64
--- /dev/null
+++ b/modules/markup/markdown/transform_list.go
@@ -0,0 +1,86 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"fmt"
+
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/yuin/goldmark/ast"
+	east "github.com/yuin/goldmark/extension/ast"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+)
+
+func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*TaskCheckBoxListItem)
+	if entering {
+		if n.Attributes() != nil {
+			_, _ = w.WriteString("<li")
+			html.RenderAttributes(w, n, html.ListItemAttributeFilter)
+			_ = w.WriteByte('>')
+		} else {
+			_, _ = w.WriteString("<li>")
+		}
+		fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
+		if n.IsChecked {
+			_, _ = w.WriteString(` checked=""`)
+		}
+		if r.XHTML {
+			_, _ = w.WriteString(` />`)
+		} else {
+			_ = w.WriteByte('>')
+		}
+		fc := n.FirstChild()
+		if fc != nil {
+			if _, ok := fc.(*ast.TextBlock); !ok {
+				_ = w.WriteByte('\n')
+			}
+		}
+	} else {
+		_, _ = w.WriteString("</li>\n")
+	}
+	return ast.WalkContinue, nil
+}
+
+func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformList(ctx *markup.RenderContext, v *ast.List, reader text.Reader, rc *RenderConfig) {
+	if v.HasChildren() {
+		children := make([]ast.Node, 0, v.ChildCount())
+		child := v.FirstChild()
+		for child != nil {
+			children = append(children, child)
+			child = child.NextSibling()
+		}
+		v.RemoveChildren(v)
+
+		for _, child := range children {
+			listItem := child.(*ast.ListItem)
+			if !child.HasChildren() || !child.FirstChild().HasChildren() {
+				v.AppendChild(v, child)
+				continue
+			}
+			taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
+			if !ok {
+				v.AppendChild(v, child)
+				continue
+			}
+			newChild := NewTaskCheckBoxListItem(listItem)
+			newChild.IsChecked = taskCheckBox.IsChecked
+			newChild.SetAttributeString("class", []byte("task-list-item"))
+			segments := newChild.FirstChild().Lines()
+			if segments.Len() > 0 {
+				segment := segments.At(0)
+				newChild.SourcePosition = rc.metaLength + segment.Start
+			}
+			v.AppendChild(v, newChild)
+		}
+	}
+	g.applyElementDir(v)
+}

From 7ba485bd494b0d7a4091806afa73a7f03dc44743 Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Thu, 28 Mar 2024 10:47:05 +0800
Subject: [PATCH 560/679] Apply to become a maintainer (#30151)

PRs:https://github.com/go-gitea/gitea/pulls?q=is%3Apr+author%3AHEREYUA+is%3Aclosed

Discord: hereyua
---
 MAINTAINERS | 1 +
 1 file changed, 1 insertion(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 2f95fdca50..eed87529a3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -60,3 +60,4 @@ Nanguan Lin <nanguanlin6@gmail.com> (@lng2020)
 kerwin612 <kerwin612@qq.com> (@kerwin612)
 Gary Wang <git@blumia.net> (@BLumia)
 Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
+Yu Liu <1240335630@qq.com> (@HEREYUA)

From 0d5abe3454c73f11d90d2809af0949a0e0636c22 Mon Sep 17 00:00:00 2001
From: delvh <dev.lh@web.de>
Date: Thu, 28 Mar 2024 04:13:42 +0100
Subject: [PATCH 561/679] Remember login for a month by default (#30150)

Previously, the default was a week.
As most instances don't set the setting, this leads to a bad user
experience by default.

## :warning: Breaking

If your instance requires a high level of security,
you may want to set `[security].LOGIN_REMEMBER_DAYS` so that logins are
not valid as long.

---------

Co-authored-by: Jason Song <i@wolfogre.com>
---
 custom/conf/app.example.ini                             | 2 +-
 docs/content/administration/config-cheat-sheet.en-us.md | 2 +-
 docs/content/administration/config-cheat-sheet.zh-cn.md | 2 +-
 modules/setting/security.go                             | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index b4b4f3a8a2..e2723bd8ae 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -441,7 +441,7 @@ INTERNAL_TOKEN =
 ;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
 ;;
 ;; How long to remember that a user is logged in before requiring relogin (in days)
-;LOGIN_REMEMBER_DAYS = 7
+;LOGIN_REMEMBER_DAYS = 31
 ;;
 ;; Name of the cookie used to store the current username.
 ;COOKIE_USERNAME = gitea_awesome
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 2309021f94..03ab517d80 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -528,7 +528,7 @@ And the following unique queues:
 - `INSTALL_LOCK`: **false**: Controls access to the installation page. When set to "true", the installation page is not accessible.
 - `SECRET_KEY`: **\<random at every install\>**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
 - `SECRET_KEY_URI`: **_empty_**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_key`). It shouldn't be lost like SECRET_KEY.
-- `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days.
+- `LOGIN_REMEMBER_DAYS`: **31**: How long to remember that a user is logged in before requiring relogin (in days).
 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication
    information.
 - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**: Header name for reverse proxy
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 3115e4cc06..41c8844ae5 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -507,7 +507,7 @@ Gitea 创建以下非唯一队列:
 - `INSTALL_LOCK`: **false**:控制是否能够访问安装向导页面,设置为 `true` 则禁止访问安装向导页面。
 - `SECRET_KEY`: **\<每次安装时随机生成\>**:全局服务器安全密钥。这个密钥非常重要,如果丢失将无法解密加密的数据(例如 2FA)。
 - `SECRET_KEY_URI`: **_empty_**:与定义 `SECRET_KEY` 不同,此选项可用于使用存储在文件中的密钥(示例值:`file:/etc/gitea/secret_key`)。它不应该像 `SECRET_KEY` 一样容易丢失。
-- `LOGIN_REMEMBER_DAYS`: **7**:Cookie 保存时间,单位为天。
+- `LOGIN_REMEMBER_DAYS`: **31**:在要求重新登录之前,记住用户的登录状态多长时间(以天为单位)。
 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**:保存自动登录信息的 Cookie 名称。
 - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**:反向代理认证的 HTTP 头部名称,用于提供用户信息。
 - `REVERSE_PROXY_AUTHENTICATION_EMAIL`: **X-WEBAUTH-EMAIL**:反向代理认证的 HTTP 头部名称,用于提供邮箱信息。
diff --git a/modules/setting/security.go b/modules/setting/security.go
index 380360a696..3d7b1f9ce7 100644
--- a/modules/setting/security.go
+++ b/modules/setting/security.go
@@ -103,7 +103,7 @@ func generateSaveInternalToken(rootCfg ConfigProvider) {
 func loadSecurityFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("security")
 	InstallLock = HasInstallLock(rootCfg)
-	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
+	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31)
 	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
 	if SecretKey == "" {
 		// FIXME: https://github.com/go-gitea/gitea/issues/16832

From 7443a10fc3d722d3326a0cb7b15b208f907c72d7 Mon Sep 17 00:00:00 2001
From: YR Chen <stevapple@icloud.com>
Date: Thu, 28 Mar 2024 16:01:15 +0800
Subject: [PATCH 562/679] Move from `max( id )` to `max( index )` for latest
 commit statuses (#30076)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This PR replaces the use of `max( id )`, and instead using ``max(
`index` )`` for determining the latest commit status. Building business
logic over an `auto_increment` primary key like `id` is risky and
there’re already plenty of discussions on the Internet.

There‘s no guarantee for `auto_increment` values to be monotonic,
especially upon failures or with a cluster. In the specific case, we met
the problem of commit statuses being outdated when using TiDB as the
database. As [being
documented](https://docs.pingcap.com/tidb/stable/auto-increment),
`auto_increment` values assigned to an `insert` statement will only be
monotonic on a per server (node) basis.

Closes #30074.
---
 models/git/commit_status.go | 120 ++++++++++++++++++++++--------------
 1 file changed, 73 insertions(+), 47 deletions(-)

diff --git a/models/git/commit_status.go b/models/git/commit_status.go
index 2d1d1bcb06..bb75dcca26 100644
--- a/models/git/commit_status.go
+++ b/models/git/commit_status.go
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/translation"
 
 	"xorm.io/builder"
+	"xorm.io/xorm"
 )
 
 // CommitStatus holds a single Status of a single Commit
@@ -269,44 +270,48 @@ type CommitStatusIndex struct {
 
 // GetLatestCommitStatus returns all statuses with a unique context for a given commit.
 func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) {
-	ids := make([]int64, 0, 10)
-	sess := db.GetEngine(ctx).Table(&CommitStatus{}).
-		Where("repo_id = ?", repoID).And("sha = ?", sha).
-		Select("max( id ) as id").
-		GroupBy("context_hash").OrderBy("max( id ) desc")
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{}).
+			Where("repo_id = ?", repoID).And("sha = ?", sha)
+	}
+	indices := make([]int64, 0, 10)
+	sess := getBase().Select("max( `index` ) as `index`").
+		GroupBy("context_hash").OrderBy("max( `index` ) desc")
 	if !listOptions.IsListAll() {
 		sess = db.SetSessionPagination(sess, &listOptions)
 	}
-	count, err := sess.FindAndCount(&ids)
+	count, err := sess.FindAndCount(&indices)
 	if err != nil {
 		return nil, count, err
 	}
-	statuses := make([]*CommitStatus, 0, len(ids))
-	if len(ids) == 0 {
+	statuses := make([]*CommitStatus, 0, len(indices))
+	if len(indices) == 0 {
 		return statuses, count, nil
 	}
-	return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses)
+	return statuses, count, getBase().And(builder.In("`index`", indices)).Find(&statuses)
 }
 
 // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
 func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) {
 	type result struct {
-		ID     int64
+		Index  int64
 		RepoID int64
 	}
 
 	results := make([]result, 0, len(repoIDsToLatestCommitSHAs))
 
-	sess := db.GetEngine(ctx).Table(&CommitStatus{})
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{})
+	}
 
 	// Create a disjunction of conditions for each repoID and SHA pair
 	conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs))
 	for repoID, sha := range repoIDsToLatestCommitSHAs {
 		conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha})
 	}
-	sess = sess.Where(builder.Or(conds...)).
-		Select("max( id ) as id, repo_id").
-		GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
+	sess := getBase().Where(builder.Or(conds...)).
+		Select("max( `index` ) as `index`, repo_id").
+		GroupBy("context_hash, repo_id").OrderBy("max( `index` ) desc")
 
 	if !listOptions.IsListAll() {
 		sess = db.SetSessionPagination(sess, &listOptions)
@@ -317,15 +322,21 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
 		return nil, err
 	}
 
-	ids := make([]int64, 0, len(results))
 	repoStatuses := make(map[int64][]*CommitStatus)
-	for _, result := range results {
-		ids = append(ids, result.ID)
-	}
 
-	statuses := make([]*CommitStatus, 0, len(ids))
-	if len(ids) > 0 {
-		err = db.GetEngine(ctx).In("id", ids).Find(&statuses)
+	if len(results) > 0 {
+		statuses := make([]*CommitStatus, 0, len(results))
+
+		conds = make([]builder.Cond, 0, len(results))
+		for _, result := range results {
+			cond := builder.Eq{
+				"`index`": result.Index,
+				"repo_id": result.RepoID,
+				"sha":     repoIDsToLatestCommitSHAs[result.RepoID],
+			}
+			conds = append(conds, cond)
+		}
+		err = getBase().Where(builder.Or(conds...)).Find(&statuses)
 		if err != nil {
 			return nil, err
 		}
@@ -342,42 +353,43 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
 // GetLatestCommitStatusForRepoCommitIDs returns all statuses with a unique context for a given list of repo-sha pairs
 func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, commitIDs []string) (map[string][]*CommitStatus, error) {
 	type result struct {
-		ID  int64
-		Sha string
+		Index int64
+		SHA   string
 	}
 
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID)
+	}
 	results := make([]result, 0, len(commitIDs))
 
-	sess := db.GetEngine(ctx).Table(&CommitStatus{})
-
-	// Create a disjunction of conditions for each repoID and SHA pair
 	conds := make([]builder.Cond, 0, len(commitIDs))
 	for _, sha := range commitIDs {
 		conds = append(conds, builder.Eq{"sha": sha})
 	}
-	sess = sess.Where(builder.Eq{"repo_id": repoID}.And(builder.Or(conds...))).
-		Select("max( id ) as id, sha").
-		GroupBy("context_hash, sha").OrderBy("max( id ) desc")
+	sess := getBase().And(builder.Or(conds...)).
+		Select("max( `index` ) as `index`, sha").
+		GroupBy("context_hash, sha").OrderBy("max( `index` ) desc")
 
 	err := sess.Find(&results)
 	if err != nil {
 		return nil, err
 	}
 
-	ids := make([]int64, 0, len(results))
 	repoStatuses := make(map[string][]*CommitStatus)
-	for _, result := range results {
-		ids = append(ids, result.ID)
-	}
 
-	statuses := make([]*CommitStatus, 0, len(ids))
-	if len(ids) > 0 {
-		err = db.GetEngine(ctx).In("id", ids).Find(&statuses)
+	if len(results) > 0 {
+		statuses := make([]*CommitStatus, 0, len(results))
+
+		conds = make([]builder.Cond, 0, len(results))
+		for _, result := range results {
+			conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA})
+		}
+		err = getBase().And(builder.Or(conds...)).Find(&statuses)
 		if err != nil {
 			return nil, err
 		}
 
-		// Group the statuses by repo ID
+		// Group the statuses by commit
 		for _, status := range statuses {
 			repoStatuses[status.SHA] = append(repoStatuses[status.SHA], status)
 		}
@@ -388,22 +400,36 @@ func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, co
 
 // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
 func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) {
+	type result struct {
+		Index int64
+		SHA   string
+	}
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID)
+	}
+
 	start := timeutil.TimeStampNow().AddDuration(-before)
-	ids := make([]int64, 0, 10)
-	if err := db.GetEngine(ctx).Table("commit_status").
-		Where("repo_id = ?", repoID).
-		And("updated_unix >= ?", start).
-		Select("max( id ) as id").
-		GroupBy("context_hash").OrderBy("max( id ) desc").
-		Find(&ids); err != nil {
+	results := make([]result, 0, 10)
+
+	sess := getBase().And("updated_unix >= ?", start).
+		Select("max( `index` ) as `index`, sha").
+		GroupBy("context_hash, sha").OrderBy("max( `index` ) desc")
+
+	err := sess.Find(&results)
+	if err != nil {
 		return nil, err
 	}
 
-	contexts := make([]string, 0, len(ids))
-	if len(ids) == 0 {
+	contexts := make([]string, 0, len(results))
+	if len(results) == 0 {
 		return contexts, nil
 	}
-	return contexts, db.GetEngine(ctx).Select("context").Table("commit_status").In("id", ids).Find(&contexts)
+
+	conds := make([]builder.Cond, 0, len(results))
+	for _, result := range results {
+		conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA})
+	}
+	return contexts, getBase().And(builder.Or(conds...)).Select("context").Find(&contexts)
 }
 
 // NewCommitStatusOptions holds options for creating a CommitStatus

From 226a82a9396dc94f362ba27bd1c9318630df74b4 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 28 Mar 2024 09:31:07 +0100
Subject: [PATCH 563/679] Migrate font-family to tailwind (#30118)

Enable us to use tailwind's
[`font-family`](https://tailwindcss.com/docs/font-family) classes as
well as remove `gt-mono` in favor of `tw-font-mono`. I also merged the
"compensation" to one selector, previously this was two different values
0.9em and 0.95em. I did not declare a `serif` font because I don't think
there will ever be a use case for those. Command ran:

```sh
perl -p -i -e 's#gt-mono#tw-font-mono#g' web_src/js/**/* templates/**/*
---
 docs/content/contributing/guidelines-frontend.en-us.md | 2 +-
 docs/content/contributing/guidelines-frontend.zh-cn.md | 2 +-
 tailwind.config.js                                     | 4 ++++
 templates/repo/commits_list_small.tmpl                 | 2 +-
 templates/repo/diff/blob_excerpt.tmpl                  | 6 +++---
 templates/repo/diff/box.tmpl                           | 8 ++++----
 templates/repo/diff/section_split.tmpl                 | 8 ++++----
 templates/repo/diff/section_unified.tmpl               | 2 +-
 templates/repo/file_info.tmpl                          | 2 +-
 templates/repo/release/list.tmpl                       | 2 +-
 templates/repo/settings/lfs.tmpl                       | 2 +-
 templates/repo/settings/lfs_pointers.tmpl              | 4 ++--
 templates/repo/tag/list.tmpl                           | 2 +-
 templates/shared/combomarkdowneditor.tmpl              | 2 +-
 web_src/css/base.css                                   | 9 ++++++++-
 web_src/css/helpers.css                                | 5 -----
 web_src/js/components/DiffCommitSelector.vue           | 2 +-
 web_src/js/components/DiffFileList.vue                 | 2 +-
 web_src/js/features/comp/ComboMarkdownEditor.js        | 2 +-
 19 files changed, 37 insertions(+), 31 deletions(-)

diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index 3535e97903..efeaf38bb2 100644
--- a/docs/content/contributing/guidelines-frontend.en-us.md
+++ b/docs/content/contributing/guidelines-frontend.en-us.md
@@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
 11. Custom event names are recommended to use `ce-` prefix.
-12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-mono`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
+12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-word-break`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
 
 ### Accessibility / ARIA
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index c7998c6dc5..394097b259 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
 11. 推荐使用自定义事件名称前缀`ce-`。
-12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-mono`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
+12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-word-break`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
 13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
 
 ### 可访问性 / ARIA
diff --git a/tailwind.config.js b/tailwind.config.js
index 5bce37e023..d49e9d7a1c 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -68,6 +68,10 @@ export default {
       '3xl': '24px',
       'full': 'var(--border-radius-circle)', // 50%
     },
+    fontFamily: {
+      sans: 'var(--fonts-regular)',
+      mono: 'var(--fonts-monospace)',
+    },
     fontWeight: {
       light: 'var(--font-weight-light)',
       normal: 'var(--font-weight-normal)',
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index 0af29291d8..d96b314d01 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -38,7 +38,7 @@
 			</a>
 		</span>
 
-		<span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
+		<span class="tw-font-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
 		{{if IsMultilineCommitMessage .Message}}
 			<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
 		{{end}}
diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl
index 201bff805a..8312b5d913 100644
--- a/templates/repo/diff/blob_excerpt.tmpl
+++ b/templates/repo/diff/blob_excerpt.tmpl
@@ -27,7 +27,7 @@
 			{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
 			<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
 			<td class="blob-excerpt lines-escape lines-escape-old">{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
-			<td class="blob-excerpt lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="gt-mono" data-type-marker=""></span>{{end}}</td>
+			<td class="blob-excerpt lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
 			<td class="blob-excerpt lines-code lines-code-old">{{/*
 				*/}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/*
 					*/}}<code class="code-inner"></code>{{/*
@@ -35,7 +35,7 @@
 			*/}}</td>
 			<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
 			<td class="blob-excerpt lines-escape lines-escape-new">{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
-			<td class="blob-excerpt lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="gt-mono" data-type-marker=""></span>{{end}}</td>
+			<td class="blob-excerpt lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
 			<td class="blob-excerpt lines-code lines-code-new">{{/*
 				*/}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/*
 					*/}}<code class="code-inner"></code>{{/*
@@ -73,7 +73,7 @@
 		{{end}}
 		{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
 		<td class="blob-excerpt lines-escape">{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
-		<td class="blob-excerpt lines-type-marker"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
+		<td class="blob-excerpt lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
 		<td class="blob-excerpt lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}"><code {{if $inlineDiff.EscapeStatus.Escaped}}class="code-inner has-escaped" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"{{else}}class="code-inner"{{end}}>{{$inlineDiff.Content}}</code></td>
 	</tr>
 	{{end}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 555ffafc62..5327b7f02c 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -119,7 +119,7 @@
 										{{svg "octicon-chevron-down" 18}}
 									{{end}}
 								</button>
-								<div class="tw-font-semibold tw-flex tw-items-center gt-mono">
+								<div class="tw-font-semibold tw-flex tw-items-center tw-font-mono">
 									{{if $file.IsBin}}
 										<span class="tw-ml-0.5 tw-mr-2">
 											{{ctx.Locale.Tr "repo.diff.bin"}}
@@ -128,7 +128,7 @@
 										{{template "repo/diff/stats" dict "file" . "root" $}}
 									{{end}}
 								</div>
-								<span class="file gt-mono"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}</span>
+								<span class="file tw-font-mono"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}</span>
 								<button class="btn interact-fg tw-p-2" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
 								{{if $file.IsGenerated}}
 									<span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span>
@@ -139,9 +139,9 @@
 								{{if and $file.Mode $file.OldMode}}
 									{{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}}
 									{{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}
-									<span class="tw-ml-4 gt-mono">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
+									<span class="tw-ml-4 tw-font-mono">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
 								{{else if $file.Mode}}
-									<span class="tw-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
+									<span class="tw-ml-4 tw-font-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
 								{{end}}
 							</div>
 							<div class="diff-file-header-actions tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index 5af5da09b4..67e2b195de 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -44,7 +44,7 @@
 					{{- $rightDiff := ""}}{{if $match.RightIdx}}{{$rightDiff = $section.GetComputedInlineDiffFor $match ctx.Locale}}{{end}}
 					<td class="lines-num lines-num-old del-code" data-line-num="{{$line.LeftIdx}}"><span rel="diff-{{$file.NameHash}}L{{$line.LeftIdx}}"></span></td>
 					<td class="lines-escape del-code lines-escape-old">{{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $leftDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-old del-code"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
+					<td class="lines-type-marker lines-type-marker-old del-code"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
 					<td class="lines-code lines-code-old del-code">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
 							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
@@ -59,7 +59,7 @@
 					*/}}</td>
 					<td class="lines-num lines-num-new add-code" data-line-num="{{if $match.RightIdx}}{{$match.RightIdx}}{{end}}"><span rel="{{if $match.RightIdx}}diff-{{$file.NameHash}}R{{$match.RightIdx}}{{end}}"></span></td>
 					<td class="lines-escape add-code lines-escape-new">{{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $rightDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="gt-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td>
+					<td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-new add-code">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
 							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/*
@@ -76,7 +76,7 @@
 					{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
 					<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$file.NameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
 					<td class="lines-escape lines-escape-old">{{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
+					<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-old">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/*
 							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
@@ -91,7 +91,7 @@
 					*/}}</td>
 					<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$file.NameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
 					<td class="lines-escape lines-escape-new">{{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
+					<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-new">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/*
 							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/*
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index eb51c46d88..4111159709 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -44,7 +44,7 @@
 					<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>
 				{{- end -}}
 			</td>
-			<td class="lines-type-marker"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
+			<td class="lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
 			{{if eq .GetType 4}}
 				<td class="chroma lines-code blob-hunk">{{/*
 					*/}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/*
diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl
index 0fb56a9a19..86c613e3a1 100644
--- a/templates/repo/file_info.tmpl
+++ b/templates/repo/file_info.tmpl
@@ -1,4 +1,4 @@
-<div class="file-info text grey normal gt-mono">
+<div class="file-info text grey normal tw-font-mono">
 	{{if .FileIsSymlink}}
 		<div class="file-info-entry">
 			{{ctx.Locale.Tr "repo.symbolic_link"}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 9da6c48c8e..3139022bb4 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -11,7 +11,7 @@
 					<div class="ui four wide column meta">
 						<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 "tw-mr-1"}}{{$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 "tw-mr-1"}}{{ShortSha $release.Sha1}}</a>
+							<a class="muted tw-font-mono" href="{{$.RepoLink}}/src/commit/{{$release.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha $release.Sha1}}</a>
 							{{template "repo/branch_dropdown" dict "root" $ "release" $release}}
 						{{end}}
 					</div>
diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl
index dca4d1f1ce..e0864ff221 100644
--- a/templates/repo/settings/lfs.tmpl
+++ b/templates/repo/settings/lfs.tmpl
@@ -12,7 +12,7 @@
 				{{range .LFSFiles}}
 					<tr>
 						<td>
-							<a href="{{$.Link}}/show/{{.Oid}}" title="{{.Oid}}" class="ui brown button gt-mono">
+							<a href="{{$.Link}}/show/{{.Oid}}" title="{{.Oid}}" class="ui brown button tw-font-mono">
 								{{ShortSha .Oid}}
 							</a>
 						</td>
diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl
index fa2e376ff3..a0bb8c46f0 100644
--- a/templates/repo/settings/lfs_pointers.tmpl
+++ b/templates/repo/settings/lfs_pointers.tmpl
@@ -32,12 +32,12 @@
 					{{range .Pointers}}
 						<tr>
 							<td>
-								<a href="{{$.RepoLink}}/raw/blob/{{.SHA}}" rel="nofollow" target="_blank" title="{{.SHA}}" class="ui button gt-mono">
+								<a href="{{$.RepoLink}}/raw/blob/{{.SHA}}" rel="nofollow" target="_blank" title="{{.SHA}}" class="ui button tw-font-mono">
 									{{ShortSha .SHA}}
 								</a>
 							</td>
 							<td>
-								<a {{if and .Exists .InRepo}}href="{{$.LFSFilesLink}}/show/{{.Oid}}" rel="nofollow" target="_blank"{{end}} title="{{.Oid}}" class="ui brown button gt-mono">
+								<a {{if and .Exists .InRepo}}href="{{$.LFSFilesLink}}/show/{{.Oid}}" rel="nofollow" target="_blank"{{end}} title="{{.Oid}}" class="ui brown button tw-font-mono">
 									{{ShortSha .Oid}}
 								</a>
 							</td>
diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl
index a107bd1ad3..5378a8a322 100644
--- a/templates/repo/tag/list.tmpl
+++ b/templates/repo/tag/list.tmpl
@@ -29,7 +29,7 @@
 											<span class="tw-mr-2">{{svg "octicon-clock" 16 "tw-mr-1"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
 										{{end}}
 
-										<a class="tw-mr-2 gt-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .Sha1}}</a>
+										<a class="tw-mr-2 tw-font-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .Sha1}}</a>
 
 										{{if not $.DisableDownloadSourceArchives}}
 											<a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}ZIP</a>
diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl
index c6e86133cd..96fcf04cef 100644
--- a/templates/shared/combomarkdowneditor.tmpl
+++ b/templates/shared/combomarkdowneditor.tmpl
@@ -49,7 +49,7 @@ Template Attributes:
 		</text-expander>
 		<script>
 			if (localStorage?.getItem('markdown-editor-monospace') === 'true') {
-				document.querySelector('.markdown-text-editor').classList.add('gt-mono');
+				document.querySelector('.markdown-text-editor').classList.add('tw-font-mono');
 			}
 		</script>
 	</div>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 07f15cac2b..18db136bc9 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -62,10 +62,17 @@ pre,
 code,
 kbd,
 samp {
-  font-size: 0.9em; /* compensate for monospace fonts being usually slightly larger */
   font-family: var(--fonts-monospace);
 }
 
+pre,
+code,
+kbd,
+samp,
+.tw-font-mono {
+  font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */
+}
+
 b,
 strong,
 h1,
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 66d0f03257..13962f19d7 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -3,11 +3,6 @@ Gitea's tailwind-style CSS helper classes have `gt-` prefix.
 Gitea's private styles use `g-` prefix.
 */
 
-.gt-mono {
-  font-family: var(--fonts-monospace) !important;
-  font-size: .95em !important; /* compensate for monospace fonts being usually slightly larger */
-}
-
 .gt-word-break {
   word-wrap: break-word !important;
   word-break: break-word; /* compat: Safari */
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index cbb1f20873..352d085731 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -252,7 +252,7 @@ export default {
               </span>
             </div>
           </div>
-          <div class="gt-mono">
+          <div class="tw-font-mono">
             {{ commit.short_sha }}
           </div>
         </div>
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index bc6f1bee7d..916780d913 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -47,7 +47,7 @@ export default {
       </div>
       <!-- todo finish all file status, now modify, add, delete and rename -->
       <span :class="['status', diffTypeToString(file.Type)]" :data-tooltip-content="diffTypeToString(file.Type)">&nbsp;</span>
-      <a class="file gt-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
+      <a class="file tw-font-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
     </li>
     <li v-if="store.isIncomplete" class="tw-pt-1">
       <span class="file tw-flex tw-items-center tw-justify-between">{{ store.tooManyFilesMessage }}
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index 1e728ca201..d3fab375a9 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -105,7 +105,7 @@ class ComboMarkdownEditor {
       e.preventDefault();
       const enabled = localStorage?.getItem('markdown-editor-monospace') !== 'true';
       localStorage.setItem('markdown-editor-monospace', String(enabled));
-      this.textarea.classList.toggle('gt-mono', enabled);
+      this.textarea.classList.toggle('tw-font-mono', enabled);
       const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text');
       monospaceButton.setAttribute('data-tooltip-content', text);
       monospaceButton.setAttribute('aria-checked', String(enabled));

From eca4c485343069fc3e59f0dba7823cc13b5ab1d8 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Thu, 28 Mar 2024 11:42:08 +0200
Subject: [PATCH 564/679] Bump `@github/relative-time-element` to v4.4.0
 (#30154)

I tested and all timestamps work as before.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 package-lock.json | 8 ++++----
 package.json      | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 1c169c5636..fa4f80fbe8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
         "@citation-js/plugin-software-formats": "0.6.1",
         "@claviska/jquery-minicolors": "2.3.6",
         "@github/markdown-toolbar-element": "2.2.3",
-        "@github/relative-time-element": "4.3.1",
+        "@github/relative-time-element": "4.4.0",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
         "@primer/octicons": "19.9.0",
@@ -1004,9 +1004,9 @@
       "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
     },
     "node_modules/@github/relative-time-element": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.3.1.tgz",
-      "integrity": "sha512-zL79nlhZVCg7x2Pf/HT5MB0mowmErE71VXpF10/3Wy8dQwkninNO1M9aOizh2wKC5LkSpDXqNYjDZwbH0/bcSg=="
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.0.tgz",
+      "integrity": "sha512-CrI6oAecoahG7PF5dsgjdvlF5kCtusVMjg810EULD81TvnDsP+k/FRi/ClFubWLgBo4EGpr2EfvmumtqQFo7ow=="
     },
     "node_modules/@github/text-expander-element": {
       "version": "2.6.1",
diff --git a/package.json b/package.json
index 1eeb4d196c..b5bfda9dc6 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "@citation-js/plugin-software-formats": "0.6.1",
     "@claviska/jquery-minicolors": "2.3.6",
     "@github/markdown-toolbar-element": "2.2.3",
-    "@github/relative-time-element": "4.3.1",
+    "@github/relative-time-element": "4.4.0",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
     "@primer/octicons": "19.9.0",

From e40fc75bac65933f2ed3de8fbc5fb336195b59f5 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 28 Mar 2024 11:42:31 +0100
Subject: [PATCH 565/679] Render code tags in commit messages (#30146)

Extend https://github.com/go-gitea/gitea/pull/21432 to commit messages.
Color is changed because the markup code block bg does not offer enough
contrast on varying backgrounds.

<img width="568" alt="Screenshot 2024-03-27 at 19 52 55"
src="https://github.com/go-gitea/gitea/assets/115237/ddc9307e-f32f-4e97-8b88-91f88ced2a36">
<img width="573" alt="Screenshot 2024-03-27 at 19 53 33"
src="https://github.com/go-gitea/gitea/assets/115237/14b30fd2-bf28-46b8-9e82-eb60a28f6bf2">
<img width="422" alt="Screenshot 2024-03-27 at 19 53 01"
src="https://github.com/go-gitea/gitea/assets/115237/a12136b5-c02b-460c-9830-f830542987ae">
<img width="397" alt="Screenshot 2024-03-27 at 19 53 27"
src="https://github.com/go-gitea/gitea/assets/115237/c9f05d81-c73e-468e-98e9-e5929bc0da3e">
<img width="333" alt="Screenshot 2024-03-27 at 19 53 07"
src="https://github.com/go-gitea/gitea/assets/115237/06b5a9f9-f95d-46b6-8c57-df0b02555652">
<img width="279" alt="Screenshot 2024-03-27 at 19 53 21"
src="https://github.com/go-gitea/gitea/assets/115237/b06a0afc-ddd8-48ae-b557-a6dc47802e68">
---
 modules/templates/util_render.go | 4 ++--
 web_src/css/base.css             | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 7ed3a8b9b4..d1c9b082fa 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -41,7 +41,7 @@ func RenderCommitMessage(ctx context.Context, msg string, metas map[string]strin
 	if len(msgLines) == 0 {
 		return template.HTML("")
 	}
-	return template.HTML(msgLines[0])
+	return RenderCodeBlock(template.HTML(msgLines[0]))
 }
 
 // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
@@ -68,7 +68,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string,
 		log.Error("RenderCommitMessageSubject: %v", err)
 		return template.HTML("")
 	}
-	return template.HTML(renderedMessage)
+	return RenderCodeBlock(template.HTML(renderedMessage))
 }
 
 // RenderCommitBody extracts the body of a commit message without its title.
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 18db136bc9..368bc56126 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -300,8 +300,8 @@ a.label,
 
 .inline-code-block {
   padding: 2px 4px;
-  border-radius: var(--border-radius-medium);
-  background-color: var(--color-markup-code-block);
+  border-radius: .24em;
+  background-color: var(--color-label-bg);
 }
 
 /* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */

From 242b331260925e604150346e61329097d5731e77 Mon Sep 17 00:00:00 2001
From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
Date: Thu, 28 Mar 2024 08:19:24 -0700
Subject: [PATCH 566/679] Prevent re-review and dismiss review actions on
 closed and merged PRs (#30065)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Resolves #29965.

---
Manually tested this by:
- Following the
[installation](https://docs.gitea.com/next/installation/install-with-docker#basics)
guide (but built a local Docker image instead)
- Creating 2 users, one who is the `Owner` of a newly-created repository
and the other a `Collaborator`
- Had the `Collaborator` create a PR that the `Owner` reviews
- `Collaborator` resolves conversation and `Owner` merges PR

And with this change we see that we can no longer see re-request review
button for the `Owner`:

<img width="1351" alt="Screenshot 2024-03-25 at 12 39 18 AM"
src="https://github.com/go-gitea/gitea/assets/60799661/bcd9c579-3cf7-474f-a51e-b436fe1a39a4">
---
 models/issues/review.go                       | 38 +++++++++++++--
 models/issues/review_test.go                  | 30 ++++++++++++
 routers/api/v1/repo/pull_review.go            | 17 ++++---
 routers/web/repo/issue.go                     |  4 ++
 routers/web/repo/pull_review.go               |  4 ++
 services/pull/review.go                       | 33 +++++++++++++
 services/pull/review_test.go                  | 48 +++++++++++++++++++
 .../repo/issue/view_content/sidebar.tmpl      |  4 +-
 templates/swagger/v1_json.tmpl                |  3 ++
 9 files changed, 170 insertions(+), 11 deletions(-)
 create mode 100644 services/pull/review_test.go

diff --git a/models/issues/review.go b/models/issues/review.go
index 455bcda50a..92764db4d1 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -66,6 +66,23 @@ func (err ErrNotValidReviewRequest) Unwrap() error {
 	return util.ErrInvalidArgument
 }
 
+// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR.
+type ErrReviewRequestOnClosedPR struct{}
+
+// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
+func IsErrReviewRequestOnClosedPR(err error) bool {
+	_, ok := err.(ErrReviewRequestOnClosedPR)
+	return ok
+}
+
+func (err ErrReviewRequestOnClosedPR) Error() string {
+	return "cannot request a re-review on a closed or merged PR"
+}
+
+func (err ErrReviewRequestOnClosedPR) Unwrap() error {
+	return util.ErrPermissionDenied
+}
+
 // ReviewType defines the sort of feedback a review gives
 type ReviewType int
 
@@ -618,9 +635,24 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
 		return nil, err
 	}
 
-	// skip it when reviewer hase been request to review
-	if review != nil && review.Type == ReviewTypeRequest {
-		return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
+	if review != nil {
+		// skip it when reviewer hase been request to review
+		if review.Type == ReviewTypeRequest {
+			return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
+		}
+
+		if issue.IsClosed {
+			return nil, ErrReviewRequestOnClosedPR{}
+		}
+
+		if issue.IsPull {
+			if err := issue.LoadPullRequest(ctx); err != nil {
+				return nil, err
+			}
+			if issue.PullRequest.HasMerged {
+				return nil, ErrReviewRequestOnClosedPR{}
+			}
+		}
 	}
 
 	// if the reviewer is an official reviewer,
diff --git a/models/issues/review_test.go b/models/issues/review_test.go
index 1868cb1bfa..ac1b84adeb 100644
--- a/models/issues/review_test.go
+++ b/models/issues/review_test.go
@@ -288,3 +288,33 @@ func TestDeleteDismissedReview(t *testing.T) {
 	assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review))
 	unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
 }
+
+func TestAddReviewRequest(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+	assert.NoError(t, pull.LoadIssue(db.DefaultContext))
+	issue := pull.Issue
+	assert.NoError(t, issue.LoadRepo(db.DefaultContext))
+	reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	_, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
+		Issue:    issue,
+		Reviewer: reviewer,
+		Type:     issues_model.ReviewTypeReject,
+	})
+
+	assert.NoError(t, err)
+	pull.HasMerged = false
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	issue.IsClosed = true
+	_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
+	assert.Error(t, err)
+	assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
+
+	pull.HasMerged = true
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	issue.IsClosed = false
+	_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
+	assert.Error(t, err)
+	assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
+}
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index d314c4e7f7..17bb2085b6 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -640,6 +640,8 @@ func DeleteReviewRequests(ctx *context.APIContext) {
 	//     "$ref": "#/responses/empty"
 	//   "422":
 	//     "$ref": "#/responses/validationError"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 	opts := web.GetForm(ctx).(*api.PullReviewRequestOptions)
@@ -708,6 +710,10 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
 	for _, reviewer := range reviewers {
 		comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
 		if err != nil {
+			if issues_model.IsErrReviewRequestOnClosedPR(err) {
+				ctx.Error(http.StatusForbidden, "", err)
+				return
+			}
 			ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
 			return
 		}
@@ -874,7 +880,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
 		ctx.Error(http.StatusForbidden, "", "Must be repo admin")
 		return
 	}
-	review, pr, isWrong := prepareSingleReview(ctx)
+	review, _, isWrong := prepareSingleReview(ctx)
 	if isWrong {
 		return
 	}
@@ -884,13 +890,12 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
 		return
 	}
 
-	if pr.Issue.IsClosed {
-		ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed")
-		return
-	}
-
 	_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
 	if err != nil {
+		if pull_service.IsErrDismissRequestOnClosedPR(err) {
+			ctx.Error(http.StatusForbidden, "", err)
+			return
+		}
 		ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
 		return
 	}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 12233d0e17..6c2d4a7390 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -2498,6 +2498,10 @@ func UpdatePullReviewRequest(ctx *context.Context) {
 
 		_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
 		if err != nil {
+			if issues_model.IsErrReviewRequestOnClosedPR(err) {
+				ctx.Status(http.StatusForbidden)
+				return
+			}
 			ctx.ServerError("ReviewRequest", err)
 			return
 		}
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 5385ebfc97..c8d149a482 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -277,6 +277,10 @@ func DismissReview(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.DismissReviewForm)
 	comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
 	if err != nil {
+		if pull_service.IsErrDismissRequestOnClosedPR(err) {
+			ctx.Status(http.StatusForbidden)
+			return
+		}
 		ctx.ServerError("pull_service.DismissReview", err)
 		return
 	}
diff --git a/services/pull/review.go b/services/pull/review.go
index de1021c5c0..5bf1991d13 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -20,11 +20,29 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
 var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
 
+// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR.
+type ErrDismissRequestOnClosedPR struct{}
+
+// IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR.
+func IsErrDismissRequestOnClosedPR(err error) bool {
+	_, ok := err.(ErrDismissRequestOnClosedPR)
+	return ok
+}
+
+func (err ErrDismissRequestOnClosedPR) Error() string {
+	return "can't dismiss a review associated to a closed or merged PR"
+}
+
+func (err ErrDismissRequestOnClosedPR) Unwrap() error {
+	return util.ErrPermissionDenied
+}
+
 // checkInvalidation checks if the line of code comment got changed by another commit.
 // If the line got changed the comment is going to be invalidated.
 func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
@@ -382,6 +400,21 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
 		return nil, fmt.Errorf("reviews's repository is not the same as the one we expect")
 	}
 
+	issue := review.Issue
+
+	if issue.IsClosed {
+		return nil, ErrDismissRequestOnClosedPR{}
+	}
+
+	if issue.IsPull {
+		if err := issue.LoadPullRequest(ctx); err != nil {
+			return nil, err
+		}
+		if issue.PullRequest.HasMerged {
+			return nil, ErrDismissRequestOnClosedPR{}
+		}
+	}
+
 	if err := issues_model.DismissReview(ctx, review, isDismiss); err != nil {
 		return nil, err
 	}
diff --git a/services/pull/review_test.go b/services/pull/review_test.go
new file mode 100644
index 0000000000..3bce1e523d
--- /dev/null
+++ b/services/pull/review_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull_test
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	pull_service "code.gitea.io/gitea/services/pull"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDismissReview(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{})
+	assert.NoError(t, pull.LoadIssue(db.DefaultContext))
+	issue := pull.Issue
+	assert.NoError(t, issue.LoadRepo(db.DefaultContext))
+	reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
+		Issue:    issue,
+		Reviewer: reviewer,
+		Type:     issues_model.ReviewTypeReject,
+	})
+
+	assert.NoError(t, err)
+	issue.IsClosed = true
+	pull.HasMerged = false
+	assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
+	assert.Error(t, err)
+	assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
+
+	pull.HasMerged = true
+	pull.Issue.IsClosed = false
+	assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
+	assert.Error(t, err)
+	assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
+}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index bc2a841708..5913916ae4 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -59,7 +59,7 @@
 							{{end}}
 						</div>
 						<div class="tw-flex tw-items-center tw-gap-2">
-							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
+							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
 								<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
 									{{svg "octicon-x" 20}}
 								</a>
@@ -91,7 +91,7 @@
 									{{svg "octicon-hourglass" 16}}
 								</span>
 							{{end}}
-							{{if .CanChange}}
+							{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
 								<a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{if .Checked}}{{svg "octicon-trash"}}{{else}}{{svg "octicon-sync"}}{{end}}</a>
 							{{end}}
 							{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index f835df084d..ef6126ff85 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -11276,6 +11276,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           },

From 9585e19bb4386691760f741e23fba56cbfca8afb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Bogus=C5=82awski?= <pawel.boguslawski@ib.pl>
Date: Thu, 28 Mar 2024 16:24:30 +0100
Subject: [PATCH 567/679] Adjust VS Code debug filename match in .gitignore
 (#30158)

---
 .dockerignore | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.dockerignore b/.dockerignore
index 5cec84c9a3..7143c039fd 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -14,7 +14,7 @@ _test
 
 # MS VSCode
 .vscode
-__debug_bin
+__debug_bin*
 
 # Architecture specific extensions/prefixes
 *.[568vq]

From 40cdc84b368cce8328b4b49ea5ecf1c5fa040300 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 29 Mar 2024 00:14:30 +0800
Subject: [PATCH 568/679] Fix migration v292 (#30153)

Fix https://github.com/go-gitea/gitea/pull/29874#discussion_r1542227686

- The migration of v292 will miss many projects. These projects will
have no default board. This PR introduced a new migration number and
removed v292 migration.

- This PR also added the missed transactions on project-related
operations.

- Only `SetDefaultBoard` will remove duplicated defaults but not in
`GetDefaultBoard`
---
 models/migrations/migrations.go               |   2 +
 models/migrations/v1_22/v292.go               |  84 +-------------
 models/migrations/v1_22/v293.go               | 108 ++++++++++++++++++
 .../v1_22/{v292_test.go => v293_test.go}      |   6 +-
 models/project/board.go                       |  92 +++++++--------
 models/project/board_test.go                  |   6 +-
 6 files changed, 163 insertions(+), 135 deletions(-)
 create mode 100644 models/migrations/v1_22/v293.go
 rename models/migrations/v1_22/{v292_test.go => v293_test.go} (87%)

diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 77895fba61..0daa799ff6 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -569,6 +569,8 @@ var migrations = []Migration{
 	// v291 -> v292
 	NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
 	// v292 -> v293
+	NewMigration("Ensure every project has exactly one default column - No Op", noopMigration),
+	// v293 -> v294
 	NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
 }
 
diff --git a/models/migrations/v1_22/v292.go b/models/migrations/v1_22/v292.go
index 7c051a2b75..beca556aee 100644
--- a/models/migrations/v1_22/v292.go
+++ b/models/migrations/v1_22/v292.go
@@ -3,83 +3,7 @@
 
 package v1_22 //nolint
 
-import (
-	"code.gitea.io/gitea/models/project"
-	"code.gitea.io/gitea/modules/setting"
-
-	"xorm.io/builder"
-	"xorm.io/xorm"
-)
-
-// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
-func CheckProjectColumnsConsistency(x *xorm.Engine) error {
-	sess := x.NewSession()
-	defer sess.Close()
-
-	if err := sess.Begin(); err != nil {
-		return err
-	}
-
-	limit := setting.Database.IterateBufferSize
-	if limit <= 0 {
-		limit = 50
-	}
-
-	start := 0
-
-	for {
-		var projects []project.Project
-		if err := sess.SQL("SELECT DISTINCT `p`.`id`, `p`.`creator_id` FROM `project` `p` WHERE (SELECT COUNT(*) FROM `project_board` `pb` WHERE `pb`.`project_id` = `p`.`id` AND `pb`.`default` = ?) != 1", true).
-			Limit(limit, start).
-			Find(&projects); err != nil {
-			return err
-		}
-
-		if len(projects) == 0 {
-			break
-		}
-		start += len(projects)
-
-		for _, p := range projects {
-			var boards []project.Board
-			if err := sess.Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
-				return err
-			}
-
-			if len(boards) == 0 {
-				if _, err := sess.Insert(project.Board{
-					ProjectID: p.ID,
-					Default:   true,
-					Title:     "Uncategorized",
-					CreatorID: p.CreatorID,
-				}); err != nil {
-					return err
-				}
-				continue
-			}
-
-			var boardsToUpdate []int64
-			for id, b := range boards {
-				if id > 0 {
-					boardsToUpdate = append(boardsToUpdate, b.ID)
-				}
-			}
-
-			if _, err := sess.Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
-				Cols("`default`").Update(&project.Board{Default: false}); err != nil {
-				return err
-			}
-		}
-
-		if start%1000 == 0 {
-			if err := sess.Commit(); err != nil {
-				return err
-			}
-			if err := sess.Begin(); err != nil {
-				return err
-			}
-		}
-	}
-
-	return sess.Commit()
-}
+// NOTE: noop the original migration has bug which some projects will be skip, so
+// these projects will have no default board.
+// So that this migration will be skipped and go to v293.go
+// This file is a placeholder so that readers can know what happened
diff --git a/models/migrations/v1_22/v293.go b/models/migrations/v1_22/v293.go
new file mode 100644
index 0000000000..53cc719294
--- /dev/null
+++ b/models/migrations/v1_22/v293.go
@@ -0,0 +1,108 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/xorm"
+)
+
+// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
+func CheckProjectColumnsConsistency(x *xorm.Engine) error {
+	sess := x.NewSession()
+	defer sess.Close()
+
+	limit := setting.Database.IterateBufferSize
+	if limit <= 0 {
+		limit = 50
+	}
+
+	type Project struct {
+		ID        int64
+		CreatorID int64
+		BoardID   int64
+	}
+
+	type ProjectBoard struct {
+		ID      int64 `xorm:"pk autoincr"`
+		Title   string
+		Default bool   `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
+		Sorting int8   `xorm:"NOT NULL DEFAULT 0"`
+		Color   string `xorm:"VARCHAR(7)"`
+
+		ProjectID int64 `xorm:"INDEX NOT NULL"`
+		CreatorID int64 `xorm:"NOT NULL"`
+
+		CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+		UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+	}
+
+	for {
+		if err := sess.Begin(); err != nil {
+			return err
+		}
+
+		// all these projects without defaults will be fixed in the same loop, so
+		// we just need to always get projects without defaults until no such project
+		var projects []*Project
+		if err := sess.Select("project.id as id, project.creator_id, project_board.id as board_id").
+			Join("LEFT", "project_board", "project_board.project_id = project.id AND project_board.`default`=?", true).
+			Where("project_board.id is NULL OR project_board.id = 0").
+			Limit(limit).
+			Find(&projects); err != nil {
+			return err
+		}
+
+		for _, p := range projects {
+			if _, err := sess.Insert(ProjectBoard{
+				ProjectID: p.ID,
+				Default:   true,
+				Title:     "Uncategorized",
+				CreatorID: p.CreatorID,
+			}); err != nil {
+				return err
+			}
+		}
+		if err := sess.Commit(); err != nil {
+			return err
+		}
+
+		if len(projects) == 0 {
+			break
+		}
+	}
+	sess.Close()
+
+	return removeDuplicatedBoardDefault(x)
+}
+
+func removeDuplicatedBoardDefault(x *xorm.Engine) error {
+	type ProjectInfo struct {
+		ProjectID  int64
+		DefaultNum int
+	}
+	var projects []ProjectInfo
+	if err := x.Select("project_id, count(*) AS default_num").
+		Table("project_board").
+		Where("`default` = ?", true).
+		GroupBy("project_id").
+		Having("count(*) > 1").
+		Find(&projects); err != nil {
+		return err
+	}
+
+	for _, project := range projects {
+		if _, err := x.Where("project_id=?", project.ProjectID).
+			Table("project_board").
+			Limit(project.DefaultNum - 1).
+			Update(map[string]bool{
+				"`default`": false,
+			}); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/models/migrations/v1_22/v292_test.go b/models/migrations/v1_22/v293_test.go
similarity index 87%
rename from models/migrations/v1_22/v292_test.go
rename to models/migrations/v1_22/v293_test.go
index 5e32e0220f..ccc92f39a6 100644
--- a/models/migrations/v1_22/v292_test.go
+++ b/models/migrations/v1_22/v293_test.go
@@ -31,14 +31,14 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) {
 	assert.Equal(t, int64(1), defaultBoard.ProjectID)
 	assert.True(t, defaultBoard.Default)
 
-	// check if multiple defaults were removed
+	// check if multiple defaults, previous were removed and last will be kept
 	expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
-	assert.True(t, expectDefaultBoard.Default)
+	assert.False(t, expectDefaultBoard.Default)
 
 	expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
-	assert.False(t, expectNonDefaultBoard.Default)
+	assert.True(t, expectNonDefaultBoard.Default)
 }
diff --git a/models/project/board.go b/models/project/board.go
index 5605f259b5..5f142a356c 100644
--- a/models/project/board.go
+++ b/models/project/board.go
@@ -209,7 +209,6 @@ func deleteBoardByProjectID(ctx context.Context, projectID int64) error {
 // GetBoard fetches the current board of a project
 func GetBoard(ctx context.Context, boardID int64) (*Board, error) {
 	board := new(Board)
-
 	has, err := db.GetEngine(ctx).ID(boardID).Get(board)
 	if err != nil {
 		return nil, err
@@ -260,71 +259,62 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
 
 // getDefaultBoard return default board and ensure only one exists
 func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
-	var boards []Board
-	if err := db.GetEngine(ctx).Where("project_id=? AND `default` = ?", p.ID, true).OrderBy("sorting").Find(&boards); err != nil {
+	var board Board
+	has, err := db.GetEngine(ctx).
+		Where("project_id=? AND `default` = ?", p.ID, true).
+		Desc("id").Get(&board)
+	if err != nil {
 		return nil, err
 	}
 
-	// create a default board if none is found
-	if len(boards) == 0 {
-		board := Board{
-			ProjectID: p.ID,
-			Default:   true,
-			Title:     "Uncategorized",
-			CreatorID: p.CreatorID,
-		}
-		if _, err := db.GetEngine(ctx).Insert(); err != nil {
-			return nil, err
-		}
+	if has {
 		return &board, nil
 	}
 
-	// unset default boards where too many default boards exist
-	if len(boards) > 1 {
-		var boardsToUpdate []int64
-		for id, b := range boards {
-			if id > 0 {
-				boardsToUpdate = append(boardsToUpdate, b.ID)
-			}
-		}
-
-		if _, err := db.GetEngine(ctx).Where(builder.Eq{"project_id": p.ID}.And(builder.In("id", boardsToUpdate))).
-			Cols("`default`").Update(&Board{Default: false}); err != nil {
-			return nil, err
-		}
+	// create a default board if none is found
+	board = Board{
+		ProjectID: p.ID,
+		Default:   true,
+		Title:     "Uncategorized",
+		CreatorID: p.CreatorID,
 	}
-
-	return &boards[0], nil
+	if _, err := db.GetEngine(ctx).Insert(&board); err != nil {
+		return nil, err
+	}
+	return &board, nil
 }
 
 // SetDefaultBoard represents a board for issues not assigned to one
 func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
-	if _, err := GetBoard(ctx, boardID); err != nil {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		if _, err := GetBoard(ctx, boardID); err != nil {
+			return err
+		}
+
+		if _, err := db.GetEngine(ctx).Where(builder.Eq{
+			"project_id": projectID,
+			"`default`":  true,
+		}).Cols("`default`").Update(&Board{Default: false}); err != nil {
+			return err
+		}
+
+		_, err := db.GetEngine(ctx).ID(boardID).
+			Where(builder.Eq{"project_id": projectID}).
+			Cols("`default`").Update(&Board{Default: true})
 		return err
-	}
-
-	if _, err := db.GetEngine(ctx).Where(builder.Eq{
-		"project_id": projectID,
-		"`default`":  true,
-	}).Cols("`default`").Update(&Board{Default: false}); err != nil {
-		return err
-	}
-
-	_, err := db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}).
-		Cols("`default`").Update(&Board{Default: true})
-
-	return err
+	})
 }
 
 // UpdateBoardSorting update project board sorting
 func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
-	for i := range bs {
-		_, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
-			"sorting",
-		).Update(bs[i])
-		if err != nil {
-			return err
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		for i := range bs {
+			if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
+				"sorting",
+			).Update(bs[i]); err != nil {
+				return err
+			}
 		}
-	}
-	return nil
+		return nil
+	})
 }
diff --git a/models/project/board_test.go b/models/project/board_test.go
index c1c6f0180b..71ba29a589 100644
--- a/models/project/board_test.go
+++ b/models/project/board_test.go
@@ -31,8 +31,12 @@ func TestGetDefaultBoard(t *testing.T) {
 	board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(6), board.ProjectID)
-	assert.Equal(t, int64(8), board.ID)
+	assert.Equal(t, int64(9), board.ID)
 
+	// set 8 as default board
+	assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8))
+
+	// then 9 will become a non-default board
 	board, err = GetBoard(db.DefaultContext, 9)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(6), board.ProjectID)

From 61036235966773a0af6b690b10b33ff8222df1d7 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Fri, 29 Mar 2024 04:15:39 +0900
Subject: [PATCH 569/679] Fix `DEFAULT_SHOW_FULL_NAME=false` has no effect in
 commit list and commit graph page (#30096)

Fix #20446

This PR will fix the username in:
repo home page

![image](https://github.com/go-gitea/gitea/assets/18380374/347c0f70-ea42-432d-aae3-bf87a7e07ae1)
repo commit list page

![image](https://github.com/go-gitea/gitea/assets/18380374/b3b1f5d5-c371-4222-ac2e-64b8994c7551)
repo commit graph page

![image](https://github.com/go-gitea/gitea/assets/18380374/01b7117c-3aea-4d7d-8bd1-35e5ea942821)
pr commit page

![image](https://github.com/go-gitea/gitea/assets/18380374/4d180c30-2150-4348-8eeb-0b4b2559ec19)

Will not fix:
wiki revisions page:

![image](https://github.com/go-gitea/gitea/assets/18380374/b49df6bf-d751-4374-b7ea-1ac85e2739e3)
ps: the author name is `FullName` by default
---
 templates/repo/commits_list.tmpl  | 2 +-
 templates/repo/graph/commits.tmpl | 2 +-
 templates/repo/latest_commit.tmpl | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index bae9924141..53052333fa 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -16,7 +16,7 @@
 						<td class="author tw-flex">
 							{{$userName := .Author.Name}}
 							{{if .User}}
-								{{if .User.FullName}}
+								{{if and .User.FullName DefaultShowFullName}}
 									{{$userName = .User.FullName}}
 								{{end}}
 								{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index 96d09072da..f141dbeada 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -61,7 +61,7 @@
 					<span class="author tw-flex tw-items-center tw-mr-2">
 						{{$userName := $commit.Commit.Author.Name}}
 						{{if $commit.User}}
-							{{if $commit.User.FullName}}
+							{{if and $commit.User.FullName DefaultShowFullName}}
 								{{$userName = $commit.User.FullName}}
 							{{end}}
 							<span class="tw-mr-1">{{ctx.AvatarUtils.Avatar $commit.User}}</span>
diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl
index f945e9dfa1..8bacb427bf 100644
--- a/templates/repo/latest_commit.tmpl
+++ b/templates/repo/latest_commit.tmpl
@@ -3,7 +3,7 @@
 {{else}}
 	{{if .LatestCommitUser}}
 		{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}}
-		{{if .LatestCommitUser.FullName}}
+		{{if and .LatestCommitUser.FullName DefaultShowFullName}}
 			<a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
 		{{else}}
 			<a class="muted author-wrapper" title="{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>

From 62b073e6f31645e446c7e8d6b5a506f61b47924e Mon Sep 17 00:00:00 2001
From: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Date: Fri, 29 Mar 2024 04:40:35 +0800
Subject: [PATCH 570/679] Add API for `Variables` (#29520)

close #27801

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 models/actions/variable.go                   |  27 +-
 modules/structs/variable.go                  |  37 +
 modules/util/util.go                         |   9 +
 modules/util/util_test.go                    |   5 +
 routers/api/v1/api.go                        |  27 +
 routers/api/v1/org/variables.go              | 291 +++++++
 routers/api/v1/repo/action.go                | 296 ++++++++
 routers/api/v1/swagger/action.go             |  14 +
 routers/api/v1/swagger/options.go            |   6 +
 routers/api/v1/user/action.go                | 250 +++++++
 routers/web/shared/actions/variables.go      |  67 +-
 routers/web/shared/secrets/secrets.go        |   4 +-
 services/actions/variables.go                | 100 +++
 templates/swagger/v1_json.tmpl               | 750 ++++++++++++++++++-
 tests/integration/api_repo_variables_test.go | 149 ++++
 tests/integration/api_user_variables_test.go | 144 ++++
 16 files changed, 2102 insertions(+), 74 deletions(-)
 create mode 100644 modules/structs/variable.go
 create mode 100644 routers/api/v1/org/variables.go
 create mode 100644 services/actions/variables.go
 create mode 100644 tests/integration/api_repo_variables_test.go
 create mode 100644 tests/integration/api_user_variables_test.go

diff --git a/models/actions/variable.go b/models/actions/variable.go
index 14ded60fac..b0a455e675 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -6,13 +6,11 @@ package actions
 import (
 	"context"
 	"errors"
-	"fmt"
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 )
@@ -55,24 +53,24 @@ type FindVariablesOpts struct {
 	db.ListOptions
 	OwnerID int64
 	RepoID  int64
+	Name    string
 }
 
 func (opts FindVariablesOpts) ToConds() builder.Cond {
 	cond := builder.NewCond()
+	// Since we now support instance-level variables,
+	// there is no need to check for null values for `owner_id` and `repo_id`
 	cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
 	cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+
+	if opts.Name != "" {
+		cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
+	}
 	return cond
 }
 
-func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) {
-	var variable ActionVariable
-	has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist)
-	}
-	return &variable, nil
+func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) {
+	return db.Find[ActionVariable](ctx, opts)
 }
 
 func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
@@ -84,6 +82,13 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error)
 	return count != 0, err
 }
 
+func DeleteVariable(ctx context.Context, id int64) error {
+	if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil {
+		return err
+	}
+	return nil
+}
+
 func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
 	variables := map[string]string{}
 
diff --git a/modules/structs/variable.go b/modules/structs/variable.go
new file mode 100644
index 0000000000..cc846cf0ec
--- /dev/null
+++ b/modules/structs/variable.go
@@ -0,0 +1,37 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package structs
+
+// CreateVariableOption the option when creating variable
+// swagger:model
+type CreateVariableOption struct {
+	// Value of the variable to create
+	//
+	// required: true
+	Value string `json:"value" binding:"Required"`
+}
+
+// UpdateVariableOption the option when updating variable
+// swagger:model
+type UpdateVariableOption struct {
+	// New name for the variable. If the field is empty, the variable name won't be updated.
+	Name string `json:"name"`
+	// Value of the variable to update
+	//
+	// required: true
+	Value string `json:"value" binding:"Required"`
+}
+
+// ActionVariable return value of the query API
+// swagger:model
+type ActionVariable struct {
+	// the owner to which the variable belongs
+	OwnerID int64 `json:"owner_id"`
+	// the repository to which the variable belongs
+	RepoID int64 `json:"repo_id"`
+	// the name of the variable
+	Name string `json:"name"`
+	// the value of the variable
+	Data string `json:"data"`
+}
diff --git a/modules/util/util.go b/modules/util/util.go
index c94fb91047..b6e730eb54 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -221,3 +221,12 @@ func IfZero[T comparable](v, def T) T {
 	}
 	return v
 }
+
+func ReserveLineBreakForTextarea(input string) string {
+	// Since the content is from a form which is a textarea, the line endings are \r\n.
+	// It's a standard behavior of HTML.
+	// But we want to store them as \n like what GitHub does.
+	// And users are unlikely to really need to keep the \r.
+	// Other than this, we should respect the original content, even leading or trailing spaces.
+	return strings.ReplaceAll(input, "\r\n", "\n")
+}
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 819e12ee91..5c5b13d04b 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -235,3 +235,8 @@ func TestToPointer(t *testing.T) {
 	val123 := 123
 	assert.False(t, &val123 == ToPointer(val123))
 }
+
+func TestReserveLineBreakForTextarea(t *testing.T) {
+	assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata")
+	assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n")
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c65650c388..e870378c4b 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -955,6 +955,15 @@ func Routes() *web.Route {
 						Delete(user.DeleteSecret)
 				})
 
+				m.Group("/variables", func() {
+					m.Get("", user.ListVariables)
+					m.Combo("/{variablename}").
+						Get(user.GetVariable).
+						Delete(user.DeleteVariable).
+						Post(bind(api.CreateVariableOption{}), user.CreateVariable).
+						Put(bind(api.UpdateVariableOption{}), user.UpdateVariable)
+				})
+
 				m.Group("/runners", func() {
 					m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
 				})
@@ -1073,6 +1082,15 @@ func Routes() *web.Route {
 							Delete(reqToken(), reqOwner(), repo.DeleteSecret)
 					})
 
+					m.Group("/variables", func() {
+						m.Get("", reqToken(), reqOwner(), repo.ListVariables)
+						m.Combo("/{variablename}").
+							Get(reqToken(), reqOwner(), repo.GetVariable).
+							Delete(reqToken(), reqOwner(), repo.DeleteVariable).
+							Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
+							Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
+					})
+
 					m.Group("/runners", func() {
 						m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
 					})
@@ -1452,6 +1470,15 @@ func Routes() *web.Route {
 						Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
 				})
 
+				m.Group("/variables", func() {
+					m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
+					m.Combo("/{variablename}").
+						Get(reqToken(), reqOrgOwnership(), org.GetVariable).
+						Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
+						Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
+						Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
+				})
+
 				m.Group("/runners", func() {
 					m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
 				})
diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go
new file mode 100644
index 0000000000..eaf7bdc45b
--- /dev/null
+++ b/routers/api/v1/org/variables.go
@@ -0,0 +1,291 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+	"errors"
+	"net/http"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
+)
+
+// ListVariables list org-level variables
+func ListVariables(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
+	// ---
+	// summary: Get an org-level variables list
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//		 "$ref": "#/responses/VariableList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+		OwnerID:     ctx.Org.Organization.ID,
+		ListOptions: utils.GetListOptions(ctx),
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+		return
+	}
+
+	variables := make([]*api.ActionVariable, len(vars))
+	for i, v := range vars {
+		variables[i] = &api.ActionVariable{
+			OwnerID: v.OwnerID,
+			RepoID:  v.RepoID,
+			Name:    v.Name,
+			Data:    v.Data,
+		}
+	}
+
+	ctx.SetTotalCountHeader(count)
+	ctx.JSON(http.StatusOK, variables)
+}
+
+// GetVariable get an org-level variable
+func GetVariable(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
+	// ---
+	// summary: Get an org-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//		 "$ref": "#/responses/ActionVariable"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Org.Organization.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	variable := &api.ActionVariable{
+		OwnerID: v.OwnerID,
+		RepoID:  v.RepoID,
+		Name:    v.Name,
+		Data:    v.Data,
+	}
+
+	ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete an org-level variable
+func DeleteVariable(ctx *context.APIContext) {
+	// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
+	// ---
+	// summary: Delete an org-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "201":
+	//     description: response when deleting a variable
+	//   "204":
+	//     description: response when deleting a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+		} else if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create an org-level variable
+func CreateVariable(ctx *context.APIContext) {
+	// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
+	// ---
+	// summary: Create an org-level variable
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/CreateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when creating an org-level variable
+	//   "204":
+	//     description: response when creating an org-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+	ownerID := ctx.Org.Organization.ID
+	variableName := ctx.Params("variablename")
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ownerID,
+		Name:    variableName,
+	})
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		return
+	}
+	if v != nil && v.ID > 0 {
+		ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+		return
+	}
+
+	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update an org-level variable
+func UpdateVariable(ctx *context.APIContext) {
+	// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
+	// ---
+	// summary: Update an org-level variable
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UpdateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when updating an org-level variable
+	//   "204":
+	//     description: response when updating an org-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Org.Organization.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	if opt.Name == "" {
+		opt.Name = ctx.Params("variablename")
+	}
+	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index e0af276c71..03321d956d 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -7,9 +7,13 @@ import (
 	"errors"
 	"net/http"
 
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
 	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
@@ -127,3 +131,295 @@ func DeleteSecret(ctx *context.APIContext) {
 
 	ctx.Status(http.StatusNoContent)
 }
+
+// GetVariable get a repo-level variable
+func GetVariable(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
+	// ---
+	// summary: Get a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		RepoID: ctx.Repo.Repository.ID,
+		Name:   ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	variable := &api.ActionVariable{
+		OwnerID: v.OwnerID,
+		RepoID:  v.RepoID,
+		Name:    v.Name,
+		Data:    v.Data,
+	}
+
+	ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete a repo-level variable
+func DeleteVariable(ctx *context.APIContext) {
+	// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
+	// ---
+	// summary: Delete a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "201":
+	//     description: response when deleting a variable
+	//   "204":
+	//     description: response when deleting a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+		} else if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create a repo-level variable
+func CreateVariable(ctx *context.APIContext) {
+	// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
+	// ---
+	// summary: Create a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/CreateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when creating a repo-level variable
+	//   "204":
+	//     description: response when creating a repo-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+	repoID := ctx.Repo.Repository.ID
+	variableName := ctx.Params("variablename")
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		RepoID: repoID,
+		Name:   variableName,
+	})
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		return
+	}
+	if v != nil && v.ID > 0 {
+		ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+		return
+	}
+
+	if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a repo-level variable
+func UpdateVariable(ctx *context.APIContext) {
+	// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
+	// ---
+	// summary: Update a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UpdateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when updating a repo-level variable
+	//   "204":
+	//     description: response when updating a repo-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		RepoID: ctx.Repo.Repository.ID,
+		Name:   ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	if opt.Name == "" {
+		opt.Name = ctx.Params("variablename")
+	}
+	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// ListVariables list repo-level variables
+func ListVariables(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
+	// ---
+	// summary: Get repo-level variables list
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//		 "$ref": "#/responses/VariableList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+		RepoID:      ctx.Repo.Repository.ID,
+		ListOptions: utils.GetListOptions(ctx),
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+		return
+	}
+
+	variables := make([]*api.ActionVariable, len(vars))
+	for i, v := range vars {
+		variables[i] = &api.ActionVariable{
+			OwnerID: v.OwnerID,
+			RepoID:  v.RepoID,
+			Name:    v.Name,
+		}
+	}
+
+	ctx.SetTotalCountHeader(count)
+	ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go
index 3771780718..665f4d0b85 100644
--- a/routers/api/v1/swagger/action.go
+++ b/routers/api/v1/swagger/action.go
@@ -18,3 +18,17 @@ type swaggerResponseSecret struct {
 	// in:body
 	Body api.Secret `json:"body"`
 }
+
+// ActionVariable
+// swagger:response ActionVariable
+type swaggerResponseActionVariable struct {
+	// in:body
+	Body api.ActionVariable `json:"body"`
+}
+
+// VariableList
+// swagger:response VariableList
+type swaggerResponseVariableList struct {
+	// in:body
+	Body []api.ActionVariable `json:"body"`
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 471e7d9c4e..cd551cbdfa 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -193,4 +193,10 @@ type swaggerParameterBodies struct {
 
 	// in:body
 	UserBadgeOption api.UserBadgeOption
+
+	// in:body
+	CreateVariableOption api.CreateVariableOption
+
+	// in:body
+	UpdateVariableOption api.UpdateVariableOption
 }
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index babb8c0cf7..bf78c2c864 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -7,9 +7,13 @@ import (
 	"errors"
 	"net/http"
 
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
 	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
@@ -101,3 +105,249 @@ func DeleteSecret(ctx *context.APIContext) {
 
 	ctx.Status(http.StatusNoContent)
 }
+
+// CreateVariable create a user-level variable
+func CreateVariable(ctx *context.APIContext) {
+	// swagger:operation POST /user/actions/variables/{variablename} user createUserVariable
+	// ---
+	// summary: Create a user-level variable
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/CreateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when creating a variable
+	//   "204":
+	//     description: response when creating a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+	ownerID := ctx.Doer.ID
+	variableName := ctx.Params("variablename")
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ownerID,
+		Name:    variableName,
+	})
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		return
+	}
+	if v != nil && v.ID > 0 {
+		ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+		return
+	}
+
+	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a user-level variable which is created by current doer
+func UpdateVariable(ctx *context.APIContext) {
+	// swagger:operation PUT /user/actions/variables/{variablename} user updateUserVariable
+	// ---
+	// summary: Update a user-level variable which is created by current doer
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UpdateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when updating a variable
+	//   "204":
+	//     description: response when updating a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Doer.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	if opt.Name == "" {
+		opt.Name = ctx.Params("variablename")
+	}
+	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// DeleteVariable delete a user-level variable which is created by current doer
+func DeleteVariable(ctx *context.APIContext) {
+	// swagger:operation DELETE /user/actions/variables/{variablename} user deleteUserVariable
+	// ---
+	// summary: Delete a user-level variable which is created by current doer
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "201":
+	//     description: response when deleting a variable
+	//   "204":
+	//     description: response when deleting a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+		} else if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// GetVariable get a user-level variable which is created by current doer
+func GetVariable(ctx *context.APIContext) {
+	// swagger:operation GET /user/actions/variables/{variablename} user getUserVariable
+	// ---
+	// summary: Get a user-level variable which is created by current doer
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Doer.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	variable := &api.ActionVariable{
+		OwnerID: v.OwnerID,
+		RepoID:  v.RepoID,
+		Name:    v.Name,
+		Data:    v.Data,
+	}
+
+	ctx.JSON(http.StatusOK, variable)
+}
+
+// ListVariables list user-level variables
+func ListVariables(ctx *context.APIContext) {
+	// swagger:operation GET /user/actions/variables user getUserVariablesList
+	// ---
+	// summary: Get the user-level list of variables which is created by current doer
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/VariableList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+		OwnerID:     ctx.Doer.ID,
+		ListOptions: utils.GetListOptions(ctx),
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+		return
+	}
+
+	variables := make([]*api.ActionVariable, len(vars))
+	for i, v := range vars {
+		variables[i] = &api.ActionVariable{
+			OwnerID: v.OwnerID,
+			RepoID:  v.RepoID,
+			Name:    v.Name,
+			Data:    v.Data,
+		}
+	}
+
+	ctx.SetTotalCountHeader(count)
+	ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index 0f705399c9..79c03e4e8c 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -4,17 +4,13 @@
 package actions
 
 import (
-	"errors"
-	"regexp"
-	"strings"
-
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/web"
+	actions_service "code.gitea.io/gitea/services/actions"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
-	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
 func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
@@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
 	ctx.Data["Variables"] = variables
 }
 
-// some regular expression of `variables` and `secrets`
-// reference to:
-// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
-// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
-var (
-	forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
-)
-
-func envNameCIRegexMatch(name string) error {
-	if forbiddenEnvNameCIRx.MatchString(name) {
-		log.Error("Env Name cannot be ci")
-		return errors.New("env name cannot be ci")
-	}
-	return nil
-}
-
 func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
 
-	if err := secret_service.ValidateName(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	if err := envNameCIRegexMatch(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
+	v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
 	if err != nil {
-		log.Error("InsertVariable error: %v", err)
+		log.Error("CreateVariable: %v", err)
 		ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
 		return
 	}
+
 	ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
 	ctx.JSONRedirect(redirectURL)
 }
@@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
 	id := ctx.ParamsInt64(":variable_id")
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
 
-	if err := secret_service.ValidateName(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	if err := envNameCIRegexMatch(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
-		ID:   id,
-		Name: strings.ToUpper(form.Name),
-		Data: ReserveLineBreakForTextarea(form.Data),
-	})
-	if err != nil || !ok {
-		log.Error("UpdateVariable error: %v", err)
+	if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
+		log.Error("UpdateVariable: %v", err)
 		ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
 		return
 	}
@@ -99,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
 func DeleteVariable(ctx *context.Context, redirectURL string) {
 	id := ctx.ParamsInt64(":variable_id")
 
-	if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
+	if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
 		log.Error("Delete variable [%d] failed: %v", id, err)
 		ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
 		return
@@ -107,12 +63,3 @@ func DeleteVariable(ctx *context.Context, redirectURL string) {
 	ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
 	ctx.JSONRedirect(redirectURL)
 }
-
-func ReserveLineBreakForTextarea(input string) string {
-	// Since the content is from a form which is a textarea, the line endings are \r\n.
-	// It's a standard behavior of HTML.
-	// But we want to store them as \n like what GitHub does.
-	// And users are unlikely to really need to keep the \r.
-	// Other than this, we should respect the original content, even leading or trailing spaces.
-	return strings.ReplaceAll(input, "\r\n", "\n")
-}
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index 73505ec372..3bd421f86a 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -7,8 +7,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	secret_model "code.gitea.io/gitea/models/secret"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
-	"code.gitea.io/gitea/routers/web/shared/actions"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	secret_service "code.gitea.io/gitea/services/secrets"
@@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
 func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
 	form := web.GetForm(ctx).(*forms.AddSecretForm)
 
-	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
+	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
 	if err != nil {
 		log.Error("CreateOrUpdateSecret failed: %v", err)
 		ctx.JSONError(ctx.Tr("secrets.creation.failed"))
diff --git a/services/actions/variables.go b/services/actions/variables.go
new file mode 100644
index 0000000000..8dde9c4af5
--- /dev/null
+++ b/services/actions/variables.go
@@ -0,0 +1,100 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"context"
+	"regexp"
+	"strings"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
+	secret_service "code.gitea.io/gitea/services/secrets"
+)
+
+func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
+	if err := secret_service.ValidateName(name); err != nil {
+		return nil, err
+	}
+
+	if err := envNameCIRegexMatch(name); err != nil {
+		return nil, err
+	}
+
+	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data))
+	if err != nil {
+		return nil, err
+	}
+
+	return v, nil
+}
+
+func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
+	if err := secret_service.ValidateName(name); err != nil {
+		return false, err
+	}
+
+	if err := envNameCIRegexMatch(name); err != nil {
+		return false, err
+	}
+
+	return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
+		ID:   variableID,
+		Name: strings.ToUpper(name),
+		Data: util.ReserveLineBreakForTextarea(data),
+	})
+}
+
+func DeleteVariableByID(ctx context.Context, variableID int64) error {
+	return actions_model.DeleteVariable(ctx, variableID)
+}
+
+func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
+	if err := secret_service.ValidateName(name); err != nil {
+		return err
+	}
+
+	if err := envNameCIRegexMatch(name); err != nil {
+		return err
+	}
+
+	v, err := GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ownerID,
+		RepoID:  repoID,
+		Name:    name,
+	})
+	if err != nil {
+		return err
+	}
+
+	return actions_model.DeleteVariable(ctx, v.ID)
+}
+
+func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) {
+	vars, err := actions_model.FindVariables(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+	if len(vars) != 1 {
+		return nil, util.NewNotExistErrorf("variable not found")
+	}
+	return vars[0], nil
+}
+
+// some regular expression of `variables` and `secrets`
+// reference to:
+// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
+// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
+var (
+	forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
+)
+
+func envNameCIRegexMatch(name string) error {
+	if forbiddenEnvNameCIRx.MatchString(name) {
+		log.Error("Env Name cannot be ci")
+		return util.NewInvalidArgumentErrorf("env name cannot be ci")
+	}
+	return nil
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index ef6126ff85..b5677c77e0 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1844,6 +1844,232 @@
         }
       }
     },
+    "/orgs/{org}/actions/variables": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Get an org-level variables list",
+        "operationId": "getOrgVariablesList",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/VariableList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
+    "/orgs/{org}/actions/variables/{variablename}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Get an org-level variable",
+        "operationId": "getOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Update an org-level variable",
+        "operationId": "updateOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UpdateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when updating an org-level variable"
+          },
+          "204": {
+            "description": "response when updating an org-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Create an org-level variable",
+        "operationId": "createOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when creating an org-level variable"
+          },
+          "204": {
+            "description": "response when creating an org-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Delete an org-level variable",
+        "operationId": "deleteOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "201": {
+            "description": "response when deleting a variable"
+          },
+          "204": {
+            "description": "response when deleting a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/orgs/{org}/activities/feeds": {
       "get": {
         "produces": [
@@ -3723,6 +3949,261 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/actions/variables": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get repo-level variables list",
+        "operationId": "getRepoVariablesList",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/VariableList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
+    "/repos/{owner}/{repo}/actions/variables/{variablename}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get a repo-level variable",
+        "operationId": "getRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Update a repo-level variable",
+        "operationId": "updateRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UpdateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when updating a repo-level variable"
+          },
+          "204": {
+            "description": "response when updating a repo-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Create a repo-level variable",
+        "operationId": "createRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when creating a repo-level variable"
+          },
+          "204": {
+            "description": "response when creating a repo-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Delete a repo-level variable",
+        "operationId": "deleteRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "201": {
+            "description": "response when deleting a variable"
+          },
+          "204": {
+            "description": "response when deleting a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/activities/feeds": {
       "get": {
         "produces": [
@@ -15050,6 +15531,194 @@
         }
       }
     },
+    "/user/actions/variables": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Get the user-level list of variables which is created by current doer",
+        "operationId": "getUserVariablesList",
+        "parameters": [
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/VariableList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
+    "/user/actions/variables/{variablename}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Get a user-level variable which is created by current doer",
+        "operationId": "getUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Update a user-level variable which is created by current doer",
+        "operationId": "updateUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UpdateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when updating a variable"
+          },
+          "204": {
+            "description": "response when updating a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Create a user-level variable",
+        "operationId": "createUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when creating a variable"
+          },
+          "204": {
+            "description": "response when creating a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Delete a user-level variable which is created by current doer",
+        "operationId": "deleteUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when deleting a variable"
+          },
+          "204": {
+            "description": "response when deleting a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/user/applications/oauth2": {
       "get": {
         "produces": [
@@ -17193,6 +17862,35 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "ActionVariable": {
+      "description": "ActionVariable return value of the query API",
+      "type": "object",
+      "properties": {
+        "data": {
+          "description": "the value of the variable",
+          "type": "string",
+          "x-go-name": "Data"
+        },
+        "name": {
+          "description": "the name of the variable",
+          "type": "string",
+          "x-go-name": "Name"
+        },
+        "owner_id": {
+          "description": "the owner to which the variable belongs",
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "OwnerID"
+        },
+        "repo_id": {
+          "description": "the repository to which the variable belongs",
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "RepoID"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "Activity": {
       "type": "object",
       "properties": {
@@ -19079,6 +19777,21 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "CreateVariableOption": {
+      "description": "CreateVariableOption the option when creating variable",
+      "type": "object",
+      "required": [
+        "value"
+      ],
+      "properties": {
+        "value": {
+          "description": "Value of the variable to create",
+          "type": "string",
+          "x-go-name": "Value"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "CreateWikiPageOptions": {
       "description": "CreateWikiPageOptions form for creating wiki",
       "type": "object",
@@ -23371,6 +24084,26 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "UpdateVariableOption": {
+      "description": "UpdateVariableOption the option when updating variable",
+      "type": "object",
+      "required": [
+        "value"
+      ],
+      "properties": {
+        "name": {
+          "description": "New name for the variable. If the field is empty, the variable name won't be updated.",
+          "type": "string",
+          "x-go-name": "Name"
+        },
+        "value": {
+          "description": "Value of the variable to update",
+          "type": "string",
+          "x-go-name": "Value"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "User": {
       "description": "User represents a user",
       "type": "object",
@@ -23752,6 +24485,12 @@
         }
       }
     },
+    "ActionVariable": {
+      "description": "ActionVariable",
+      "schema": {
+        "$ref": "#/definitions/ActionVariable"
+      }
+    },
     "ActivityFeedsList": {
       "description": "ActivityFeedsList",
       "schema": {
@@ -24635,6 +25374,15 @@
         }
       }
     },
+    "VariableList": {
+      "description": "VariableList",
+      "schema": {
+        "type": "array",
+        "items": {
+          "$ref": "#/definitions/ActionVariable"
+        }
+      }
+    },
     "WatchInfo": {
       "description": "WatchInfo",
       "schema": {
@@ -24710,7 +25458,7 @@
     "parameterBodies": {
       "description": "parameterBodies",
       "schema": {
-        "$ref": "#/definitions/UserBadgeOption"
+        "$ref": "#/definitions/UpdateVariableOption"
       }
     },
     "redirect": {
diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go
new file mode 100644
index 0000000000..7847962b07
--- /dev/null
+++ b/tests/integration/api_repo_variables_test.go
@@ -0,0 +1,149 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	auth_model "code.gitea.io/gitea/models/auth"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+)
+
+func TestAPIRepoVariables(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+	t.Run("CreateRepoVariable", func(t *testing.T) {
+		cases := []struct {
+			Name           string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "-",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "_",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "TEST_VAR",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "test_var",
+				ExpectedStatus: http.StatusConflict,
+			},
+			{
+				Name:           "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "123var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "var@test",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "github_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "gitea_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.CreateVariableOption{
+				Value: "value",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("UpdateRepoVariable", func(t *testing.T) {
+		variableName := "test_update_var"
+		url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		cases := []struct {
+			Name           string
+			UpdateName     string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "not_found_var",
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "1invalid",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "invalid@name",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           variableName,
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.UpdateVariableOption{
+				Name:  c.UpdateName,
+				Value: "updated_val",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("DeleteRepoVariable", func(t *testing.T) {
+		variableName := "test_delete_var"
+		url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
+
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNotFound)
+	})
+}
diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go
new file mode 100644
index 0000000000..dd5501f0b9
--- /dev/null
+++ b/tests/integration/api_user_variables_test.go
@@ -0,0 +1,144 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	auth_model "code.gitea.io/gitea/models/auth"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+)
+
+func TestAPIUserVariables(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	session := loginUser(t, "user1")
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
+
+	t.Run("CreateRepoVariable", func(t *testing.T) {
+		cases := []struct {
+			Name           string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "-",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "_",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "TEST_VAR",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "test_var",
+				ExpectedStatus: http.StatusConflict,
+			},
+			{
+				Name:           "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "123var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "var@test",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "github_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "gitea_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.CreateVariableOption{
+				Value: "value",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("UpdateRepoVariable", func(t *testing.T) {
+		variableName := "test_update_var"
+		url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		cases := []struct {
+			Name           string
+			UpdateName     string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "not_found_var",
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "1invalid",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "invalid@name",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           variableName,
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.UpdateVariableOption{
+				Name:  c.UpdateName,
+				Value: "updated_val",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("DeleteRepoVariable", func(t *testing.T) {
+		variableName := "test_delete_var"
+		url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
+
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNotFound)
+	})
+}

From dd8dde2be89921b2b1497c6cc5eafdde213429cb Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 29 Mar 2024 04:00:07 +0100
Subject: [PATCH 571/679] replace jquery-minicolors with coloris (#30055)

Get rid of one more jQuery dependant and have a nicer color picker as
well.

Now there is only a single global color picker init because that is all
that's necessary because the elements are present on the page when the
init code runs. The init is slightly weird because the module only takes
a selector instead of DOM elements directly.

The label modals now also perform form validation because previously it
was possible to trigger a 500 error `Color cannot be empty.` by clearing
out the color value on labels.

<img width="867" alt="Screenshot 2024-03-25 at 00 21 05"
src="https://github.com/go-gitea/gitea/assets/115237/71215c39-abb1-4881-b5c1-9954b4a89adb">
<img width="860" alt="Screenshot 2024-03-25 at 00 20 48"
src="https://github.com/go-gitea/gitea/assets/115237/a12cb68f-c38b-4433-ba05-53bbb4b1023e">
---
 .dockerignore                                 |   1 -
 .gitignore                                    |   1 -
 Makefile                                      |   2 +-
 package-lock.json                             |  15 +-
 package.json                                  |   2 +-
 templates/projects/view.tmpl                  |   8 +-
 .../repo/issue/labels/edit_delete_label.tmpl  |   4 +-
 templates/repo/issue/labels/label_new.tmpl    |   4 +-
 web_src/css/base.css                          |   5 -
 web_src/css/features/colorpicker.css          | 164 ++++++++++++++++++
 web_src/css/features/projects.css             |  23 ---
 web_src/css/repo.css                          |  18 --
 web_src/js/features/colorpicker.js            |  35 +++-
 web_src/js/features/common-global.js          |   6 +-
 web_src/js/features/comp/ColorPicker.js       |  16 --
 web_src/js/features/comp/LabelEdit.js         |  17 +-
 web_src/js/index.js                           |   2 +
 webpack.config.js                             |   7 -
 18 files changed, 224 insertions(+), 106 deletions(-)
 create mode 100644 web_src/css/features/colorpicker.css
 delete mode 100644 web_src/js/features/comp/ColorPicker.js

diff --git a/.dockerignore b/.dockerignore
index 7143c039fd..b299c7313d 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -78,7 +78,6 @@ cpu.out
 /public/assets/css
 /public/assets/fonts
 /public/assets/img/avatar
-/public/assets/img/webpack
 /vendor
 /web_src/fomantic/node_modules
 /web_src/fomantic/build/*
diff --git a/.gitignore b/.gitignore
index abf9565cff..501fef7dcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,7 +77,6 @@ cpu.out
 /public/assets/css
 /public/assets/fonts
 /public/assets/licenses.txt
-/public/assets/img/webpack
 /vendor
 /web_src/fomantic/node_modules
 /web_src/fomantic/build/*
diff --git a/Makefile b/Makefile
index b4fa62e05e..8489520920 100644
--- a/Makefile
+++ b/Makefile
@@ -119,7 +119,7 @@ FOMANTIC_WORK_DIR := web_src/fomantic
 WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
 WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
 WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
-WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/img/webpack
+WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
 
 BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
 BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
diff --git a/package-lock.json b/package-lock.json
index fa4f80fbe8..25fe14e1a6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,11 @@
         "@citation-js/plugin-bibtex": "0.7.9",
         "@citation-js/plugin-csl": "0.7.9",
         "@citation-js/plugin-software-formats": "0.6.1",
-        "@claviska/jquery-minicolors": "2.3.6",
         "@github/markdown-toolbar-element": "2.2.3",
         "@github/relative-time-element": "4.4.0",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+        "@melloware/coloris": "0.23.0",
         "@primer/octicons": "19.9.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
@@ -394,14 +394,6 @@
         "node": ">=14.0.0"
       }
     },
-    "node_modules/@claviska/jquery-minicolors": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/@claviska/jquery-minicolors/-/jquery-minicolors-2.3.6.tgz",
-      "integrity": "sha512-8Ro6D4GCrmOl41+6w4NFhEOpx8vjxwVRI69bulXsFDt49uVRKhLU5TnzEV7AmOJrylkVq+ugnYNMiGHBieeKUQ==",
-      "peerDependencies": {
-        "jquery": ">= 1.7.x"
-      }
-    },
     "node_modules/@csstools/css-parser-algorithms": {
       "version": "2.6.1",
       "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
@@ -1297,6 +1289,11 @@
         "@mcaptcha/core-glue": "^0.1.0-alpha-5"
       }
     },
+    "node_modules/@melloware/coloris": {
+      "version": "0.23.0",
+      "resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
+      "integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
diff --git a/package.json b/package.json
index b5bfda9dc6..d5a1d46056 100644
--- a/package.json
+++ b/package.json
@@ -8,11 +8,11 @@
     "@citation-js/plugin-bibtex": "0.7.9",
     "@citation-js/plugin-csl": "0.7.9",
     "@citation-js/plugin-software-formats": "0.6.1",
-    "@claviska/jquery-minicolors": "2.3.6",
     "@github/markdown-toolbar-element": "2.2.3",
     "@github/relative-time-element": "4.4.0",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+    "@melloware/coloris": "0.23.0",
     "@primer/octicons": "19.9.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index b45174b086..33dd758c79 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -42,8 +42,8 @@
 
 						<div class="field color-field">
 							<label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
-							<div class="color picker column">
-								<input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
+							<div class="js-color-picker-input column">
+								<input maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
 								{{template "repo/issue/label_precolors"}}
 							</div>
 						</div>
@@ -114,8 +114,8 @@
 
 											<div class="field color-field">
 												<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
-												<div class="color picker column">
-													<input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
+												<div class="js-color-picker-input column">
+													<input maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
 													{{template "repo/issue/label_precolors"}}
 												</div>
 											</div>
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index 98e0f47020..fcf69217ea 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -52,8 +52,8 @@
 			</div>
 			<div class="field color-field">
 				<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
-				<div class="color picker column">
-					<input class="color-picker" name="color" value="#70c24a" required maxlength="7">
+				<div class="column js-color-picker-input">
+					<input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7">
 					{{template "repo/issue/label_precolors"}}
 				</div>
 			</div>
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl
index 2b2b2336c4..32fd8e76d7 100644
--- a/templates/repo/issue/labels/label_new.tmpl
+++ b/templates/repo/issue/labels/label_new.tmpl
@@ -27,8 +27,8 @@
 			</div>
 			<div class="field color-field">
 				<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
-				<div class="color picker column">
-					<input class="color-picker" name="color" value="#70c24a" required maxlength="7">
+				<div class="js-color-picker-input column">
+					<input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7">
 					{{template "repo/issue/label_precolors"}}
 				</div>
 			</div>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 368bc56126..21090f67ba 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1436,11 +1436,6 @@ table th[data-sortt-desc] .svg {
   vertical-align: -0.15em;
 }
 
-/* for the jquery.minicolors plugin */
-.minicolors-panel {
-  background: var(--color-secondary-dark-1) !important;
-}
-
 .ui.tabular.menu {
   border-color: var(--color-secondary);
 }
diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css
new file mode 100644
index 0000000000..0c651cfeb3
--- /dev/null
+++ b/web_src/css/features/colorpicker.css
@@ -0,0 +1,164 @@
+/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
+   opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
+   based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
+
+.js-color-picker-input {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.js-color-picker-input input {
+  padding-top: 8px !important;
+  padding-bottom: 8px !important;
+  padding-left: 32px !important;
+}
+
+.clr-picker {
+  display: none;
+  flex-wrap: wrap;
+  position: absolute;
+  width: 200px;
+  z-index: 1002; /* above .ui.modal which has 1001 */
+  border-radius: var(--border-radius);
+  background-color: var(--color-menu);
+  justify-content: flex-end;
+  direction: ltr;
+  box-shadow: 0 5px 20px var(--color-shadow);
+  user-select: none;
+}
+
+.clr-picker.clr-open {
+  display: flex;
+}
+
+.clr-gradient {
+  position: relative;
+  width: 100%;
+  height: 100px;
+  border-radius: 3px 3px 0 0;
+  background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+  cursor: pointer;
+}
+
+.clr-marker {
+  position: absolute;
+  width: 12px;
+  height: 12px;
+  margin: -6px 0 0 -6px;
+  border: 1px solid var(--color-white);
+  border-radius: 50%;
+  background-color: currentcolor;
+  cursor: pointer;
+}
+
+.clr-picker input[type="range"]::-webkit-slider-runnable-track {
+  width: 100%;
+  height: 16px;
+}
+
+.clr-picker input[type="range"]::-webkit-slider-thumb {
+  width: 16px;
+  height: 16px;
+  -webkit-appearance: none;
+}
+
+.clr-picker input[type="range"]::-moz-range-track {
+  width: 100%;
+  height: 16px;
+  border: 0;
+}
+
+.clr-picker input[type="range"]::-moz-range-thumb {
+  width: 16px;
+  height: 16px;
+  border: 0;
+}
+
+.clr-hue {
+  background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+  position: relative;
+  width: calc(100% - 40px);
+  height: 10px;
+  margin: 10px 20px;
+  border-radius: 4px;
+}
+
+.clr-hue input[type="range"] {
+  position: absolute;
+  width: calc(100% + 32px);
+  margin: 0;
+  background-color: transparent;
+  opacity: 0;
+  cursor: pointer;
+  appearance: none;
+}
+
+.clr-hue div {
+  position: absolute;
+  width: 16px;
+  height: 16px;
+  left: 0;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  border: 2px solid var(--color-white);
+  border-radius: 50%;
+  background-color: currentcolor;
+  box-shadow: 0 0 1px var(--color-shadow);
+  pointer-events: none;
+}
+
+.clr-field {
+  flex: 1;
+  position: relative;
+  color: transparent;
+}
+
+.clr-field button {
+  position: absolute;
+  aspect-ratio: 1;
+  height: 16px;
+  left: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  margin: 0;
+  padding: 0;
+  border: 0;
+  color: inherit;
+  pointer-events: none;
+  border-radius: 2px;
+  background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+  background-position: 0 0, 4px 4px;
+  background-size: 8px 8px;
+}
+
+.clr-field button::after {
+  content: "";
+  display: block;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  top: 0;
+  border-radius: inherit;
+  background-color: currentcolor;
+}
+
+.clr-marker:focus {
+  outline: none;
+}
+
+.clr-keyboard-nav .clr-marker:focus,
+.clr-keyboard-nav .clr-hue input:focus + div,
+.clr-keyboard-nav .clr-alpha input:focus + div {
+  outline: none;
+  box-shadow: 0 0 2px 2px var(--color-white);
+}
+
+.clr-picker .clr-preview,
+.clr-picker .clr-clear,
+.clr-picker .clr-swatches,
+.clr-picker .clr-format,
+.clr-picker .clr-alpha,
+.clr-picker .clr-color {
+  display: none;
+}
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index 30df994c38..cec5e6fc64 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -102,26 +102,3 @@
 .card-ghost * {
   opacity: 0;
 }
-
-.color-field .minicolors.minicolors-theme-default {
-  display: block;
-}
-
-.color-field .minicolors.minicolors-theme-default .minicolors-input {
-  height: 38px;
-  padding-left: 2rem;
-}
-
-.color-field .minicolors.minicolors-theme-default .minicolors-swatch {
-  top: 10px;
-}
-
-.edit-project-column-modal .color.picker.column,
-.new-project-column-modal .color.picker.column {
-  display: flex;
-}
-
-.edit-project-column-modal .color.picker.column .minicolors,
-.new-project-column-modal .color.picker.column .minicolors {
-  flex: 1;
-}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 18f28dc4a6..780093fb7f 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2260,24 +2260,6 @@
   padding-top: 15px;
 }
 
-.edit-label.modal .form .color.picker.column,
-.new-label.modal .form .color.picker.column {
-  display: flex;
-}
-
-.edit-label.modal .form .color.picker.column .minicolors,
-.new-label.modal .form .color.picker.column .minicolors {
-  flex: 1;
-}
-
-.edit-label.modal .form .minicolors-swatch.minicolors-sprite,
-.new-label.modal .form .minicolors-swatch.minicolors-sprite {
-  top: 10px;
-  left: 10px;
-  width: 15px;
-  height: 15px;
-}
-
 .tab-size-1 {
   tab-size: 1 !important;
   -moz-tab-size: 1 !important;
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
index df0353376d..f342598e66 100644
--- a/web_src/js/features/colorpicker.js
+++ b/web_src/js/features/colorpicker.js
@@ -1,12 +1,31 @@
-import $ from 'jquery';
+export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
+  const inputEls = document.querySelectorAll(selector);
+  if (!inputEls.length) return;
 
-export async function createColorPicker(els) {
-  if (!els.length) return;
-
-  await Promise.all([
-    import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors'),
-    import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors/jquery.minicolors.css'),
+  const [{coloris, init}] = await Promise.all([
+    import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
+    import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
   ]);
 
-  return $(els).minicolors();
+  init();
+  coloris({
+    el: selector,
+    alpha: false,
+    focusInput: true,
+    selectInput: false,
+    ...opts,
+  });
+
+  for (const inputEl of inputEls) {
+    const parent = inputEl.closest('.js-color-picker-input');
+    // prevent tabbing on the color preview `button` inside the input
+    parent.querySelector('button').tabIndex = -1;
+    // init precolors
+    for (const el of parent.querySelectorAll('.precolors .color')) {
+      el.addEventListener('click', (e) => {
+        inputEl.value = e.target.getAttribute('data-color-hex');
+        inputEl.dispatchEvent(new Event('input', {bubbles: true}));
+      });
+    }
+  }
 }
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 18849ba7c1..ce702f041f 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
 import '../vendor/jquery.are-you-sure.js';
 import {clippie} from 'clippie';
 import {createDropzone} from './dropzone.js';
-import {initCompColorPicker} from './comp/ColorPicker.js';
 import {showGlobalErrorMessage} from '../bootstrap.js';
 import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
 import {svg} from '../svg.js';
@@ -379,10 +378,7 @@ function initGlobalShowModal() {
         $attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
       }
     }
-    const $colorPickers = $modal.find('.color-picker');
-    if ($colorPickers.length > 0) {
-      initCompColorPicker(); // FIXME: this might cause duplicate init
-    }
+
     $modal.modal('setting', {
       onApprove: () => {
         // "form-fetch-action" can handle network errors gracefully,
diff --git a/web_src/js/features/comp/ColorPicker.js b/web_src/js/features/comp/ColorPicker.js
deleted file mode 100644
index d7e7038803..0000000000
--- a/web_src/js/features/comp/ColorPicker.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import $ from 'jquery';
-import {createColorPicker} from '../colorpicker.js';
-
-export function initCompColorPicker() {
-  (async () => {
-    await createColorPicker(document.querySelectorAll('.color-picker'));
-
-    for (const el of document.querySelectorAll('.precolors .color')) {
-      el.addEventListener('click', (e) => {
-        const color = e.target.getAttribute('data-color-hex');
-        const parent = e.target.closest('.color.picker');
-        $(parent.querySelector('.color-picker')).minicolors('value', color);
-      });
-    }
-  })();
-}
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index 843657a6b6..2cc75cc6b0 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -1,5 +1,4 @@
 import $ from 'jquery';
-import {initCompColorPicker} from './ColorPicker.js';
 
 function isExclusiveScopeName(name) {
   return /.*[^/]\/[^/].*/.test(name);
@@ -28,13 +27,17 @@ function updateExclusiveLabelEdit(form) {
 
 export function initCompLabelEdit(selector) {
   if (!$(selector).length) return;
-  initCompColorPicker();
 
   // Create label
   $('.new-label.button').on('click', () => {
     updateExclusiveLabelEdit('.new-label');
     $('.new-label.modal').modal({
       onApprove() {
+        const form = document.querySelector('.new-label.form');
+        if (!form.checkValidity()) {
+          form.reportValidity();
+          return false;
+        }
         $('.new-label.form').trigger('submit');
       },
     }).modal('show');
@@ -60,10 +63,18 @@ export function initCompLabelEdit(selector) {
     updateExclusiveLabelEdit('.edit-label');
 
     $('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
-    $('.edit-label .color-picker').minicolors('value', this.getAttribute('data-color'));
+
+    const colorInput = document.querySelector('.edit-label .js-color-picker-input input');
+    colorInput.value = this.getAttribute('data-color');
+    colorInput.dispatchEvent(new Event('input', {bubbles: true}));
 
     $('.edit-label.modal').modal({
       onApprove() {
+        const form = document.querySelector('.edit-label.form');
+        if (!form.checkValidity()) {
+          form.reportValidity();
+          return false;
+        }
         $('.edit-label.form').trigger('submit');
       },
     }).modal('show');
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 4c707486bd..fc2f6b9b0b 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -86,6 +86,7 @@ import {initRepoRecentCommits} from './features/recent-commits.js';
 import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 import {initDirAuto} from './modules/dirauto.js';
 import {initRepositorySearch} from './features/repo-search.js';
+import {initColorPickers} from './features/colorpicker.js';
 
 // Init Gitea's Fomantic settings
 initGiteaFomantic();
@@ -188,4 +189,5 @@ onDomReady(() => {
   initRepoDiffView();
   initPdfViewer();
   initScopedAccessTokenCategories();
+  initColorPickers();
 });
diff --git a/webpack.config.js b/webpack.config.js
index 0b0e7403e8..fdf80a5313 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -192,13 +192,6 @@ export default {
           filename: 'fonts/[name].[contenthash:8][ext]',
         },
       },
-      {
-        test: /\.png$/i,
-        type: 'asset/resource',
-        generator: {
-          filename: 'img/webpack/[name].[contenthash:8][ext]',
-        },
-      },
     ],
   },
   plugins: [

From 8acc7aab4c254c4819f45e512b86cf5a4255091f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 29 Mar 2024 11:38:16 +0800
Subject: [PATCH 572/679] Refactor topic Find functions and add more tests for
 pagination (#30127)

This also fixed #22238
---
 models/repo/topic.go                     | 31 ++++++++++--------------
 models/repo/topic_test.go                | 14 +++++------
 routers/api/v1/repo/topic.go             |  7 +++---
 routers/web/explore/topic.go             |  2 +-
 routers/web/repo/view.go                 |  2 +-
 tests/integration/api_repo_topic_test.go | 22 ++++++++++++++++-
 tests/integration/repo_topic_test.go     | 24 +++++++++++++++++-
 7 files changed, 70 insertions(+), 32 deletions(-)

diff --git a/models/repo/topic.go b/models/repo/topic.go
index 79b13e320d..430a60f603 100644
--- a/models/repo/topic.go
+++ b/models/repo/topic.go
@@ -178,7 +178,7 @@ type FindTopicOptions struct {
 	Keyword string
 }
 
-func (opts *FindTopicOptions) toConds() builder.Cond {
+func (opts *FindTopicOptions) ToConds() builder.Cond {
 	cond := builder.NewCond()
 	if opts.RepoID > 0 {
 		cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
@@ -191,29 +191,24 @@ func (opts *FindTopicOptions) toConds() builder.Cond {
 	return cond
 }
 
-// FindTopics retrieves the topics via FindTopicOptions
-func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, error) {
-	sess := db.GetEngine(ctx).Select("topic.*").Where(opts.toConds())
+func (opts *FindTopicOptions) ToOrders() string {
 	orderBy := "topic.repo_count DESC"
 	if opts.RepoID > 0 {
-		sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
 		orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
 	}
-	if opts.PageSize != 0 && opts.Page != 0 {
-		sess = db.SetSessionPagination(sess, opts)
-	}
-	topics := make([]*Topic, 0, 10)
-	total, err := sess.OrderBy(orderBy).FindAndCount(&topics)
-	return topics, total, err
+	return orderBy
 }
 
-// CountTopics counts the number of topics matching the FindTopicOptions
-func CountTopics(ctx context.Context, opts *FindTopicOptions) (int64, error) {
-	sess := db.GetEngine(ctx).Where(opts.toConds())
-	if opts.RepoID > 0 {
-		sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
+func (opts *FindTopicOptions) ToJoins() []db.JoinFunc {
+	if opts.RepoID <= 0 {
+		return nil
+	}
+	return []db.JoinFunc{
+		func(e db.Engine) error {
+			e.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
+			return nil
+		},
 	}
-	return sess.Count(new(Topic))
 }
 
 // GetRepoTopicByName retrieves topic from name for a repo if it exist
@@ -283,7 +278,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e
 
 // SaveTopics save topics to a repository
 func SaveTopics(ctx context.Context, repoID int64, topicNames ...string) error {
-	topics, _, err := FindTopics(ctx, &FindTopicOptions{
+	topics, err := db.Find[Topic](ctx, &FindTopicOptions{
 		RepoID: repoID,
 	})
 	if err != nil {
diff --git a/models/repo/topic_test.go b/models/repo/topic_test.go
index 2b609e6d66..1600896b6e 100644
--- a/models/repo/topic_test.go
+++ b/models/repo/topic_test.go
@@ -19,18 +19,18 @@ func TestAddTopic(t *testing.T) {
 
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	topics, _, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+	topics, err := db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, topics, totalNrOfTopics)
 
-	topics, total, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, total, err := db.FindAndCount[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		ListOptions: db.ListOptions{Page: 1, PageSize: 2},
 	})
 	assert.NoError(t, err)
 	assert.Len(t, topics, 2)
 	assert.EqualValues(t, 6, total)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		RepoID: 1,
 	})
 	assert.NoError(t, err)
@@ -38,11 +38,11 @@ func TestAddTopic(t *testing.T) {
 
 	assert.NoError(t, repo_model.SaveTopics(db.DefaultContext, 2, "golang"))
 	repo2NrOfTopics := 1
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, topics, totalNrOfTopics)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		RepoID: 2,
 	})
 	assert.NoError(t, err)
@@ -55,11 +55,11 @@ func TestAddTopic(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, topic.RepoCount)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, topics, totalNrOfTopics)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		RepoID: 2,
 	})
 	assert.NoError(t, err)
diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go
index 1d8e675bde..9852caa989 100644
--- a/routers/api/v1/repo/topic.go
+++ b/routers/api/v1/repo/topic.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"strings"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
@@ -53,7 +54,7 @@ func ListTopics(ctx *context.APIContext) {
 		RepoID:      ctx.Repo.Repository.ID,
 	}
 
-	topics, total, err := repo_model.FindTopics(ctx, opts)
+	topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -172,7 +173,7 @@ func AddTopic(ctx *context.APIContext) {
 	}
 
 	// Prevent adding more topics than allowed to repo
-	count, err := repo_model.CountTopics(ctx, &repo_model.FindTopicOptions{
+	count, err := db.Count[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
 		RepoID: ctx.Repo.Repository.ID,
 	})
 	if err != nil {
@@ -287,7 +288,7 @@ func TopicSearch(ctx *context.APIContext) {
 		ListOptions: utils.GetListOptions(ctx),
 	}
 
-	topics, total, err := repo_model.FindTopics(ctx, opts)
+	topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index 95fecfe2b8..b4507ba28d 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -23,7 +23,7 @@ func TopicSearch(ctx *context.Context) {
 		},
 	}
 
-	topics, total, err := repo_model.FindTopics(ctx, opts)
+	topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError)
 		return
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 73a7be4e89..93e0f5bcbd 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -899,7 +899,7 @@ func renderLanguageStats(ctx *context.Context) {
 }
 
 func renderRepoTopics(ctx *context.Context) {
-	topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
+	topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
 		RepoID: ctx.Repo.Repository.ID,
 	})
 	if err != nil {
diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go
index c41bc4abb6..a10e159b78 100644
--- a/tests/integration/api_repo_topic_test.go
+++ b/tests/integration/api_repo_topic_test.go
@@ -26,14 +26,34 @@ func TestAPITopicSearch(t *testing.T) {
 		TopicNames []*api.TopicResponse `json:"topics"`
 	}
 
+	// search all topics
+	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 6)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// pagination search topics first page
+	topics.TopicNames = nil
 	query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 
 	searchURL.RawQuery = query.Encode()
-	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 	DecodeJSON(t, res, &topics)
 	assert.Len(t, topics.TopicNames, 4)
 	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
 
+	// pagination search topics second page
+	topics.TopicNames = nil
+	query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
+
+	searchURL.RawQuery = query.Encode()
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 2)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// add keyword search
+	query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 	query.Add("q", "topic")
 	searchURL.RawQuery = query.Encode()
 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
diff --git a/tests/integration/repo_topic_test.go b/tests/integration/repo_topic_test.go
index 58fee8418f..f198397007 100644
--- a/tests/integration/repo_topic_test.go
+++ b/tests/integration/repo_topic_test.go
@@ -21,20 +21,42 @@ func TestTopicSearch(t *testing.T) {
 		TopicNames []*api.TopicResponse `json:"topics"`
 	}
 
+	// search all topics
+	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 6)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// pagination search topics
+	topics.TopicNames = nil
 	query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 
 	searchURL.RawQuery = query.Encode()
-	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 	DecodeJSON(t, res, &topics)
 	assert.Len(t, topics.TopicNames, 4)
 	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
 
+	// second page
+	topics.TopicNames = nil
+	query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
+
+	searchURL.RawQuery = query.Encode()
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 2)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// add keyword search
+	topics.TopicNames = nil
+	query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 	query.Add("q", "topic")
 	searchURL.RawQuery = query.Encode()
 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 	DecodeJSON(t, res, &topics)
 	assert.Len(t, topics.TopicNames, 2)
 
+	topics.TopicNames = nil
 	query.Set("q", "database")
 	searchURL.RawQuery = query.Encode()
 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)

From 8fd15990c5c8980caf2b9ffefc0b3427efacdc04 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 29 Mar 2024 05:56:01 +0100
Subject: [PATCH 573/679] Remove fomantic checkbox module (#30162)

CSS is pretty slim already and the `.ui.toggle.checkbox` sliders on
admin page also still work. The only necessary JS is the one that links
`input` and `label` so that it can be toggled via label. All checkboxes
except the markdown ones render at `--checkbox-size: 16px` now.

<img width="174" alt="Screenshot 2024-03-28 at 22 15 10"
src="https://github.com/go-gitea/gitea/assets/115237/3455c1bb-166b-47e4-9847-2d20dd1f04db">

<img width="499" alt="Screenshot 2024-03-28 at 21 00 07"
src="https://github.com/go-gitea/gitea/assets/115237/412be2b3-d5a0-478a-b17b-43e6bc12e8ce">

<img width="83" alt="Screenshot 2024-03-28 at 22 14 34"
src="https://github.com/go-gitea/gitea/assets/115237/d8c89838-a420-4723-8c49-89405bb39474">

---------

Co-authored-by: delvh <dev.lh@web.de>
---
 templates/admin/config_settings.tmpl          |   4 +-
 .../repo/issue/view_content/sidebar.tmpl      |   2 +-
 web_src/css/base.css                          |   1 +
 web_src/css/form.css                          |  59 +-
 web_src/css/index.css                         |   1 +
 web_src/css/modules/animations.css            |   1 -
 web_src/css/modules/checkbox.css              | 120 +++
 web_src/css/org.css                           |   4 -
 web_src/css/repo/issue-list.css               |   1 +
 web_src/fomantic/build/semantic.css           | 709 --------------
 web_src/fomantic/build/semantic.js            | 877 ------------------
 web_src/fomantic/semantic.json                |   1 -
 web_src/js/features/admin/common.js           |  23 +-
 web_src/js/features/common-global.js          |   2 -
 web_src/js/features/repo-issue.js             |  42 +-
 web_src/js/modules/fomantic.js                |   2 -
 web_src/js/modules/fomantic/aria.md           |  17 +-
 web_src/js/modules/fomantic/checkbox.js       |  43 +-
 18 files changed, 180 insertions(+), 1729 deletions(-)
 create mode 100644 web_src/css/modules/checkbox.css

diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
index d7fb022274..02ab5fd0fb 100644
--- a/templates/admin/config_settings.tmpl
+++ b/templates/admin/config_settings.tmpl
@@ -7,14 +7,14 @@
 		<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
 		<dd>
 			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
-				<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
+				<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}><label></label>
 			</div>
 		</dd>
 		<div class="divider"></div>
 		<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
 		<dd>
 			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
-				<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
+				<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}><label></label>
 			</div>
 		</dd>
 	</dl>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 5913916ae4..c917c78e68 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -677,7 +677,7 @@
 		{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
 			<div class="divider"></div>
 			<div class="inline field">
-				<div class="ui checkbox" id="allow-edits-from-maintainers"
+				<div class="ui checkbox small-loading-icon" id="allow-edits-from-maintainers"
 						data-url="{{.Issue.Link}}"
 						data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
 						data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 21090f67ba..cd0f883138 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -23,6 +23,7 @@
   --height-loading: 16rem;
   --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
   --tab-size: 4;
+  --checkbox-size: 16px; /* height and width of checkbox and radio inputs */
 }
 
 :root * {
diff --git a/web_src/css/form.css b/web_src/css/form.css
index ca65b677d7..2a976c056d 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -32,10 +32,7 @@ textarea,
 .ui.form input[type="text"],
 .ui.form input[type="time"],
 .ui.form input[type="url"],
-.ui.selection.dropdown,
-.ui.checkbox label::before,
-.ui.checkbox input:checked ~ label::before,
-.ui.checkbox input:not([type="radio"]):indeterminate ~ label::before {
+.ui.selection.dropdown {
   background: var(--color-input-background);
   border-color: var(--color-input-border);
   color: var(--color-input-text);
@@ -63,12 +60,7 @@ textarea:hover,
 .ui.form input[type="text"]:hover,
 .ui.form input[type="time"]:hover,
 .ui.form input[type="url"]:hover,
-.ui.selection.dropdown:hover,
-.ui.checkbox label:hover::before,
-.ui.checkbox label:active::before,
-.ui.radio.checkbox label::after,
-.ui.radio.checkbox input:focus ~ label::before,
-.ui.radio.checkbox input:checked ~ label::before {
+.ui.selection.dropdown:hover {
   background: var(--color-input-background);
   border-color: var(--color-input-border-hover);
   color: var(--color-input-text);
@@ -91,11 +83,7 @@ textarea:focus,
 .ui.form input[type="text"]:focus,
 .ui.form input[type="time"]:focus,
 .ui.form input[type="url"]:focus,
-.ui.selection.dropdown:focus,
-.ui.checkbox input:focus ~ label::before,
-.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::before,
-.ui.checkbox input:checked:focus ~ label::before,
-.ui.radio.checkbox input:focus:checked ~ label::before {
+.ui.selection.dropdown:focus {
   background: var(--color-input-background);
   border-color: var(--color-primary);
   color: var(--color-input-text);
@@ -106,58 +94,21 @@ textarea:focus,
 .ui.form .inline.fields .field > label,
 .ui.form .inline.fields .field > p,
 .ui.form .inline.field > label,
-.ui.form .inline.field > p,
-.ui.checkbox label,
-.ui.checkbox + label,
-.ui.checkbox label:hover,
-.ui.checkbox + label:hover,
-.ui.checkbox input:focus ~ label,
-.ui.checkbox input:active ~ label {
+.ui.form .inline.field > p {
   color: var(--color-text);
 }
 
 .ui.form .required.fields:not(.grouped) > .field > label::after,
 .ui.form .required.fields.grouped > label::after,
 .ui.form .required.field > label::after,
-.ui.form .required.fields:not(.grouped) > .field > .checkbox::after,
-.ui.form .required.field > .checkbox::after,
 .ui.form label.required::after {
   color: var(--color-red);
 }
 
-.ui.input,
-.ui.checkbox input:focus ~ label::after,
-.ui.checkbox input:checked ~ label::after,
-.ui.checkbox label:active::after,
-.ui.checkbox input:not([type="radio"]):indeterminate ~ label::after,
-.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::after,
-.ui.checkbox input:checked:focus ~ label::after,
-.ui.disabled.checkbox label,
-.ui.checkbox input[disabled] ~ label {
+.ui.input {
   color: var(--color-input-text);
 }
 
-.ui.radio.checkbox input:focus ~ label::after,
-.ui.radio.checkbox input:checked ~ label::after,
-.ui.radio.checkbox input:focus:checked ~ label::after {
-  background: var(--color-input-text);
-}
-
-.ui.toggle.checkbox label::before {
-  background: var(--color-input-toggle-background);
-}
-
-.ui.toggle.checkbox label,
-.ui.toggle.checkbox input:checked ~ label,
-.ui.toggle.checkbox input:focus:checked ~ label {
-  color: var(--color-text) !important;
-}
-
-.ui.toggle.checkbox input:checked ~ label::before,
-.ui.toggle.checkbox input:focus:checked ~ label::before {
-  background: var(--color-primary) !important;
-}
-
 /* match <select> padding to <input> */
 .ui.form select {
   padding: 0.67857143em 1em;
diff --git a/web_src/css/index.css b/web_src/css/index.css
index aa3f6ac48e..373a84cf6a 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -12,6 +12,7 @@
 @import "./modules/message.css";
 @import "./modules/table.css";
 @import "./modules/card.css";
+@import "./modules/checkbox.css";
 @import "./modules/modal.css";
 
 @import "./modules/select.css";
diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index 788a4ed6ed..0f78ad25cb 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -6,7 +6,6 @@
 .is-loading {
   pointer-events: none !important;
   position: relative !important;
-  overflow: hidden !important;
 }
 
 .is-loading > * {
diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css
new file mode 100644
index 0000000000..fc44a7c115
--- /dev/null
+++ b/web_src/css/modules/checkbox.css
@@ -0,0 +1,120 @@
+/* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+input[type="checkbox"],
+input[type="radio"] {
+  width: var(--checkbox-size);
+  height: var(--checkbox-size);
+}
+
+.ui.checkbox {
+  position: relative;
+  display: inline-block;
+  vertical-align: baseline;
+  min-height: var(--checkbox-size);
+  line-height: var(--checkbox-size);
+  min-width: var(--checkbox-size);
+  padding: 1px;
+}
+
+.ui.checkbox input[type="checkbox"],
+.ui.checkbox input[type="radio"] {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: var(--checkbox-size);
+  height: var(--checkbox-size);
+}
+
+.ui.checkbox input[type="checkbox"]:enabled,
+.ui.checkbox input[type="radio"]:enabled,
+.ui.checkbox label:enabled {
+  cursor: pointer;
+}
+
+.ui.checkbox label {
+  cursor: auto;
+  position: relative;
+  display: block;
+  user-select: none;
+}
+
+.ui.checkbox label,
+.ui.radio.checkbox label {
+  padding-left: 1.85714em;
+}
+
+.ui.checkbox + label {
+  vertical-align: middle;
+}
+
+.ui.disabled.checkbox label,
+.ui.checkbox input[disabled] ~ label {
+  cursor: default !important;
+  opacity: 0.5;
+  pointer-events: none;
+}
+
+.ui.radio.checkbox {
+  min-height: var(--checkbox-size);
+}
+
+/* "switch" styled checkbox */
+
+.ui.toggle.checkbox {
+  min-height: 1.5rem;
+}
+.ui.toggle.checkbox input {
+  width: 3.5rem;
+  height: 1.5rem;
+  opacity: 0;
+  z-index: 3;
+}
+.ui.toggle.checkbox label {
+  min-height: 1.5rem;
+  padding-left: 4.5rem;
+  padding-top: 0.15em;
+}
+.ui.toggle.checkbox label::before {
+  display: block;
+  position: absolute;
+  content: "";
+  z-index: 1;
+  top: 0;
+  width: 3.5rem;
+  height: 1.5rem;
+  border-radius: 500rem;
+  left: 0;
+}
+.ui.toggle.checkbox label::after {
+  background: var(--color-white);
+  position: absolute;
+  content: "";
+  opacity: 1;
+  z-index: 2;
+  width: 1.5rem;
+  height: 1.5rem;
+  top: 0;
+  left: 0;
+  border-radius: 500rem;
+  transition: background 0.3s ease, left 0.3s ease;
+}
+.ui.toggle.checkbox input ~ label::after {
+  left: -0.05rem;
+}
+.ui.toggle.checkbox input:checked ~ label::after {
+  left: 2.15rem;
+}
+.ui.toggle.checkbox input:focus ~ label::before,
+.ui.toggle.checkbox label::before {
+  background: var(--color-input-toggle-background);
+}
+.ui.toggle.checkbox label,
+.ui.toggle.checkbox input:checked ~ label,
+.ui.toggle.checkbox input:focus:checked ~ label {
+  color: var(--color-text) !important;
+}
+.ui.toggle.checkbox input:checked ~ label::before,
+.ui.toggle.checkbox input:focus:checked ~ label::before {
+  background: var(--color-primary) !important;
+}
diff --git a/web_src/css/org.css b/web_src/css/org.css
index 8b3684d0c0..32e8a914fa 100644
--- a/web_src/css/org.css
+++ b/web_src/css/org.css
@@ -89,10 +89,6 @@
   text-align: center;
 }
 
-.organization.options input {
-  min-width: 300px;
-}
-
 .page-content.organization .org-avatar {
   margin-right: 15px;
 }
diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css
index 1421577af2..fe8231d718 100644
--- a/web_src/css/repo/issue-list.css
+++ b/web_src/css/repo/issue-list.css
@@ -9,6 +9,7 @@
 
 .issue-list-toolbar-left {
   display: flex;
+  align-items: center;
 }
 
 .issue-list-toolbar-right .filter.menu {
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 21c41a6161..525a3af8c6 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -2323,715 +2323,6 @@
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Checkbox
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-           Checkbox
-*******************************/
-
-/*--------------
-    Content
----------------*/
-
-.ui.checkbox {
-  position: relative;
-  display: inline-block;
-  backface-visibility: hidden;
-  outline: none;
-  vertical-align: baseline;
-  font-style: normal;
-  min-height: 17px;
-  font-size: 1em;
-  line-height: 17px;
-  min-width: 17px;
-}
-
-/* HTML Checkbox */
-
-.ui.checkbox input[type="checkbox"],
-.ui.checkbox input[type="radio"] {
-  cursor: pointer;
-  position: absolute;
-  top: 0;
-  left: 0;
-  opacity: 0 !important;
-  outline: none;
-  z-index: 3;
-  width: 17px;
-  height: 17px;
-}
-
-.ui.checkbox label {
-  cursor: auto;
-  position: relative;
-  display: block;
-  padding-left: 1.85714em;
-  outline: none;
-  font-size: 1em;
-}
-
-.ui.checkbox label:before {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 17px;
-  height: 17px;
-  content: '';
-  background: #FFFFFF;
-  border-radius: 0.21428571rem;
-  transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease;
-  border: 1px solid #D4D4D5;
-}
-
-/*--------------
-    Checkmark
----------------*/
-
-.ui.checkbox label:after {
-  position: absolute;
-  font-size: 14px;
-  top: 0;
-  left: 0;
-  width: 17px;
-  height: 17px;
-  text-align: center;
-  opacity: 0;
-  color: rgba(0, 0, 0, 0.87);
-  transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease;
-}
-
-/*--------------
-      Label
----------------*/
-
-/* Inside */
-
-.ui.checkbox label,
-.ui.checkbox + label {
-  color: rgba(0, 0, 0, 0.87);
-  transition: color 0.1s ease;
-}
-
-/* Outside */
-
-.ui.checkbox + label {
-  vertical-align: middle;
-}
-
-/*******************************
-           States
-*******************************/
-
-/*--------------
-      Hover
----------------*/
-
-.ui.checkbox label:hover::before {
-  background: #FFFFFF;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox label:hover,
-.ui.checkbox + label:hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-/*--------------
-      Down
----------------*/
-
-.ui.checkbox label:active::before {
-  background: #F9FAFB;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox label:active::after {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.checkbox input:active ~ label {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-     Focus
----------------*/
-
-.ui.checkbox input:focus ~ label:before {
-  background: #FFFFFF;
-  border-color: #96C8DA;
-}
-
-.ui.checkbox input:focus ~ label:after {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.checkbox input:focus ~ label {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-     Active
----------------*/
-
-.ui.checkbox input:checked ~ label:before {
-  background: #FFFFFF;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox input:checked ~ label:after {
-  opacity: 1;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-    Indeterminate
-  ---------------*/
-
-.ui.checkbox input:not([type=radio]):indeterminate ~ label:before {
-  background: #FFFFFF;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox input:not([type=radio]):indeterminate ~ label:after {
-  opacity: 1;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.indeterminate.toggle.checkbox input:not([type=radio]):indeterminate ~ label:before {
-  background: rgba(0, 0, 0, 0.15);
-}
-
-.ui.indeterminate.toggle.checkbox input:not([type=radio]) ~ label:after {
-  left: 1.075rem;
-}
-
-/*--------------
-  Active Focus
----------------*/
-
-.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:before,
-.ui.checkbox input:checked:focus ~ label:before {
-  background: #FFFFFF;
-  border-color: #96C8DA;
-}
-
-.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:after,
-.ui.checkbox input:checked:focus ~ label:after {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-    Read-Only
----------------*/
-
-.ui.read-only.checkbox,
-.ui.read-only.checkbox label {
-  cursor: default;
-}
-
-/*--------------
-       Disabled
-  ---------------*/
-
-.ui.disabled.checkbox label,
-.ui.checkbox input[disabled] ~ label {
-  cursor: default !important;
-  opacity: 0.5;
-  color: #000000;
-  pointer-events: none;
-}
-
-/*--------------
-     Hidden
----------------*/
-
-/* Initialized checkbox moves input below element
- to prevent manually triggering */
-
-.ui.checkbox input.hidden {
-  z-index: -1;
-}
-
-/* Selectable Label */
-
-.ui.checkbox input.hidden + label {
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*--------------
-       Radio
-  ---------------*/
-
-.ui.radio.checkbox {
-  min-height: 15px;
-}
-
-.ui.radio.checkbox label {
-  padding-left: 1.85714em;
-}
-
-/* Box */
-
-.ui.radio.checkbox label:before {
-  content: '';
-  transform: none;
-  width: 15px;
-  height: 15px;
-  border-radius: 500rem;
-  top: 1px;
-  left: 0;
-}
-
-/* Bullet */
-
-.ui.radio.checkbox label:after {
-  border: none;
-  content: '' !important;
-  line-height: 15px;
-  top: 1px;
-  left: 0;
-  width: 15px;
-  height: 15px;
-  border-radius: 500rem;
-  transform: scale(0.46666667);
-  background-color: rgba(0, 0, 0, 0.87);
-}
-
-/* Focus */
-
-.ui.radio.checkbox input:focus ~ label:before {
-  background-color: #FFFFFF;
-}
-
-.ui.radio.checkbox input:focus ~ label:after {
-  background-color: rgba(0, 0, 0, 0.95);
-}
-
-/* Indeterminate */
-
-.ui.radio.checkbox input:indeterminate ~ label:after {
-  opacity: 0;
-}
-
-/* Active */
-
-.ui.radio.checkbox input:checked ~ label:before {
-  background-color: #FFFFFF;
-}
-
-.ui.radio.checkbox input:checked ~ label:after {
-  background-color: rgba(0, 0, 0, 0.95);
-}
-
-/* Active Focus */
-
-.ui.radio.checkbox input:focus:checked ~ label:before {
-  background-color: #FFFFFF;
-}
-
-.ui.radio.checkbox input:focus:checked ~ label:after {
-  background-color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-       Slider
-  ---------------*/
-
-.ui.slider.checkbox {
-  min-height: 1.25rem;
-}
-
-/* Input */
-
-.ui.slider.checkbox input {
-  width: 3.5rem;
-  height: 1.25rem;
-}
-
-/* Label */
-
-.ui.slider.checkbox label {
-  padding-left: 4.5rem;
-  line-height: 1rem;
-  color: rgba(0, 0, 0, 0.4);
-}
-
-/* Line */
-
-.ui.slider.checkbox label:before {
-  display: block;
-  position: absolute;
-  content: '';
-  transform: none;
-  border: none !important;
-  left: 0;
-  z-index: 1;
-  top: 0.4rem;
-  background-color: rgba(0, 0, 0, 0.05);
-  width: 3.5rem;
-  height: 0.21428571rem;
-  border-radius: 500rem;
-  transition: background 0.3s ease;
-}
-
-/* Handle */
-
-.ui.slider.checkbox label:after {
-  background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05));
-  position: absolute;
-  content: '' !important;
-  opacity: 1;
-  z-index: 2;
-  border: none;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-  width: 1.5rem;
-  height: 1.5rem;
-  top: -0.25rem;
-  left: 0;
-  transform: none;
-  border-radius: 500rem;
-  transition: left 0.3s ease;
-}
-
-/* Focus */
-
-.ui.slider.checkbox input:focus ~ label:before {
-  background-color: rgba(0, 0, 0, 0.15);
-  border: none;
-}
-
-/* Hover */
-
-.ui.slider.checkbox label:hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.slider.checkbox label:hover::before {
-  background: rgba(0, 0, 0, 0.15);
-}
-
-/* Active */
-
-.ui.slider.checkbox input:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.slider.checkbox input:checked ~ label:before {
-  background-color: #545454 !important;
-}
-
-.ui.slider.checkbox input:checked ~ label:after {
-  left: 2rem;
-}
-
-/* Active Focus */
-
-.ui.slider.checkbox input:focus:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.slider.checkbox input:focus:checked ~ label:before {
-  background-color: #000000 !important;
-}
-
-/*--------------
-       Toggle
-  ---------------*/
-
-.ui.toggle.checkbox {
-  min-height: 1.5rem;
-}
-
-/* Input */
-
-.ui.toggle.checkbox input {
-  width: 3.5rem;
-  height: 1.5rem;
-}
-
-/* Label */
-
-.ui.toggle.checkbox label {
-  min-height: 1.5rem;
-  padding-left: 4.5rem;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.toggle.checkbox label {
-  padding-top: 0.15em;
-}
-
-/* Switch */
-
-.ui.toggle.checkbox label:before {
-  display: block;
-  position: absolute;
-  content: '';
-  z-index: 1;
-  transform: none;
-  border: none;
-  top: 0;
-  background: rgba(0, 0, 0, 0.05);
-  box-shadow: none;
-  width: 3.5rem;
-  height: 1.5rem;
-  border-radius: 500rem;
-}
-
-/* Handle */
-
-.ui.toggle.checkbox label:after {
-  background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05));
-  position: absolute;
-  content: '' !important;
-  opacity: 1;
-  z-index: 2;
-  border: none;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-  width: 1.5rem;
-  height: 1.5rem;
-  top: 0;
-  left: 0;
-  border-radius: 500rem;
-  transition: background 0.3s ease, left 0.3s ease;
-}
-
-.ui.toggle.checkbox input ~ label:after {
-  left: -0.05rem;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-}
-
-/* Focus */
-
-.ui.toggle.checkbox input:focus ~ label:before {
-  background-color: rgba(0, 0, 0, 0.15);
-  border: none;
-}
-
-/* Hover */
-
-.ui.toggle.checkbox label:hover::before {
-  background-color: rgba(0, 0, 0, 0.15);
-  border: none;
-}
-
-/* Active */
-
-.ui.toggle.checkbox input:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.toggle.checkbox input:checked ~ label:before {
-  background-color: #2185D0 !important;
-}
-
-.ui.toggle.checkbox input:checked ~ label:after {
-  left: 2.15rem;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-}
-
-/* Active Focus */
-
-.ui.toggle.checkbox input:focus:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.toggle.checkbox input:focus:checked ~ label:before {
-  background-color: #0d71bb !important;
-}
-
-/*******************************
-            Variations
-*******************************/
-
-/*--------------
-       Fitted
-  ---------------*/
-
-.ui.fitted.checkbox label {
-  padding-left: 0 !important;
-}
-
-.ui.fitted.toggle.checkbox {
-  width: 3.5rem;
-}
-
-.ui.fitted.slider.checkbox {
-  width: 3.5rem;
-}
-
-/*--------------------
-        Size
----------------------*/
-
-.ui.mini.checkbox {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.checkbox {
-  font-size: 0.85714286em;
-}
-
-.ui.small.checkbox {
-  font-size: 0.92857143em;
-}
-
-.ui.large.checkbox {
-  font-size: 1.14285714em;
-}
-
-.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.14285714);
-  transform-origin: left;
-}
-
-.ui.large.form .checkbox.radio label:before,
-.ui.large.checkbox.radio label:before {
-  transform: scale(1.14285714);
-  transform-origin: left;
-}
-
-.ui.large.form .checkbox.radio label:after,
-.ui.large.checkbox.radio label:after {
-  transform: scale(0.57142857);
-  transform-origin: left;
-  left: 0.33571429em;
-}
-
-.ui.big.checkbox {
-  font-size: 1.28571429em;
-}
-
-.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.28571429);
-  transform-origin: left;
-}
-
-.ui.big.form .checkbox.radio label:before,
-.ui.big.checkbox.radio label:before {
-  transform: scale(1.28571429);
-  transform-origin: left;
-}
-
-.ui.big.form .checkbox.radio label:after,
-.ui.big.checkbox.radio label:after {
-  transform: scale(0.64285714);
-  transform-origin: left;
-  left: 0.37142857em;
-}
-
-.ui.huge.checkbox {
-  font-size: 1.42857143em;
-}
-
-.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.42857143);
-  transform-origin: left;
-}
-
-.ui.huge.form .checkbox.radio label:before,
-.ui.huge.checkbox.radio label:before {
-  transform: scale(1.42857143);
-  transform-origin: left;
-}
-
-.ui.huge.form .checkbox.radio label:after,
-.ui.huge.checkbox.radio label:after {
-  transform: scale(0.71428571);
-  transform-origin: left;
-  left: 0.40714286em;
-}
-
-.ui.massive.checkbox {
-  font-size: 1.71428571em;
-}
-
-.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.71428571);
-  transform-origin: left;
-}
-
-.ui.massive.form .checkbox.radio label:before,
-.ui.massive.checkbox.radio label:before {
-  transform: scale(1.71428571);
-  transform-origin: left;
-}
-
-.ui.massive.form .checkbox.radio label:after,
-.ui.massive.checkbox.radio label:after {
-  transform: scale(0.85714286);
-  transform-origin: left;
-  left: 0.47857143em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-@font-face {
-  font-family: 'Checkbox';
-  src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype');
-}
-
-/* Checkmark */
-
-.ui.checkbox label:after,
-.ui.checkbox .box:after {
-  font-family: 'Checkbox';
-}
-
-/* Checked */
-
-.ui.checkbox input:checked ~ .box:after,
-.ui.checkbox input:checked ~ label:after {
-  content: '\e800';
-}
-
-/* Indeterminate */
-
-.ui.checkbox input:indeterminate ~ .box:after,
-.ui.checkbox input:indeterminate ~ label:after {
-  font-size: 12px;
-  content: '\e801';
-}
-
-/*  UTF Reference
-.check:before { content: '\e800'; }
-.dash:before  { content: '\e801'; }
-.plus:before { content: '\e802'; }
-*/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js
index 1199e9c82f..c150c8d9db 100644
--- a/web_src/fomantic/build/semantic.js
+++ b/web_src/fomantic/build/semantic.js
@@ -1184,883 +1184,6 @@ $.api.settings = {
 
 
 
-})( jQuery, window, document );
-
-/*!
- * # Fomantic-UI - Checkbox
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-'use strict';
-
-$.isFunction = $.isFunction || function(obj) {
-  return typeof obj === "function" && typeof obj.nodeType !== "number";
-};
-
-window = (typeof window != 'undefined' && window.Math == Math)
-  ? window
-  : (typeof self != 'undefined' && self.Math == Math)
-    ? self
-    : Function('return this')()
-;
-
-$.fn.checkbox = function(parameters) {
-  var
-    $allModules    = $(this),
-    moduleSelector = $allModules.selector || '',
-
-    time           = new Date().getTime(),
-    performance    = [],
-
-    query          = arguments[0],
-    methodInvoked  = (typeof query == 'string'),
-    queryArguments = [].slice.call(arguments, 1),
-    returnedValue
-  ;
-
-  $allModules
-    .each(function() {
-      var
-        settings        = $.extend(true, {}, $.fn.checkbox.settings, parameters),
-
-        className       = settings.className,
-        namespace       = settings.namespace,
-        selector        = settings.selector,
-        error           = settings.error,
-
-        eventNamespace  = '.' + namespace,
-        moduleNamespace = 'module-' + namespace,
-
-        $module         = $(this),
-        $label          = $(this).children(selector.label),
-        $input          = $(this).children(selector.input),
-        input           = $input[0],
-
-        initialLoad     = false,
-        shortcutPressed = false,
-        instance        = $module.data(moduleNamespace),
-
-        observer,
-        element         = this,
-        module
-      ;
-
-      module      = {
-
-        initialize: function() {
-          module.verbose('Initializing checkbox', settings);
-
-          module.create.label();
-          module.bind.events();
-
-          module.set.tabbable();
-          module.hide.input();
-
-          module.observeChanges();
-          module.instantiate();
-          module.setup();
-        },
-
-        instantiate: function() {
-          module.verbose('Storing instance of module', module);
-          instance = module;
-          $module
-            .data(moduleNamespace, module)
-          ;
-        },
-
-        destroy: function() {
-          module.verbose('Destroying module');
-          module.unbind.events();
-          module.show.input();
-          $module.removeData(moduleNamespace);
-        },
-
-        fix: {
-          reference: function() {
-            if( $module.is(selector.input) ) {
-              module.debug('Behavior called on <input> adjusting invoked element');
-              $module = $module.closest(selector.checkbox);
-              module.refresh();
-            }
-          }
-        },
-
-        setup: function() {
-          module.set.initialLoad();
-          if( module.is.indeterminate() ) {
-            module.debug('Initial value is indeterminate');
-            module.indeterminate();
-          }
-          else if( module.is.checked() ) {
-            module.debug('Initial value is checked');
-            module.check();
-          }
-          else {
-            module.debug('Initial value is unchecked');
-            module.uncheck();
-          }
-          module.remove.initialLoad();
-        },
-
-        refresh: function() {
-          $label = $module.children(selector.label);
-          $input = $module.children(selector.input);
-          input  = $input[0];
-        },
-
-        hide: {
-          input: function() {
-            module.verbose('Modifying <input> z-index to be unselectable');
-            $input.addClass(className.hidden);
-          }
-        },
-        show: {
-          input: function() {
-            module.verbose('Modifying <input> z-index to be selectable');
-            $input.removeClass(className.hidden);
-          }
-        },
-
-        observeChanges: function() {
-          if('MutationObserver' in window) {
-            observer = new MutationObserver(function(mutations) {
-              module.debug('DOM tree modified, updating selector cache');
-              module.refresh();
-            });
-            observer.observe(element, {
-              childList : true,
-              subtree   : true
-            });
-            module.debug('Setting up mutation observer', observer);
-          }
-        },
-
-        attachEvents: function(selector, event) {
-          var
-            $element = $(selector)
-          ;
-          event = $.isFunction(module[event])
-            ? module[event]
-            : module.toggle
-          ;
-          if($element.length > 0) {
-            module.debug('Attaching checkbox events to element', selector, event);
-            $element
-              .on('click' + eventNamespace, event)
-            ;
-          }
-          else {
-            module.error(error.notFound);
-          }
-        },
-
-        preventDefaultOnInputTarget: function() {
-          if(typeof event !== 'undefined' && event !== null && $(event.target).is(selector.input)) {
-            module.verbose('Preventing default check action after manual check action');
-            event.preventDefault();
-          }
-        },
-
-        event: {
-          change: function(event) {
-            if( !module.should.ignoreCallbacks() ) {
-              settings.onChange.call(input);
-            }
-          },
-          click: function(event) {
-            var
-              $target = $(event.target)
-            ;
-            if( $target.is(selector.input) ) {
-              module.verbose('Using default check action on initialized checkbox');
-              return;
-            }
-            if( $target.is(selector.link) ) {
-              module.debug('Clicking link inside checkbox, skipping toggle');
-              return;
-            }
-            module.toggle();
-            $input.focus();
-            event.preventDefault();
-          },
-          keydown: function(event) {
-            var
-              key     = event.which,
-              keyCode = {
-                enter  : 13,
-                space  : 32,
-                escape : 27,
-                left   : 37,
-                up     : 38,
-                right  : 39,
-                down   : 40
-              }
-            ;
-
-            var r = module.get.radios(),
-                rIndex = r.index($module),
-                rLen = r.length,
-                checkIndex = false;
-
-            if(key == keyCode.left || key == keyCode.up) {
-              checkIndex = (rIndex === 0 ? rLen : rIndex) - 1;
-            } else if(key == keyCode.right || key == keyCode.down) {
-              checkIndex = rIndex === rLen-1 ? 0 : rIndex+1;
-            }
-
-            if (!module.should.ignoreCallbacks() && checkIndex !== false) {
-              if(settings.beforeUnchecked.apply(input)===false) {
-                module.verbose('Option not allowed to be unchecked, cancelling key navigation');
-                return false;
-              }
-              if (settings.beforeChecked.apply($(r[checkIndex]).children(selector.input)[0])===false) {
-                module.verbose('Next option should not allow check, cancelling key navigation');
-                return false;
-              }
-            }
-
-            if(key == keyCode.escape) {
-              module.verbose('Escape key pressed blurring field');
-              $input.blur();
-              shortcutPressed = true;
-            }
-            else if(!event.ctrlKey && ( key == keyCode.space || (key == keyCode.enter && settings.enableEnterKey)) ) {
-              module.verbose('Enter/space key pressed, toggling checkbox');
-              module.toggle();
-              shortcutPressed = true;
-            }
-            else {
-              shortcutPressed = false;
-            }
-          },
-          keyup: function(event) {
-            if(shortcutPressed) {
-              event.preventDefault();
-            }
-          }
-        },
-
-        check: function() {
-          if( !module.should.allowCheck() ) {
-            return;
-          }
-          module.debug('Checking checkbox', $input);
-          module.set.checked();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onChecked.call(input);
-            module.trigger.change();
-          }
-          module.preventDefaultOnInputTarget();
-        },
-
-        uncheck: function() {
-          if( !module.should.allowUncheck() ) {
-            return;
-          }
-          module.debug('Unchecking checkbox');
-          module.set.unchecked();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onUnchecked.call(input);
-            module.trigger.change();
-          }
-          module.preventDefaultOnInputTarget();
-        },
-
-        indeterminate: function() {
-          if( module.should.allowIndeterminate() ) {
-            module.debug('Checkbox is already indeterminate');
-            return;
-          }
-          module.debug('Making checkbox indeterminate');
-          module.set.indeterminate();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onIndeterminate.call(input);
-            module.trigger.change();
-          }
-        },
-
-        determinate: function() {
-          if( module.should.allowDeterminate() ) {
-            module.debug('Checkbox is already determinate');
-            return;
-          }
-          module.debug('Making checkbox determinate');
-          module.set.determinate();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onDeterminate.call(input);
-            module.trigger.change();
-          }
-        },
-
-        enable: function() {
-          if( module.is.enabled() ) {
-            module.debug('Checkbox is already enabled');
-            return;
-          }
-          module.debug('Enabling checkbox');
-          module.set.enabled();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onEnable.call(input);
-            // preserve legacy callbacks
-            settings.onEnabled.call(input);
-            module.trigger.change();
-          }
-        },
-
-        disable: function() {
-          if( module.is.disabled() ) {
-            module.debug('Checkbox is already disabled');
-            return;
-          }
-          module.debug('Disabling checkbox');
-          module.set.disabled();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onDisable.call(input);
-            // preserve legacy callbacks
-            settings.onDisabled.call(input);
-            module.trigger.change();
-          }
-        },
-
-        get: {
-          radios: function() {
-            var
-              name = module.get.name()
-            ;
-            return $('input[name="' + name + '"]').closest(selector.checkbox);
-          },
-          otherRadios: function() {
-            return module.get.radios().not($module);
-          },
-          name: function() {
-            return $input.attr('name');
-          }
-        },
-
-        is: {
-          initialLoad: function() {
-            return initialLoad;
-          },
-          radio: function() {
-            return ($input.hasClass(className.radio) || $input.attr('type') == 'radio');
-          },
-          indeterminate: function() {
-            return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate');
-          },
-          checked: function() {
-            return $input.prop('checked') !== undefined && $input.prop('checked');
-          },
-          disabled: function() {
-            return $input.prop('disabled') !== undefined && $input.prop('disabled');
-          },
-          enabled: function() {
-            return !module.is.disabled();
-          },
-          determinate: function() {
-            return !module.is.indeterminate();
-          },
-          unchecked: function() {
-            return !module.is.checked();
-          }
-        },
-
-        should: {
-          allowCheck: function() {
-            if(module.is.determinate() && module.is.checked() && !module.is.initialLoad() ) {
-              module.debug('Should not allow check, checkbox is already checked');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeChecked.apply(input) === false) {
-              module.debug('Should not allow check, beforeChecked cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowUncheck: function() {
-            if(module.is.determinate() && module.is.unchecked() && !module.is.initialLoad() ) {
-              module.debug('Should not allow uncheck, checkbox is already unchecked');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeUnchecked.apply(input) === false) {
-              module.debug('Should not allow uncheck, beforeUnchecked cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowIndeterminate: function() {
-            if(module.is.indeterminate() && !module.is.initialLoad() ) {
-              module.debug('Should not allow indeterminate, checkbox is already indeterminate');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeIndeterminate.apply(input) === false) {
-              module.debug('Should not allow indeterminate, beforeIndeterminate cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowDeterminate: function() {
-            if(module.is.determinate() && !module.is.initialLoad() ) {
-              module.debug('Should not allow determinate, checkbox is already determinate');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeDeterminate.apply(input) === false) {
-              module.debug('Should not allow determinate, beforeDeterminate cancelled');
-              return false;
-            }
-            return true;
-          },
-          ignoreCallbacks: function() {
-            return (initialLoad && !settings.fireOnInit);
-          }
-        },
-
-        can: {
-          change: function() {
-            return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') );
-          },
-          uncheck: function() {
-            return (typeof settings.uncheckable === 'boolean')
-              ? settings.uncheckable
-              : !module.is.radio()
-            ;
-          }
-        },
-
-        set: {
-          initialLoad: function() {
-            initialLoad = true;
-          },
-          checked: function() {
-            module.verbose('Setting class to checked');
-            $module
-              .removeClass(className.indeterminate)
-              .addClass(className.checked)
-            ;
-            if( module.is.radio() ) {
-              module.uncheckOthers();
-            }
-            if(!module.is.indeterminate() && module.is.checked()) {
-              module.debug('Input is already checked, skipping input property change');
-              return;
-            }
-            module.verbose('Setting state to checked', input);
-            $input
-              .prop('indeterminate', false)
-              .prop('checked', true)
-            ;
-          },
-          unchecked: function() {
-            module.verbose('Removing checked class');
-            $module
-              .removeClass(className.indeterminate)
-              .removeClass(className.checked)
-            ;
-            if(!module.is.indeterminate() &&  module.is.unchecked() ) {
-              module.debug('Input is already unchecked');
-              return;
-            }
-            module.debug('Setting state to unchecked');
-            $input
-              .prop('indeterminate', false)
-              .prop('checked', false)
-            ;
-          },
-          indeterminate: function() {
-            module.verbose('Setting class to indeterminate');
-            $module
-              .addClass(className.indeterminate)
-            ;
-            if( module.is.indeterminate() ) {
-              module.debug('Input is already indeterminate, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to indeterminate');
-            $input
-              .prop('indeterminate', true)
-            ;
-          },
-          determinate: function() {
-            module.verbose('Removing indeterminate class');
-            $module
-              .removeClass(className.indeterminate)
-            ;
-            if( module.is.determinate() ) {
-              module.debug('Input is already determinate, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to determinate');
-            $input
-              .prop('indeterminate', false)
-            ;
-          },
-          disabled: function() {
-            module.verbose('Setting class to disabled');
-            $module
-              .addClass(className.disabled)
-            ;
-            if( module.is.disabled() ) {
-              module.debug('Input is already disabled, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to disabled');
-            $input
-              .prop('disabled', 'disabled')
-            ;
-          },
-          enabled: function() {
-            module.verbose('Removing disabled class');
-            $module.removeClass(className.disabled);
-            if( module.is.enabled() ) {
-              module.debug('Input is already enabled, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to enabled');
-            $input
-              .prop('disabled', false)
-            ;
-          },
-          tabbable: function() {
-            module.verbose('Adding tabindex to checkbox');
-            if( $input.attr('tabindex') === undefined) {
-              $input.attr('tabindex', 0);
-            }
-          }
-        },
-
-        remove: {
-          initialLoad: function() {
-            initialLoad = false;
-          }
-        },
-
-        trigger: {
-          change: function() {
-            var
-              inputElement = $input[0]
-            ;
-            if(inputElement) {
-              var events = document.createEvent('HTMLEvents');
-              module.verbose('Triggering native change event');
-              events.initEvent('change', true, false);
-              inputElement.dispatchEvent(events);
-            }
-          }
-        },
-
-
-        create: {
-          label: function() {
-            if($input.prevAll(selector.label).length > 0) {
-              $input.prev(selector.label).detach().insertAfter($input);
-              module.debug('Moving existing label', $label);
-            }
-            else if( !module.has.label() ) {
-              $label = $('<label>').insertAfter($input);
-              module.debug('Creating label', $label);
-            }
-          }
-        },
-
-        has: {
-          label: function() {
-            return ($label.length > 0);
-          }
-        },
-
-        bind: {
-          events: function() {
-            module.verbose('Attaching checkbox events');
-            $module
-              .on('click'   + eventNamespace, module.event.click)
-              .on('change'  + eventNamespace, module.event.change)
-              .on('keydown' + eventNamespace, selector.input, module.event.keydown)
-              .on('keyup'   + eventNamespace, selector.input, module.event.keyup)
-            ;
-          }
-        },
-
-        unbind: {
-          events: function() {
-            module.debug('Removing events');
-            $module
-              .off(eventNamespace)
-            ;
-          }
-        },
-
-        uncheckOthers: function() {
-          var
-            $radios = module.get.otherRadios()
-          ;
-          module.debug('Unchecking other radios', $radios);
-          $radios.removeClass(className.checked);
-        },
-
-        toggle: function() {
-          if( !module.can.change() ) {
-            if(!module.is.radio()) {
-              module.debug('Checkbox is read-only or disabled, ignoring toggle');
-            }
-            return;
-          }
-          if( module.is.indeterminate() || module.is.unchecked() ) {
-            module.debug('Currently unchecked');
-            module.check();
-          }
-          else if( module.is.checked() && module.can.uncheck() ) {
-            module.debug('Currently checked');
-            module.uncheck();
-          }
-        },
-        setting: function(name, value) {
-          module.debug('Changing setting', name, value);
-          if( $.isPlainObject(name) ) {
-            $.extend(true, settings, name);
-          }
-          else if(value !== undefined) {
-            if($.isPlainObject(settings[name])) {
-              $.extend(true, settings[name], value);
-            }
-            else {
-              settings[name] = value;
-            }
-          }
-          else {
-            return settings[name];
-          }
-        },
-        internal: function(name, value) {
-          if( $.isPlainObject(name) ) {
-            $.extend(true, module, name);
-          }
-          else if(value !== undefined) {
-            module[name] = value;
-          }
-          else {
-            return module[name];
-          }
-        },
-        debug: function() {
-          if(!settings.silent && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
-            }
-            else {
-              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.debug.apply(console, arguments);
-            }
-          }
-        },
-        verbose: function() {
-          if(!settings.silent && settings.verbose && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
-            }
-            else {
-              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.verbose.apply(console, arguments);
-            }
-          }
-        },
-        error: function() {
-          if(!settings.silent) {
-            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
-            module.error.apply(console, arguments);
-          }
-        },
-        performance: {
-          log: function(message) {
-            var
-              currentTime,
-              executionTime,
-              previousTime
-            ;
-            if(settings.performance) {
-              currentTime   = new Date().getTime();
-              previousTime  = time || currentTime;
-              executionTime = currentTime - previousTime;
-              time          = currentTime;
-              performance.push({
-                'Name'           : message[0],
-                'Arguments'      : [].slice.call(message, 1) || '',
-                'Element'        : element,
-                'Execution Time' : executionTime
-              });
-            }
-            clearTimeout(module.performance.timer);
-            module.performance.timer = setTimeout(module.performance.display, 500);
-          },
-          display: function() {
-            var
-              title = settings.name + ':',
-              totalTime = 0
-            ;
-            time = false;
-            clearTimeout(module.performance.timer);
-            $.each(performance, function(index, data) {
-              totalTime += data['Execution Time'];
-            });
-            title += ' ' + totalTime + 'ms';
-            if(moduleSelector) {
-              title += ' \'' + moduleSelector + '\'';
-            }
-            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
-              console.groupCollapsed(title);
-              if(console.table) {
-                console.table(performance);
-              }
-              else {
-                $.each(performance, function(index, data) {
-                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
-                });
-              }
-              console.groupEnd();
-            }
-            performance = [];
-          }
-        },
-        invoke: function(query, passedArguments, context) {
-          var
-            object = instance,
-            maxDepth,
-            found,
-            response
-          ;
-          passedArguments = passedArguments || queryArguments;
-          context         = element         || context;
-          if(typeof query == 'string' && object !== undefined) {
-            query    = query.split(/[\. ]/);
-            maxDepth = query.length - 1;
-            $.each(query, function(depth, value) {
-              var camelCaseValue = (depth != maxDepth)
-                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
-                : query
-              ;
-              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
-                object = object[camelCaseValue];
-              }
-              else if( object[camelCaseValue] !== undefined ) {
-                found = object[camelCaseValue];
-                return false;
-              }
-              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
-                object = object[value];
-              }
-              else if( object[value] !== undefined ) {
-                found = object[value];
-                return false;
-              }
-              else {
-                module.error(error.method, query);
-                return false;
-              }
-            });
-          }
-          if ( $.isFunction( found ) ) {
-            response = found.apply(context, passedArguments);
-          }
-          else if(found !== undefined) {
-            response = found;
-          }
-          if(Array.isArray(returnedValue)) {
-            returnedValue.push(response);
-          }
-          else if(returnedValue !== undefined) {
-            returnedValue = [returnedValue, response];
-          }
-          else if(response !== undefined) {
-            returnedValue = response;
-          }
-          return found;
-        }
-      };
-
-      if(methodInvoked) {
-        if(instance === undefined) {
-          module.initialize();
-        }
-        module.invoke(query);
-      }
-      else {
-        if(instance !== undefined) {
-          instance.invoke('destroy');
-        }
-        module.initialize();
-      }
-    })
-  ;
-
-  return (returnedValue !== undefined)
-    ? returnedValue
-    : this
-  ;
-};
-
-$.fn.checkbox.settings = {
-
-  name                : 'Checkbox',
-  namespace           : 'checkbox',
-
-  silent              : false,
-  debug               : false,
-  verbose             : true,
-  performance         : true,
-
-  // delegated event context
-  uncheckable         : 'auto',
-  fireOnInit          : false,
-  enableEnterKey      : true,
-
-  onChange            : function(){},
-
-  beforeChecked       : function(){},
-  beforeUnchecked     : function(){},
-  beforeDeterminate   : function(){},
-  beforeIndeterminate : function(){},
-
-  onChecked           : function(){},
-  onUnchecked         : function(){},
-
-  onDeterminate       : function() {},
-  onIndeterminate     : function() {},
-
-  onEnable            : function(){},
-  onDisable           : function(){},
-
-  // preserve misspelled callbacks (will be removed in 3.0)
-  onEnabled           : function(){},
-  onDisabled          : function(){},
-
-  className       : {
-    checked       : 'checked',
-    indeterminate : 'indeterminate',
-    disabled      : 'disabled',
-    hidden        : 'hidden',
-    radio         : 'radio',
-    readOnly      : 'read-only'
-  },
-
-  error     : {
-    method       : 'The method you called is not defined'
-  },
-
-  selector : {
-    checkbox : '.ui.checkbox',
-    label    : 'label, .box',
-    input    : 'input[type="checkbox"], input[type="radio"]',
-    link     : 'a[href]'
-  }
-
-};
-
 })( jQuery, window, document );
 
 /*!
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index b916af6922..151273f3ca 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -23,7 +23,6 @@
   "components": [
     "api",
     "button",
-    "checkbox",
     "dimmer",
     "dropdown",
     "form",
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 8a88996742..f388b1122e 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -218,17 +218,24 @@ export function initAdminCommon() {
     });
 
     // Select actions
-    const $checkboxes = $('.select.table .ui.checkbox');
+    const checkboxes = document.querySelectorAll('.select.table .ui.checkbox input');
+
     $('.select.action').on('click', function () {
       switch ($(this).data('action')) {
         case 'select-all':
-          $checkboxes.checkbox('check');
+          for (const checkbox of checkboxes) {
+            checkbox.checked = true;
+          }
           break;
         case 'deselect-all':
-          $checkboxes.checkbox('uncheck');
+          for (const checkbox of checkboxes) {
+            checkbox.checked = false;
+          }
           break;
         case 'inverse':
-          $checkboxes.checkbox('toggle');
+          for (const checkbox of checkboxes) {
+            checkbox.checked = !checkbox.checked;
+          }
           break;
       }
     });
@@ -236,11 +243,11 @@ export function initAdminCommon() {
       e.preventDefault();
       this.classList.add('is-loading', 'disabled');
       const data = new FormData();
-      $checkboxes.each(function () {
-        if ($(this).checkbox('is checked')) {
-          data.append('ids[]', this.getAttribute('data-id'));
+      for (const checkbox of checkboxes) {
+        if (checkbox.checked) {
+          data.append('ids[]', checkbox.closest('.ui.checkbox').getAttribute('data-id'));
         }
-      });
+      }
       await POST(this.getAttribute('data-link'), {data});
       window.location.href = this.getAttribute('data-redirect');
     });
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index ce702f041f..009dbd9421 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -195,8 +195,6 @@ export function initGlobalCommon() {
   $uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward');
   $uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward');
 
-  $('.ui.checkbox').checkbox();
-
   $('.tabular.menu .item').tab();
 
   initSubmitEventPolyfill();
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 47b4d5f71c..bb45c6ae57 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -282,32 +282,26 @@ export function initRepoPullRequestMergeInstruction() {
 }
 
 export function initRepoPullRequestAllowMaintainerEdit() {
-  const checkbox = document.getElementById('allow-edits-from-maintainers');
-  if (!checkbox) return;
+  const wrapper = document.getElementById('allow-edits-from-maintainers');
+  if (!wrapper) return;
 
-  const $checkbox = $(checkbox);
-
-  const promptError = checkbox.getAttribute('data-prompt-error');
-  $checkbox.checkbox({
-    'onChange': async () => {
-      const checked = $checkbox.checkbox('is checked');
-      let url = checkbox.getAttribute('data-url');
-      url += '/set_allow_maintainer_edit';
-      $checkbox.checkbox('set disabled');
-      try {
-        const response = await POST(url, {
-          data: {allow_maintainer_edit: checked},
-        });
-        if (!response.ok) {
-          throw new Error('Failed to update maintainer edit permission');
-        }
-      } catch (error) {
-        console.error(error);
-        showTemporaryTooltip(checkbox, promptError);
-      } finally {
-        $checkbox.checkbox('set enabled');
+  wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => {
+    const checked = e.target.checked;
+    const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
+    wrapper.classList.add('is-loading');
+    e.target.disabled = true;
+    try {
+      const response = await POST(url, {data: {allow_maintainer_edit: checked}});
+      if (!response.ok) {
+        throw new Error('Failed to update maintainer edit permission');
       }
-    },
+    } catch (error) {
+      console.error(error);
+      showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
+    } finally {
+      wrapper.classList.remove('is-loading');
+      e.target.disabled = false;
+    }
   });
 }
 
diff --git a/web_src/js/modules/fomantic.js b/web_src/js/modules/fomantic.js
index 6aafdd5ddc..d205c2b2ee 100644
--- a/web_src/js/modules/fomantic.js
+++ b/web_src/js/modules/fomantic.js
@@ -11,8 +11,6 @@ export const fomanticMobileScreen = window.matchMedia('only screen and (max-widt
 export function initGiteaFomantic() {
   // Silence fomantic's error logging when tabs are used without a target content element
   $.fn.tab.settings.silent = true;
-  // Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element.
-  $.fn.checkbox.settings.enableEnterKey = false;
 
   // By default, use "exact match" for full text search
   $.fn.dropdown.settings.fullTextSearch = 'exact';
diff --git a/web_src/js/modules/fomantic/aria.md b/web_src/js/modules/fomantic/aria.md
index f639233346..5836a34506 100644
--- a/web_src/js/modules/fomantic/aria.md
+++ b/web_src/js/modules/fomantic/aria.md
@@ -41,24 +41,19 @@ The ideal checkboxes should be:
 <label><input type="checkbox"> ... </label>
 ```
 
-However, related CSS styles aren't supported (not implemented) yet, so at the moment,
-almost all the checkboxes are still using Fomantic UI checkbox.
-
-## Fomantic UI Checkbox
+However, the templates still have the Fomantic-style HTML layout:
 
 ```html
 <div class="ui checkbox">
-  <input type="checkbox"> <!-- class "hidden" will be added by $.checkbox() -->
+  <input type="checkbox">
   <label>...</label>
 </div>
 ```
 
-Then the JS `$.checkbox()` should be called to make it work with keyboard and label-clicking,
-then it works like the ideal checkboxes.
-
-There is still a problem: Fomantic UI checkbox is not friendly to screen readers,
-so we add IDs to all the Fomantic UI checkboxes automatically by JS.
-If the `label` part is empty, then the checkbox needs to get the `aria-label` attribute manually.
+We call `initAriaCheckboxPatch` to link the `input` and `label` which makes clicking the
+label etc. work. There is still a problem: These checkboxes are not friendly to screen readers,
+so we add IDs to all the Fomantic UI checkboxes automatically by JS. If the `label` part is empty,
+then the checkbox needs to get the `aria-label` attribute manually.
 
 # Fomantic Dropdown
 
diff --git a/web_src/js/modules/fomantic/checkbox.js b/web_src/js/modules/fomantic/checkbox.js
index 08af1c2eb6..ffe853b28f 100644
--- a/web_src/js/modules/fomantic/checkbox.js
+++ b/web_src/js/modules/fomantic/checkbox.js
@@ -1,38 +1,15 @@
-import $ from 'jquery';
 import {generateAriaId} from './base.js';
 
-const ariaPatchKey = '_giteaAriaPatchCheckbox';
-const fomanticCheckboxFn = $.fn.checkbox;
-
-// use our own `$.fn.checkbox` to patch Fomantic's checkbox module
 export function initAriaCheckboxPatch() {
-  if ($.fn.checkbox === ariaCheckboxFn) throw new Error('initAriaCheckboxPatch could only be called once');
-  $.fn.checkbox = ariaCheckboxFn;
-  ariaCheckboxFn.settings = fomanticCheckboxFn.settings;
-}
-
-// the patched `$.fn.checkbox` checkbox function
-// * it does the one-time attaching on the first call
-function ariaCheckboxFn(...args) {
-  const ret = fomanticCheckboxFn.apply(this, args);
-  for (const el of this) {
-    if (el[ariaPatchKey]) continue;
-    attachInit(el);
+  // link the label and the input element so it's clickable and accessible
+  for (const el of document.querySelectorAll('.ui.checkbox')) {
+    if (el.hasAttribute('data-checkbox-patched')) continue;
+    const label = el.querySelector('label');
+    const input = el.querySelector('input');
+    if (!label || !input || input.getAttribute('id') || label.getAttribute('for')) continue;
+    const id = generateAriaId();
+    input.setAttribute('id', id);
+    label.setAttribute('for', id);
+    el.setAttribute('data-checkbox-patched', 'true');
   }
-  return ret;
-}
-
-function attachInit(el) {
-  // Fomantic UI checkbox needs to be something like: <div class="ui checkbox"><label /><input /></div>
-  // It doesn't work well with <label><input />...</label>
-  // To make it work with aria, the "id"/"for" attributes are necessary, so add them automatically if missing.
-  // In the future, refactor to use native checkbox directly, then this patch could be removed.
-  el[ariaPatchKey] = {}; // record that this element has been patched
-  const label = el.querySelector('label');
-  const input = el.querySelector('input');
-  if (!label || !input || input.getAttribute('id')) return;
-
-  const id = generateAriaId();
-  input.setAttribute('id', id);
-  label.setAttribute('for', id);
 }

From 849eee8db70c8999d54350b85ea7a16fc44dc404 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 29 Mar 2024 15:40:17 +0300
Subject: [PATCH 574/679] Remove jQuery class from the image diff (#30140)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the image diff and it works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/imagediff.js | 29 ++++++++++++++++++++---------
 1 file changed, 20 insertions(+), 9 deletions(-)

diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index 53bf2109ba..192a642834 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -110,15 +110,15 @@ export function initImageDiff() {
     const $imagesAfter = imageInfos[0].$images;
     const $imagesBefore = imageInfos[1].$images;
 
-    initSideBySide(createContext($imagesAfter[0], $imagesBefore[0]));
+    initSideBySide(this, createContext($imagesAfter[0], $imagesBefore[0]));
     if ($imagesAfter.length > 0 && $imagesBefore.length > 0) {
       initSwipe(createContext($imagesAfter[1], $imagesBefore[1]));
       initOverlay(createContext($imagesAfter[2], $imagesBefore[2]));
     }
 
-    $container.find('> .image-diff-tabs').removeClass('is-loading');
+    this.querySelector(':scope > .image-diff-tabs')?.classList.remove('is-loading');
 
-    function initSideBySide(sizes) {
+    function initSideBySide(container, sizes) {
       let factor = 1;
       if (sizes.max.width > (diffContainerWidth - 24) / 2) {
         factor = (diffContainerWidth - 24) / 2 / sizes.max.width;
@@ -126,13 +126,24 @@ export function initImageDiff() {
 
       const widthChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalWidth !== sizes.$image2[0].naturalWidth;
       const heightChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalHeight !== sizes.$image2[0].naturalHeight;
-      if (sizes.$image1.length !== 0) {
-        $container.find('.bounds-info-after .bounds-info-width').text(`${sizes.$image1[0].naturalWidth}px`).addClass(widthChanged ? 'green' : '');
-        $container.find('.bounds-info-after .bounds-info-height').text(`${sizes.$image1[0].naturalHeight}px`).addClass(heightChanged ? 'green' : '');
+      if (sizes.$image1?.length) {
+        const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width');
+        boundsInfoAfterWidth.textContent = `${sizes.$image1[0].naturalWidth}px`;
+        if (widthChanged) boundsInfoAfterWidth.classList.add('green');
+
+        const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height');
+        boundsInfoAfterHeight.textContent = `${sizes.$image1[0].naturalHeight}px`;
+        if (heightChanged) boundsInfoAfterHeight.classList.add('green');
       }
-      if (sizes.$image2.length !== 0) {
-        $container.find('.bounds-info-before .bounds-info-width').text(`${sizes.$image2[0].naturalWidth}px`).addClass(widthChanged ? 'red' : '');
-        $container.find('.bounds-info-before .bounds-info-height').text(`${sizes.$image2[0].naturalHeight}px`).addClass(heightChanged ? 'red' : '');
+
+      if (sizes.$image2?.length) {
+        const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width');
+        boundsInfoBeforeWidth.textContent = `${sizes.$image2[0].naturalWidth}px`;
+        if (widthChanged) boundsInfoBeforeWidth.classList.add('red');
+
+        const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height');
+        boundsInfoBeforeHeight.textContent = `${sizes.$image2[0].naturalHeight}px`;
+        if (heightChanged) boundsInfoBeforeHeight.classList.add('red');
       }
 
       const image1 = sizes.$image1[0];

From 59d4aadba5c15d02f3b9f0e61abb7476870c20a5 Mon Sep 17 00:00:00 2001
From: Jack Hay <jack@allspice.io>
Date: Fri, 29 Mar 2024 11:05:41 -0400
Subject: [PATCH 575/679] Add setting to disable user features when user login
 type is not plain (#29615)

## Changes
- Adds setting `EXTERNAL_USER_DISABLE_FEATURES` to disable any supported
user features when login type is not plain
- In general, this is necessary for SSO implementations to avoid
inconsistencies between the external account management and the linked
account
- Adds helper functions to encourage correct use
---
 custom/conf/app.example.ini                   |  5 +++
 .../config-cheat-sheet.en-us.md               |  4 +++
 models/user/user.go                           | 18 ++++++++++
 models/user/user_test.go                      | 35 +++++++++++++++++++
 modules/setting/admin.go                      | 12 ++++---
 routers/api/v1/user/gpg_key.go                |  5 +--
 routers/api/v1/user/key.go                    |  4 +--
 routers/web/user/setting/account.go           |  4 +--
 routers/web/user/setting/keys.go              | 13 +++----
 9 files changed, 84 insertions(+), 16 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index e2723bd8ae..1584b10301 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1485,6 +1485,11 @@ LEVEL = Info
 ;; - manage_ssh_keys: a user cannot configure ssh keys
 ;; - manage_gpg_keys: a user cannot configure gpg keys
 ;USER_DISABLED_FEATURES =
+;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
+;; - deletion: a user cannot delete their own account
+;; - manage_ssh_keys: a user cannot configure ssh keys
+;; - manage_gpg_keys: a user cannot configure gpg keys
+;;EXTERNAL_USER_DISABLE_FEATURES =
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 03ab517d80..f2209d269a 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -522,6 +522,10 @@ And the following unique queues:
   - `deletion`: User cannot delete their own account.
   - `manage_ssh_keys`: User cannot configure ssh keys.
   - `manage_gpg_keys`: User cannot configure gpg keys.
+- `EXTERNAL_USER_DISABLE_FEATURES`: **_empty_**: Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
+  - `deletion`: User cannot delete their own account.
+  - `manage_ssh_keys`: User cannot configure ssh keys.
+  - `manage_gpg_keys`: User cannot configure gpg keys.
 
 ## Security (`security`)
 
diff --git a/models/user/user.go b/models/user/user.go
index 22a3099643..d459ec239e 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -1232,3 +1232,21 @@ func GetOrderByName() string {
 	}
 	return "name"
 }
+
+// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the
+// user if applicable
+func IsFeatureDisabledWithLoginType(user *User, feature string) bool {
+	// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
+	return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) ||
+		setting.Admin.UserDisabledFeatures.Contains(feature)
+}
+
+// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type
+// of the user if applicable
+func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
+	// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
+	if user != nil && user.LoginType > auth.Plain {
+		return &setting.Admin.ExternalUserDisableFeatures
+	}
+	return &setting.Admin.UserDisabledFeatures
+}
diff --git a/models/user/user_test.go b/models/user/user_test.go
index f4efd071ea..a4550fa655 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password/hash"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
@@ -526,3 +527,37 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
 		}
 	}
 }
+
+func TestDisabledUserFeatures(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	testValues := container.SetOf(setting.UserFeatureDeletion,
+		setting.UserFeatureManageSSHKeys,
+		setting.UserFeatureManageGPGKeys)
+
+	oldSetting := setting.Admin.ExternalUserDisableFeatures
+	defer func() {
+		setting.Admin.ExternalUserDisableFeatures = oldSetting
+	}()
+	setting.Admin.ExternalUserDisableFeatures = testValues
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0)
+
+	// no features should be disabled with a plain login type
+	assert.LessOrEqual(t, user.LoginType, auth.Plain)
+	assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0)
+	for _, f := range testValues.Values() {
+		assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f))
+	}
+
+	// check disabled features with external login type
+	user.LoginType = auth.OAuth2
+
+	// all features should be disabled
+	assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values())
+	for _, f := range testValues.Values() {
+		assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
+	}
+}
diff --git a/modules/setting/admin.go b/modules/setting/admin.go
index be214a58ce..8aebc76154 100644
--- a/modules/setting/admin.go
+++ b/modules/setting/admin.go
@@ -3,13 +3,16 @@
 
 package setting
 
-import "code.gitea.io/gitea/modules/container"
+import (
+	"code.gitea.io/gitea/modules/container"
+)
 
 // Admin settings
 var Admin struct {
-	DisableRegularOrgCreation bool
-	DefaultEmailNotification  string
-	UserDisabledFeatures      container.Set[string]
+	DisableRegularOrgCreation   bool
+	DefaultEmailNotification    string
+	UserDisabledFeatures        container.Set[string]
+	ExternalUserDisableFeatures container.Set[string]
 }
 
 func loadAdminFrom(rootCfg ConfigProvider) {
@@ -17,6 +20,7 @@ func loadAdminFrom(rootCfg ConfigProvider) {
 	Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
 	Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
 	Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
+	Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...)
 }
 
 const (
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index dcf5da0b2e..5a2f995e1b 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -10,6 +10,7 @@ import (
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
@@ -133,7 +134,7 @@ func GetGPGKey(ctx *context.APIContext) {
 
 // CreateUserGPGKey creates new GPG key to given user by ID.
 func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
-	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
 		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
 		return
 	}
@@ -274,7 +275,7 @@ func DeleteGPGKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
 		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
 		return
 	}
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index bcbfd93bd3..d9456e7ec6 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -199,7 +199,7 @@ func GetPublicKey(ctx *context.APIContext) {
 
 // CreateUserPublicKey creates new public key to given user by ID.
 func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) {
-	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
 		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
 		return
 	}
@@ -269,7 +269,7 @@ func DeletePublicKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
 		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
 		return
 	}
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index d69bda6663..c93b70af76 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -235,7 +235,7 @@ func DeleteEmail(ctx *context.Context) {
 
 // DeleteAccount render user suicide page and response for delete user himself
 func DeleteAccount(ctx *context.Context) {
-	if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureDeletion) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureDeletion) {
 		ctx.Error(http.StatusNotFound)
 		return
 	}
@@ -319,7 +319,7 @@ func loadAccountData(ctx *context.Context) {
 	ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
 	ctx.Data["ActivationsPending"] = pendingActivation
 	ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
-	ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures
+	ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
 
 	if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
 		ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 056fcc0ace..9e969e045d 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -10,6 +10,7 @@ import (
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
@@ -78,7 +79,7 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "gpg":
-		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
 			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
 			return
 		}
@@ -159,7 +160,7 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "ssh":
-		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
 			return
 		}
@@ -203,7 +204,7 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "verify_ssh":
-		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
 			return
 		}
@@ -240,7 +241,7 @@ func KeysPost(ctx *context.Context) {
 func DeleteKey(ctx *context.Context) {
 	switch ctx.FormString("type") {
 	case "gpg":
-		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageGPGKeys) {
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
 			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
 			return
 		}
@@ -250,7 +251,7 @@ func DeleteKey(ctx *context.Context) {
 			ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
 		}
 	case "ssh":
-		if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
 			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
 			return
 		}
@@ -333,5 +334,5 @@ func loadKeysData(ctx *context.Context) {
 
 	ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
 	ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
-	ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures
+	ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
 }

From c9068ef9e410deefa1c3877daf6dfc3c5000fb17 Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Fri, 29 Mar 2024 23:39:46 +0800
Subject: [PATCH 576/679] Fix:the rounded corners of the folded file are not
 displayed correctly (#29953)

Fix:    [#29933](https://github.com/go-gitea/gitea/issues/29933)

**Before**


![image](https://github.com/go-gitea/gitea/assets/37935145/71ec80f6-5896-4e4a-b686-4d792c11ebe2)


**After**


![image](https://github.com/go-gitea/gitea/assets/37935145/81348a61-946a-4562-881d-8d873e50228f)

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/css/modules/segment.css | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css
index 8bdd25bfe7..bbd39c385f 100644
--- a/web_src/css/modules/segment.css
+++ b/web_src/css/modules/segment.css
@@ -69,7 +69,8 @@
   border-radius: 0 0 0.28571429rem 0.28571429rem;
 }
 
-.ui.segments:not(.horizontal) > .segment:only-child {
+.ui.segments:not(.horizontal) > .segment:only-child,
+.ui.segments:not(.horizontal) > .segment:has(~ .tw-hidden) { /* workaround issue with :last-child ignoring hidden elements */
   border-radius: 0.28571429rem;
 }
 

From 911993429f3bec0ff4440c012b2a8f295673f961 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 29 Mar 2024 20:08:54 +0300
Subject: [PATCH 577/679] Remove jQuery class from the code range selection
 (#30173)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the code range selection functionality and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-code.js | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index cb5afa8318..63da5f2039 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -25,7 +25,9 @@ function getLineEls() {
 }
 
 function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
-  $linesEls.closest('tr').removeClass('active');
+  for (const el of $linesEls) {
+    el.closest('tr').classList.remove('active');
+  }
 
   // add hashchange to permalink
   const refInNewIssue = document.querySelector('a.ref-in-new-issue');
@@ -72,7 +74,7 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
         classes.push(`[rel=L${i}]`);
       }
       $linesEls.filter(classes.join(',')).each(function () {
-        $(this).closest('tr').addClass('active');
+        this.closest('tr').classList.add('active');
       });
       changeHash(`#L${a}-L${b}`);
 
@@ -82,7 +84,7 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
       return;
     }
   }
-  $selectionEndEl.closest('tr').addClass('active');
+  $selectionEndEl[0].closest('tr').classList.add('active');
   changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`);
 
   updateIssueHref($selectionEndEl[0].getAttribute('rel'));

From 56ac5f18e8022242316d86c8f3091bce554faebb Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 29 Mar 2024 20:17:21 +0300
Subject: [PATCH 578/679] Remove jQuery class from the notification count
 (#30172)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the notification count and it works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/notification.js | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index d9f6e50202..2de640e674 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -1,5 +1,6 @@
 import $ from 'jquery';
 import {GET} from '../modules/fetch.js';
+import {toggleElem} from '../utils/dom.js';
 
 const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
 let notificationSequenceNumber = 0;
@@ -177,14 +178,11 @@ async function updateNotificationCount() {
 
     const data = await response.json();
 
-    const $notificationCount = $('.notification_count');
-    if (data.new === 0) {
-      $notificationCount.addClass('tw-hidden');
-    } else {
-      $notificationCount.removeClass('tw-hidden');
-    }
+    toggleElem('.notification_count', data.new !== 0);
 
-    $notificationCount.text(`${data.new}`);
+    for (const el of document.getElementsByClassName('notification_count')) {
+      el.textContent = `${data.new}`;
+    }
 
     return `${data.new}`;
   } catch (error) {

From c487a32bcd093affe3284282ea279d97f52a867f Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 29 Mar 2024 21:51:44 +0300
Subject: [PATCH 579/679] Remove jQuery class from the diff view (#30176)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the diff view functionality and it works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-diff.js | 55 ++++++++++++++++++--------------
 web_src/js/utils/dom.js          |  2 +-
 2 files changed, 32 insertions(+), 25 deletions(-)

diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 596b1ea380..b2e8ec4866 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -7,7 +7,7 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
 import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
 import {initImageDiff} from './imagediff.js';
 import {showErrorToast} from '../modules/toast.js';
-import {submitEventSubmitter} from '../utils/dom.js';
+import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js';
 import {POST, GET} from '../modules/fetch.js';
 
 const {pageData, i18n} = window.config;
@@ -16,7 +16,6 @@ function initRepoDiffReviewButton() {
   const reviewBox = document.getElementById('review-box');
   if (!reviewBox) return;
 
-  const $reviewBox = $(reviewBox);
   const counter = reviewBox.querySelector('.review-comments-counter');
   if (!counter) return;
 
@@ -27,23 +26,27 @@ function initRepoDiffReviewButton() {
       const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
       counter.setAttribute('data-pending-comment-number', num);
       counter.textContent = num;
-      // Force the browser to reflow the DOM. This is to ensure that the browser replay the animation
-      $reviewBox.removeClass('pulse');
-      $reviewBox.width();
-      $reviewBox.addClass('pulse');
+
+      reviewBox.classList.remove('pulse');
+      requestAnimationFrame(() => {
+        reviewBox.classList.add('pulse');
+      });
     });
   });
 }
 
 function initRepoDiffFileViewToggle() {
   $('.file-view-toggle').on('click', function () {
-    const $this = $(this);
-    $this.parent().children().removeClass('active');
-    $this.addClass('active');
+    for (const el of queryElemSiblings(this)) {
+      el.classList.remove('active');
+    }
+    this.classList.add('active');
 
-    const $target = $($this.data('toggle-selector'));
-    $target.parent().children().addClass('tw-hidden');
-    $target.removeClass('tw-hidden');
+    const target = document.querySelector(this.getAttribute('data-toggle-selector'));
+    if (!target) return;
+
+    hideElem(queryElemSiblings(target));
+    showElem(target);
   });
 }
 
@@ -57,9 +60,9 @@ function initRepoDiffConversationForm() {
       return;
     }
 
-    if ($form.hasClass('is-loading')) return;
+    if (e.target.classList.contains('is-loading')) return;
     try {
-      $form.addClass('is-loading');
+      e.target.classList.add('is-loading');
       const formData = new FormData($form[0]);
 
       // if the form is submitted by a button, append the button's name and value to the form data
@@ -74,10 +77,14 @@ function initRepoDiffConversationForm() {
       const {path, side, idx} = $newConversationHolder.data();
 
       $form.closest('.conversation-holder').replaceWith($newConversationHolder);
+      let selector;
       if ($form.closest('tr').data('line-type') === 'same') {
-        $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).addClass('tw-invisible');
+        selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`;
       } else {
-        $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).addClass('tw-invisible');
+        selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`;
+      }
+      for (const el of document.querySelectorAll(selector)) {
+        el.classList.add('tw-invisible');
       }
       $newConversationHolder.find('.dropdown').dropdown();
       initCompReactionSelector($newConversationHolder);
@@ -85,7 +92,7 @@ function initRepoDiffConversationForm() {
       console.error('Error:', error);
       showErrorToast(i18n.network_error);
     } finally {
-      $form.removeClass('is-loading');
+      e.target.classList.remove('is-loading');
     }
   });
 
@@ -145,13 +152,13 @@ function onShowMoreFiles() {
 }
 
 export async function loadMoreFiles(url) {
-  const $target = $('a#diff-show-more-files');
-  if ($target.hasClass('disabled') || pageData.diffFileInfo.isLoadingNewData) {
+  const target = document.querySelector('a#diff-show-more-files');
+  if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
     return;
   }
 
   pageData.diffFileInfo.isLoadingNewData = true;
-  $target.addClass('disabled');
+  target?.classList.add('disabled');
 
   try {
     const response = await GET(url);
@@ -168,7 +175,7 @@ export async function loadMoreFiles(url) {
     console.error('Error:', error);
     showErrorToast('An error occurred while loading more files.');
   } finally {
-    $target.removeClass('disabled');
+    target?.classList.remove('disabled');
     pageData.diffFileInfo.isLoadingNewData = false;
   }
 }
@@ -185,11 +192,11 @@ function initRepoDiffShowMore() {
     e.preventDefault();
     const $target = $(e.target);
 
-    if ($target.hasClass('disabled')) {
+    if (e.target.classList.contains('disabled')) {
       return;
     }
 
-    $target.addClass('disabled');
+    e.target.classList.add('disabled');
 
     const url = $target.data('href');
 
@@ -205,7 +212,7 @@ function initRepoDiffShowMore() {
     } catch (error) {
       console.error('Error:', error);
     } finally {
-      $target.removeClass('disabled');
+      e.target.classList.remove('disabled');
     }
   });
 }
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index 59c455e2ab..fffe9c6109 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -51,7 +51,7 @@ export function isElemHidden(el) {
   return res[0];
 }
 
-export function queryElemSiblings(el, selector) {
+export function queryElemSiblings(el, selector = '*') {
   return Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector));
 }
 

From 42870cf40278e84024ccea41368312451f79a4d6 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 29 Mar 2024 22:24:17 +0300
Subject: [PATCH 580/679] Remove jQuery class from the commit button (#30178)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the commit button disabled toggling functionality and it works
as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-editor.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index fc951750a9..01dc4b95aa 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -147,8 +147,8 @@ export function initRepoEditor() {
       silent: true,
       dirtyClass: dirtyFileClass,
       fieldSelector: ':input:not(.commit-form-wrapper :input)',
-      change() {
-        const dirty = $(this).hasClass(dirtyFileClass);
+      change($form) {
+        const dirty = $form[0]?.classList.contains(dirtyFileClass);
         commitButton.disabled = !dirty;
       },
     });

From f31a88d3cb64106e75bbe8a3502856db71dbacfc Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 29 Mar 2024 21:32:35 +0100
Subject: [PATCH 581/679] Add `stylelint-value-no-unknown-custom-properties`
 and convert stylelint config to js (#30117)

Add
[`stylelint-value-no-unknown-custom-properties`](https://github.com/csstools/stylelint-value-no-unknown-custom-properties)
which lints for undefined CSS variables. No current violations.

To make it work properly with editor integrations, I had to convert the
config to JS to be able to pass absolute paths to the plugin, but this
is a needed change anyways.
---
 .github/labeler.yml                 |   2 +-
 .github/workflows/files-changed.yml |   2 +-
 .stylelintrc.yaml                   | 223 -------------------------
 package-lock.json                   |  17 ++
 package.json                        |   1 +
 stylelint.config.js                 | 245 ++++++++++++++++++++++++++++
 6 files changed, 265 insertions(+), 225 deletions(-)
 delete mode 100644 .stylelintrc.yaml
 create mode 100644 stylelint.config.js

diff --git a/.github/labeler.yml b/.github/labeler.yml
index 980b9c337c..4acdb6f6f5 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -53,7 +53,7 @@ modifies/internal:
           - ".gitpod.yml"
           - ".markdownlint.yaml"
           - ".spectral.yaml"
-          - ".stylelintrc.yaml"
+          - "stylelint.config.js"
           - ".yamllint.yaml"
           - ".github/**"
           - ".gitea/"
diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index b8535cb42b..9a609e0551 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -58,7 +58,7 @@ jobs:
               - "package-lock.json"
               - "Makefile"
               - ".eslintrc.yaml"
-              - ".stylelintrc.yaml"
+              - "stylelint.config.js"
               - ".npmrc"
 
             docs:
diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
deleted file mode 100644
index 60cce7dbf7..0000000000
--- a/.stylelintrc.yaml
+++ /dev/null
@@ -1,223 +0,0 @@
-plugins:
-  - stylelint-declaration-strict-value
-  - stylelint-declaration-block-no-ignored-properties
-  - "@stylistic/stylelint-plugin"
-
-ignoreFiles:
-  - "**/*.go"
-
-overrides:
-  - files: ["**/chroma/*", "**/codemirror/*", "**/standalone/*", "**/console.css", "font_i18n.css"]
-    rules:
-      scale-unlimited/declaration-strict-value: null
-  - files: ["**/chroma/*", "**/codemirror/*"]
-    rules:
-      block-no-empty: null
-  - files: ["**/*.vue"]
-    customSyntax: postcss-html
-
-rules:
-  "@stylistic/at-rule-name-case": null
-  "@stylistic/at-rule-name-newline-after": null
-  "@stylistic/at-rule-name-space-after": null
-  "@stylistic/at-rule-semicolon-newline-after": null
-  "@stylistic/at-rule-semicolon-space-before": null
-  "@stylistic/block-closing-brace-empty-line-before": null
-  "@stylistic/block-closing-brace-newline-after": null
-  "@stylistic/block-closing-brace-newline-before": null
-  "@stylistic/block-closing-brace-space-after": null
-  "@stylistic/block-closing-brace-space-before": null
-  "@stylistic/block-opening-brace-newline-after": null
-  "@stylistic/block-opening-brace-newline-before": null
-  "@stylistic/block-opening-brace-space-after": null
-  "@stylistic/block-opening-brace-space-before": always
-  "@stylistic/color-hex-case": lower
-  "@stylistic/declaration-bang-space-after": never
-  "@stylistic/declaration-bang-space-before": null
-  "@stylistic/declaration-block-semicolon-newline-after": null
-  "@stylistic/declaration-block-semicolon-newline-before": null
-  "@stylistic/declaration-block-semicolon-space-after": null
-  "@stylistic/declaration-block-semicolon-space-before": never
-  "@stylistic/declaration-block-trailing-semicolon": null
-  "@stylistic/declaration-colon-newline-after": null
-  "@stylistic/declaration-colon-space-after": null
-  "@stylistic/declaration-colon-space-before": never
-  "@stylistic/function-comma-newline-after": null
-  "@stylistic/function-comma-newline-before": null
-  "@stylistic/function-comma-space-after": null
-  "@stylistic/function-comma-space-before": null
-  "@stylistic/function-max-empty-lines": 0
-  "@stylistic/function-parentheses-newline-inside": never-multi-line
-  "@stylistic/function-parentheses-space-inside": null
-  "@stylistic/function-whitespace-after": null
-  "@stylistic/indentation": 2
-  "@stylistic/linebreaks": null
-  "@stylistic/max-empty-lines": 1
-  "@stylistic/max-line-length": null
-  "@stylistic/media-feature-colon-space-after": null
-  "@stylistic/media-feature-colon-space-before": never
-  "@stylistic/media-feature-name-case": null
-  "@stylistic/media-feature-parentheses-space-inside": null
-  "@stylistic/media-feature-range-operator-space-after": always
-  "@stylistic/media-feature-range-operator-space-before": always
-  "@stylistic/media-query-list-comma-newline-after": null
-  "@stylistic/media-query-list-comma-newline-before": null
-  "@stylistic/media-query-list-comma-space-after": null
-  "@stylistic/media-query-list-comma-space-before": null
-  "@stylistic/named-grid-areas-alignment": null
-  "@stylistic/no-empty-first-line": null
-  "@stylistic/no-eol-whitespace": true
-  "@stylistic/no-extra-semicolons": true
-  "@stylistic/no-missing-end-of-source-newline": null
-  "@stylistic/number-leading-zero": null
-  "@stylistic/number-no-trailing-zeros": null
-  "@stylistic/property-case": lower
-  "@stylistic/selector-attribute-brackets-space-inside": null
-  "@stylistic/selector-attribute-operator-space-after": null
-  "@stylistic/selector-attribute-operator-space-before": null
-  "@stylistic/selector-combinator-space-after": null
-  "@stylistic/selector-combinator-space-before": null
-  "@stylistic/selector-descendant-combinator-no-non-space": null
-  "@stylistic/selector-list-comma-newline-after": null
-  "@stylistic/selector-list-comma-newline-before": null
-  "@stylistic/selector-list-comma-space-after": always-single-line
-  "@stylistic/selector-list-comma-space-before": never-single-line
-  "@stylistic/selector-max-empty-lines": 0
-  "@stylistic/selector-pseudo-class-case": lower
-  "@stylistic/selector-pseudo-class-parentheses-space-inside": never
-  "@stylistic/selector-pseudo-element-case": lower
-  "@stylistic/string-quotes": double
-  "@stylistic/unicode-bom": null
-  "@stylistic/unit-case": lower
-  "@stylistic/value-list-comma-newline-after": null
-  "@stylistic/value-list-comma-newline-before": null
-  "@stylistic/value-list-comma-space-after": null
-  "@stylistic/value-list-comma-space-before": null
-  "@stylistic/value-list-max-empty-lines": 0
-  alpha-value-notation: null
-  annotation-no-unknown: true
-  at-rule-allowed-list: null
-  at-rule-disallowed-list: null
-  at-rule-empty-line-before: null
-  at-rule-no-unknown: [true, {ignoreAtRules: [tailwind]}]
-  at-rule-no-vendor-prefix: true
-  at-rule-property-required-list: null
-  block-no-empty: true
-  color-function-notation: null
-  color-hex-alpha: null
-  color-hex-length: null
-  color-named: null
-  color-no-hex: null
-  color-no-invalid-hex: true
-  comment-empty-line-before: null
-  comment-no-empty: true
-  comment-pattern: null
-  comment-whitespace-inside: null
-  comment-word-disallowed-list: null
-  custom-media-pattern: null
-  custom-property-empty-line-before: null
-  custom-property-no-missing-var-function: true
-  custom-property-pattern: null
-  declaration-block-no-duplicate-custom-properties: true
-  declaration-block-no-duplicate-properties: [true, {ignore: [consecutive-duplicates-with-different-values]}]
-  declaration-block-no-redundant-longhand-properties: null
-  declaration-block-no-shorthand-property-overrides: null
-  declaration-block-single-line-max-declarations: null
-  declaration-empty-line-before: null
-  declaration-no-important: null
-  declaration-property-max-values: null
-  declaration-property-unit-allowed-list: null
-  declaration-property-unit-disallowed-list: {line-height: [em]}
-  declaration-property-value-allowed-list: null
-  declaration-property-value-disallowed-list: null
-  declaration-property-value-no-unknown: true
-  font-family-name-quotes: always-where-recommended
-  font-family-no-duplicate-names: true
-  font-family-no-missing-generic-family-keyword: true
-  font-weight-notation: null
-  function-allowed-list: null
-  function-calc-no-unspaced-operator: true
-  function-disallowed-list: null
-  function-linear-gradient-no-nonstandard-direction: true
-  function-name-case: lower
-  function-no-unknown: true
-  function-url-no-scheme-relative: null
-  function-url-quotes: always
-  function-url-scheme-allowed-list: null
-  function-url-scheme-disallowed-list: null
-  hue-degree-notation: null
-  import-notation: string
-  keyframe-block-no-duplicate-selectors: true
-  keyframe-declaration-no-important: true
-  keyframe-selector-notation: null
-  keyframes-name-pattern: null
-  length-zero-no-unit: [true, ignore: [custom-properties], ignoreFunctions: [var]]
-  max-nesting-depth: null
-  media-feature-name-allowed-list: null
-  media-feature-name-disallowed-list: null
-  media-feature-name-no-unknown: true
-  media-feature-name-no-vendor-prefix: true
-  media-feature-name-unit-allowed-list: null
-  media-feature-name-value-allowed-list: null
-  media-feature-name-value-no-unknown: true
-  media-feature-range-notation: null
-  media-query-no-invalid: true
-  named-grid-areas-no-invalid: true
-  no-descending-specificity: null
-  no-duplicate-at-import-rules: true
-  no-duplicate-selectors: true
-  no-empty-source: true
-  no-invalid-double-slash-comments: true
-  no-invalid-position-at-import-rule: [true, ignoreAtRules: [tailwind]]
-  no-irregular-whitespace: true
-  no-unknown-animations: null
-  no-unknown-custom-properties: null
-  number-max-precision: null
-  plugin/declaration-block-no-ignored-properties: true
-  property-allowed-list: null
-  property-disallowed-list: null
-  property-no-unknown: true
-  property-no-vendor-prefix: null
-  rule-empty-line-before: null
-  rule-selector-property-disallowed-list: null
-  scale-unlimited/declaration-strict-value: [[/color$/, font-weight], {ignoreValues: /^(inherit|transparent|unset|initial|currentcolor|none)$/, ignoreFunctions: false, disableFix: true, expandShorthand: true}]
-  selector-anb-no-unmatchable: true
-  selector-attribute-name-disallowed-list: null
-  selector-attribute-operator-allowed-list: null
-  selector-attribute-operator-disallowed-list: null
-  selector-attribute-quotes: always
-  selector-class-pattern: null
-  selector-combinator-allowed-list: null
-  selector-combinator-disallowed-list: null
-  selector-disallowed-list: null
-  selector-id-pattern: null
-  selector-max-attribute: null
-  selector-max-class: null
-  selector-max-combinators: null
-  selector-max-compound-selectors: null
-  selector-max-id: null
-  selector-max-pseudo-class: null
-  selector-max-specificity: null
-  selector-max-type: null
-  selector-max-universal: null
-  selector-nested-pattern: null
-  selector-no-qualifying-type: null
-  selector-no-vendor-prefix: true
-  selector-not-notation: null
-  selector-pseudo-class-allowed-list: null
-  selector-pseudo-class-disallowed-list: null
-  selector-pseudo-class-no-unknown: true
-  selector-pseudo-element-allowed-list: null
-  selector-pseudo-element-colon-notation: double
-  selector-pseudo-element-disallowed-list: null
-  selector-pseudo-element-no-unknown: true
-  selector-type-case: lower
-  selector-type-no-unknown: [true, {ignore: [custom-elements]}]
-  shorthand-property-no-redundant-values: true
-  string-no-newline: true
-  time-min-milliseconds: null
-  unit-allowed-list: null
-  unit-disallowed-list: null
-  unit-no-unknown: true
-  value-keyword-case: null
-  value-no-vendor-prefix: [true, {ignoreValues: [box, inline-box]}]
diff --git a/package-lock.json b/package-lock.json
index 25fe14e1a6..21de79387f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -91,6 +91,7 @@
         "stylelint": "16.3.0",
         "stylelint-declaration-block-no-ignored-properties": "2.8.0",
         "stylelint-declaration-strict-value": "1.10.4",
+        "stylelint-value-no-unknown-custom-properties": "6.0.1",
         "svgo": "3.2.0",
         "updates": "16.0.0",
         "vite-string-plugin": "1.1.5",
@@ -10867,6 +10868,22 @@
         "stylelint": ">=7 <=16"
       }
     },
+    "node_modules/stylelint-value-no-unknown-custom-properties": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/stylelint-value-no-unknown-custom-properties/-/stylelint-value-no-unknown-custom-properties-6.0.1.tgz",
+      "integrity": "sha512-N60PTdaTknB35j6D4FhW0GL2LlBRV++bRpXMMldWMQZ240yFQaoltzlLY4lXXs7Z0J5mNUYZQ/gjyVtU2DhCMA==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0",
+        "resolve": "^1.22.8"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "peerDependencies": {
+        "stylelint": ">=16"
+      }
+    },
     "node_modules/stylelint/node_modules/ansi-regex": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
diff --git a/package.json b/package.json
index d5a1d46056..beea0e5d86 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
     "stylelint": "16.3.0",
     "stylelint-declaration-block-no-ignored-properties": "2.8.0",
     "stylelint-declaration-strict-value": "1.10.4",
+    "stylelint-value-no-unknown-custom-properties": "6.0.1",
     "svgo": "3.2.0",
     "updates": "16.0.0",
     "vite-string-plugin": "1.1.5",
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100644
index 0000000000..c34181233e
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,245 @@
+import {fileURLToPath} from 'node:url';
+
+const cssVarFiles = [
+  fileURLToPath(new URL('web_src/css/base.css', import.meta.url)),
+  fileURLToPath(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url)),
+  fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
+];
+
+/** @type {import('stylelint').Config} */
+export default {
+  plugins: [
+    'stylelint-declaration-strict-value',
+    'stylelint-declaration-block-no-ignored-properties',
+    'stylelint-value-no-unknown-custom-properties',
+    '@stylistic/stylelint-plugin',
+  ],
+  ignoreFiles: [
+    '**/*.go',
+  ],
+  overrides: [
+    {
+      files: ['**/chroma/*', '**/codemirror/*', '**/standalone/*', '**/console.css', 'font_i18n.css'],
+      rules: {
+        'scale-unlimited/declaration-strict-value': null,
+      },
+    },
+    {
+      files: ['**/chroma/*', '**/codemirror/*'],
+      rules: {
+        'block-no-empty': null,
+      },
+    },
+    {
+      files: ['**/*.vue'],
+      customSyntax: 'postcss-html',
+    },
+  ],
+  rules: {
+    '@stylistic/at-rule-name-case': null,
+    '@stylistic/at-rule-name-newline-after': null,
+    '@stylistic/at-rule-name-space-after': null,
+    '@stylistic/at-rule-semicolon-newline-after': null,
+    '@stylistic/at-rule-semicolon-space-before': null,
+    '@stylistic/block-closing-brace-empty-line-before': null,
+    '@stylistic/block-closing-brace-newline-after': null,
+    '@stylistic/block-closing-brace-newline-before': null,
+    '@stylistic/block-closing-brace-space-after': null,
+    '@stylistic/block-closing-brace-space-before': null,
+    '@stylistic/block-opening-brace-newline-after': null,
+    '@stylistic/block-opening-brace-newline-before': null,
+    '@stylistic/block-opening-brace-space-after': null,
+    '@stylistic/block-opening-brace-space-before': 'always',
+    '@stylistic/color-hex-case': 'lower',
+    '@stylistic/declaration-bang-space-after': 'never',
+    '@stylistic/declaration-bang-space-before': null,
+    '@stylistic/declaration-block-semicolon-newline-after': null,
+    '@stylistic/declaration-block-semicolon-newline-before': null,
+    '@stylistic/declaration-block-semicolon-space-after': null,
+    '@stylistic/declaration-block-semicolon-space-before': 'never',
+    '@stylistic/declaration-block-trailing-semicolon': null,
+    '@stylistic/declaration-colon-newline-after': null,
+    '@stylistic/declaration-colon-space-after': null,
+    '@stylistic/declaration-colon-space-before': 'never',
+    '@stylistic/function-comma-newline-after': null,
+    '@stylistic/function-comma-newline-before': null,
+    '@stylistic/function-comma-space-after': null,
+    '@stylistic/function-comma-space-before': null,
+    '@stylistic/function-max-empty-lines': 0,
+    '@stylistic/function-parentheses-newline-inside': 'never-multi-line',
+    '@stylistic/function-parentheses-space-inside': null,
+    '@stylistic/function-whitespace-after': null,
+    '@stylistic/indentation': 2,
+    '@stylistic/linebreaks': null,
+    '@stylistic/max-empty-lines': 1,
+    '@stylistic/max-line-length': null,
+    '@stylistic/media-feature-colon-space-after': null,
+    '@stylistic/media-feature-colon-space-before': 'never',
+    '@stylistic/media-feature-name-case': null,
+    '@stylistic/media-feature-parentheses-space-inside': null,
+    '@stylistic/media-feature-range-operator-space-after': 'always',
+    '@stylistic/media-feature-range-operator-space-before': 'always',
+    '@stylistic/media-query-list-comma-newline-after': null,
+    '@stylistic/media-query-list-comma-newline-before': null,
+    '@stylistic/media-query-list-comma-space-after': null,
+    '@stylistic/media-query-list-comma-space-before': null,
+    '@stylistic/named-grid-areas-alignment': null,
+    '@stylistic/no-empty-first-line': null,
+    '@stylistic/no-eol-whitespace': true,
+    '@stylistic/no-extra-semicolons': true,
+    '@stylistic/no-missing-end-of-source-newline': null,
+    '@stylistic/number-leading-zero': null,
+    '@stylistic/number-no-trailing-zeros': null,
+    '@stylistic/property-case': 'lower',
+    '@stylistic/selector-attribute-brackets-space-inside': null,
+    '@stylistic/selector-attribute-operator-space-after': null,
+    '@stylistic/selector-attribute-operator-space-before': null,
+    '@stylistic/selector-combinator-space-after': null,
+    '@stylistic/selector-combinator-space-before': null,
+    '@stylistic/selector-descendant-combinator-no-non-space': null,
+    '@stylistic/selector-list-comma-newline-after': null,
+    '@stylistic/selector-list-comma-newline-before': null,
+    '@stylistic/selector-list-comma-space-after': 'always-single-line',
+    '@stylistic/selector-list-comma-space-before': 'never-single-line',
+    '@stylistic/selector-max-empty-lines': 0,
+    '@stylistic/selector-pseudo-class-case': 'lower',
+    '@stylistic/selector-pseudo-class-parentheses-space-inside': 'never',
+    '@stylistic/selector-pseudo-element-case': 'lower',
+    '@stylistic/string-quotes': 'double',
+    '@stylistic/unicode-bom': null,
+    '@stylistic/unit-case': 'lower',
+    '@stylistic/value-list-comma-newline-after': null,
+    '@stylistic/value-list-comma-newline-before': null,
+    '@stylistic/value-list-comma-space-after': null,
+    '@stylistic/value-list-comma-space-before': null,
+    '@stylistic/value-list-max-empty-lines': 0,
+    'alpha-value-notation': null,
+    'annotation-no-unknown': true,
+    'at-rule-allowed-list': null,
+    'at-rule-disallowed-list': null,
+    'at-rule-empty-line-before': null,
+    'at-rule-no-unknown': [true, {ignoreAtRules: ['tailwind']}],
+    'at-rule-no-vendor-prefix': true,
+    'at-rule-property-required-list': null,
+    'block-no-empty': true,
+    'color-function-notation': null,
+    'color-hex-alpha': null,
+    'color-hex-length': null,
+    'color-named': null,
+    'color-no-hex': null,
+    'color-no-invalid-hex': true,
+    'comment-empty-line-before': null,
+    'comment-no-empty': true,
+    'comment-pattern': null,
+    'comment-whitespace-inside': null,
+    'comment-word-disallowed-list': null,
+    'csstools/value-no-unknown-custom-properties': [true, {importFrom: cssVarFiles}],
+    'custom-media-pattern': null,
+    'custom-property-empty-line-before': null,
+    'custom-property-no-missing-var-function': true,
+    'custom-property-pattern': null,
+    'declaration-block-no-duplicate-custom-properties': true,
+    'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates-with-different-values']}],
+    'declaration-block-no-redundant-longhand-properties': null,
+    'declaration-block-no-shorthand-property-overrides': null,
+    'declaration-block-single-line-max-declarations': null,
+    'declaration-empty-line-before': null,
+    'declaration-no-important': null,
+    'declaration-property-max-values': null,
+    'declaration-property-unit-allowed-list': null,
+    'declaration-property-unit-disallowed-list': {'line-height': ['em']},
+    'declaration-property-value-allowed-list': null,
+    'declaration-property-value-disallowed-list': null,
+    'declaration-property-value-no-unknown': true,
+    'font-family-name-quotes': 'always-where-recommended',
+    'font-family-no-duplicate-names': true,
+    'font-family-no-missing-generic-family-keyword': true,
+    'font-weight-notation': null,
+    'function-allowed-list': null,
+    'function-calc-no-unspaced-operator': true,
+    'function-disallowed-list': null,
+    'function-linear-gradient-no-nonstandard-direction': true,
+    'function-name-case': 'lower',
+    'function-no-unknown': true,
+    'function-url-no-scheme-relative': null,
+    'function-url-quotes': 'always',
+    'function-url-scheme-allowed-list': null,
+    'function-url-scheme-disallowed-list': null,
+    'hue-degree-notation': null,
+    'import-notation': 'string',
+    'keyframe-block-no-duplicate-selectors': true,
+    'keyframe-declaration-no-important': true,
+    'keyframe-selector-notation': null,
+    'keyframes-name-pattern': null,
+    'length-zero-no-unit': [true, {ignore: ['custom-properties']}, {ignoreFunctions: ['var']}],
+    'max-nesting-depth': null,
+    'media-feature-name-allowed-list': null,
+    'media-feature-name-disallowed-list': null,
+    'media-feature-name-no-unknown': true,
+    'media-feature-name-no-vendor-prefix': true,
+    'media-feature-name-unit-allowed-list': null,
+    'media-feature-name-value-allowed-list': null,
+    'media-feature-name-value-no-unknown': true,
+    'media-feature-range-notation': null,
+    'media-query-no-invalid': true,
+    'named-grid-areas-no-invalid': true,
+    'no-descending-specificity': null,
+    'no-duplicate-at-import-rules': true,
+    'no-duplicate-selectors': true,
+    'no-empty-source': true,
+    'no-invalid-double-slash-comments': true,
+    'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}],
+    'no-irregular-whitespace': true,
+    'no-unknown-animations': null,
+    'no-unknown-custom-properties': null,
+    'number-max-precision': null,
+    'plugin/declaration-block-no-ignored-properties': true,
+    'property-allowed-list': null,
+    'property-disallowed-list': null,
+    'property-no-unknown': true,
+    'property-no-vendor-prefix': null,
+    'rule-empty-line-before': null,
+    'rule-selector-property-disallowed-list': null,
+    'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: false, disableFix: true, expandShorthand: true}],
+    'selector-anb-no-unmatchable': true,
+    'selector-attribute-name-disallowed-list': null,
+    'selector-attribute-operator-allowed-list': null,
+    'selector-attribute-operator-disallowed-list': null,
+    'selector-attribute-quotes': 'always',
+    'selector-class-pattern': null,
+    'selector-combinator-allowed-list': null,
+    'selector-combinator-disallowed-list': null,
+    'selector-disallowed-list': null,
+    'selector-id-pattern': null,
+    'selector-max-attribute': null,
+    'selector-max-class': null,
+    'selector-max-combinators': null,
+    'selector-max-compound-selectors': null,
+    'selector-max-id': null,
+    'selector-max-pseudo-class': null,
+    'selector-max-specificity': null,
+    'selector-max-type': null,
+    'selector-max-universal': null,
+    'selector-nested-pattern': null,
+    'selector-no-qualifying-type': null,
+    'selector-no-vendor-prefix': true,
+    'selector-not-notation': null,
+    'selector-pseudo-class-allowed-list': null,
+    'selector-pseudo-class-disallowed-list': null,
+    'selector-pseudo-class-no-unknown': true,
+    'selector-pseudo-element-allowed-list': null,
+    'selector-pseudo-element-colon-notation': 'double',
+    'selector-pseudo-element-disallowed-list': null,
+    'selector-pseudo-element-no-unknown': true,
+    'selector-type-case': 'lower',
+    'selector-type-no-unknown': [true, {ignore: ['custom-elements']}],
+    'shorthand-property-no-redundant-values': true,
+    'string-no-newline': true,
+    'time-min-milliseconds': null,
+    'unit-allowed-list': null,
+    'unit-disallowed-list': null,
+    'unit-no-unknown': true,
+    'value-keyword-case': null,
+    'value-no-vendor-prefix': [true, {ignoreValues: ['box', 'inline-box']}],
+  },
+};

From b6a3cd4b8dc20ba48d0044a972f6ff0f0de6e49e Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Fri, 29 Mar 2024 22:55:10 +0100
Subject: [PATCH 582/679] Include encoding in signature payload (#30174)

Fixes #30119

Include the encoding in the signature payload.

before

![grafik](https://github.com/go-gitea/gitea/assets/1666336/01ab94a3-8af5-4d6f-be73-a10b65a15421)

after

![grafik](https://github.com/go-gitea/gitea/assets/1666336/3a37d438-c70d-4d69-b178-d170e74aa683)
---
 modules/git/commit_convert_gogit.go |  6 +++
 modules/git/commit_reader.go        |  2 +
 modules/git/commit_test.go          | 67 +++++++++++++++++++++++++++++
 3 files changed, 75 insertions(+)

diff --git a/modules/git/commit_convert_gogit.go b/modules/git/commit_convert_gogit.go
index 819ea0d1db..33ef2f4487 100644
--- a/modules/git/commit_convert_gogit.go
+++ b/modules/git/commit_convert_gogit.go
@@ -47,6 +47,12 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
 		return nil
 	}
 
+	if c.Encoding != "" && c.Encoding != "UTF-8" {
+		if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil {
+			return nil
+		}
+	}
+
 	if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
 		return nil
 	}
diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go
index 4809d6c7ed..56c41dc473 100644
--- a/modules/git/commit_reader.go
+++ b/modules/git/commit_reader.go
@@ -84,6 +84,8 @@ readLoop:
 				commit.Committer = &Signature{}
 				commit.Committer.Decode(data)
 				_, _ = payloadSB.Write(line)
+			case "encoding":
+				_, _ = payloadSB.Write(line)
 			case "gpgsig":
 				fallthrough
 			case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present.
diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go
index e512eecc56..a33e7df31a 100644
--- a/modules/git/commit_test.go
+++ b/modules/git/commit_test.go
@@ -125,6 +125,73 @@ empty commit`, commitFromReader.Signature.Payload)
 	assert.EqualValues(t, commitFromReader, commitFromReader2)
 }
 
+func TestCommitWithEncodingFromReader(t *testing.T) {
+	commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
+tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
+parent 47b24e7ab977ed31c5a39989d570847d6d0052af
+author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+encoding ISO-8859-1
+gpgsig -----BEGIN PGP SIGNATURE-----
+ 
+ iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
+ Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
+ gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
+ zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
+ frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
+ FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
+ G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
+ SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
+ yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
+ jw4YcO5u
+ =r3UU
+ -----END PGP SIGNATURE-----
+
+ISO-8859-1`
+
+	sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
+	gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
+	assert.NoError(t, err)
+	assert.NotNil(t, gitRepo)
+	defer gitRepo.Close()
+
+	commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
+	assert.NoError(t, err)
+	if !assert.NotNil(t, commitFromReader) {
+		return
+	}
+	assert.EqualValues(t, sha, commitFromReader.ID)
+	assert.EqualValues(t, `-----BEGIN PGP SIGNATURE-----
+
+iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
+Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
+gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
+zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
+frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
+FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
+G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
+SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
+yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
+jw4YcO5u
+=r3UU
+-----END PGP SIGNATURE-----
+`, commitFromReader.Signature.Signature)
+	assert.EqualValues(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
+parent 47b24e7ab977ed31c5a39989d570847d6d0052af
+author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+encoding ISO-8859-1
+
+ISO-8859-1`, commitFromReader.Signature.Payload)
+	assert.EqualValues(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String())
+
+	commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
+	assert.NoError(t, err)
+	commitFromReader.CommitMessage += "\n\n"
+	commitFromReader.Signature.Payload += "\n\n"
+	assert.EqualValues(t, commitFromReader, commitFromReader2)
+}
+
 func TestHasPreviousCommit(t *testing.T) {
 	bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
 

From 66f7d47d2c702bab4ca9bcedc1c0ba9ddfa49a17 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 30 Mar 2024 12:40:39 +0300
Subject: [PATCH 583/679] Remove jQuery class from the comment context menu
 (#30179)

- Switched from jQuery class functions to plain JavaScript
- Tested the comment context menu functionality and it works as before

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/modules/fomantic/dropdown.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
index e795e8e2c8..82e710860d 100644
--- a/web_src/js/modules/fomantic/dropdown.js
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -207,7 +207,7 @@ function attachDomEvents(dropdown, focusable, menu) {
       if (!$item) $item = $(menu).find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
       // if the selected item is clickable, then trigger the click event.
       // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
-      if ($item && ($item[0].matches('a') || $item.hasClass('js-aria-clickable'))) $item[0].click();
+      if ($item?.[0]?.matches('a, .js-aria-clickable')) $item[0].click();
     }
   });
 

From b535c6ca7b9e8c4bcf5637091ee5ad6d9c807c31 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sat, 30 Mar 2024 20:36:28 +0300
Subject: [PATCH 584/679] Remove jQuery class from the project page (#30183)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the edit column modal functionality and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-projects.js | 45 ++++++++++++++--------------
 1 file changed, 23 insertions(+), 22 deletions(-)

diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index d9ae85a8d2..80e945a0f2 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -94,47 +94,46 @@ async function initRepoProjectSortable() {
 }
 
 export function initRepoProject() {
-  if (!$('.repository.projects').length) {
+  if (!document.querySelector('.repository.projects')) {
     return;
   }
 
   const _promise = initRepoProjectSortable();
 
-  $('.edit-project-column-modal').each(function () {
-    const $projectHeader = $(this).closest('.project-column-header');
-    const $projectTitleLabel = $projectHeader.find('.project-column-title');
-    const $projectTitleInput = $(this).find('.project-column-title-input');
-    const $projectColorInput = $(this).find('#new_project_column_color');
-    const $boardColumn = $(this).closest('.project-column');
+  for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
+    const projectHeader = modal.closest('.project-column-header');
+    const projectTitleLabel = projectHeader?.querySelector('.project-column-title');
+    const projectTitleInput = modal.querySelector('.project-column-title-input');
+    const projectColorInput = modal.querySelector('#new_project_column_color');
+    const boardColumn = modal.closest('.project-column');
+    const bgColor = boardColumn?.style.backgroundColor;
 
-    const bgColor = $boardColumn[0].style.backgroundColor;
     if (bgColor) {
-      setLabelColor($projectHeader, rgbToHex(bgColor));
+      setLabelColor(projectHeader, rgbToHex(bgColor));
     }
 
-    $(this).find('.edit-project-column-button').on('click', async function (e) {
+    modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
       e.preventDefault();
-
       try {
-        await PUT($(this).data('url'), {
+        await PUT(this.getAttribute('data-url'), {
           data: {
-            title: $projectTitleInput.val(),
-            color: $projectColorInput.val(),
+            title: projectTitleInput?.value,
+            color: projectColorInput?.value,
           },
         });
       } catch (error) {
         console.error(error);
       } finally {
-        $projectTitleLabel.text($projectTitleInput.val());
-        $projectTitleInput.closest('form').removeClass('dirty');
-        if ($projectColorInput.val()) {
-          setLabelColor($projectHeader, $projectColorInput.val());
+        projectTitleLabel.textContent = projectTitleInput?.value;
+        projectTitleInput.closest('form')?.classList.remove('dirty');
+        if (projectColorInput?.value) {
+          setLabelColor(projectHeader, projectColorInput.value);
         }
-        $boardColumn[0].style = `background: ${$projectColorInput.val()} !important`;
+        boardColumn.style = `background: ${projectColorInput.value} !important`;
         $('.ui.modal').modal('hide');
       }
     });
-  });
+  }
 
   $('.default-project-column-modal').each(function () {
     const $boardColumn = $(this).closest('.project-column');
@@ -187,9 +186,11 @@ export function initRepoProject() {
 function setLabelColor(label, color) {
   const {r, g, b} = tinycolor(color).toRgb();
   if (useLightTextOnBackground(r, g, b)) {
-    label.removeClass('dark-label').addClass('light-label');
+    label.classList.remove('dark-label');
+    label.classList.add('light-label');
   } else {
-    label.removeClass('light-label').addClass('dark-label');
+    label.classList.remove('light-label');
+    label.classList.add('dark-label');
   }
 }
 

From f32ce753f6518caa815d7b6bc44bc03806e8d049 Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Sat, 30 Mar 2024 19:11:50 +0100
Subject: [PATCH 585/679] Use Crowdin action for translation sync (#30054)

Switch from the old self-built action to the official one.

We get:
- config managed inside the repo
- automatic upload when source file changes
- automatic invalidation if source string changes (tested)
- automatic download of new translation files

Tested both upload and download.
---
 .github/workflows/cron-translations.yml | 33 +++++++++----------------
 crowdin.yml                             | 12 +++++++++
 2 files changed, 23 insertions(+), 22 deletions(-)
 create mode 100644 crowdin.yml

diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml
index 390aae7c07..f1b51debf1 100644
--- a/.github/workflows/cron-translations.yml
+++ b/.github/workflows/cron-translations.yml
@@ -11,14 +11,19 @@ jobs:
     if: github.repository == 'go-gitea/gitea'
     steps:
       - uses: actions/checkout@v4
-      - name: download from crowdin
-        uses: docker://jonasfranz/crowdin
+      - uses: crowdin/github-action@v1
+        with:
+          upload_sources: true
+          upload_translations: false
+          download_sources: false
+          download_translations: true
+          push_translations: false
+          push_sources: false
+          create_pull_request: false
+          config: crowdin.yml
         env:
+          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
           CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
-          PLUGIN_DOWNLOAD: true
-          PLUGIN_EXPORT_DIR: options/locale/
-          PLUGIN_IGNORE_BRANCH: true
-          PLUGIN_PROJECT_IDENTIFIER: gitea
       - name: update locales
         run: ./build/update-locales.sh
       - name: push translations to repo
@@ -31,19 +36,3 @@ jobs:
           commit_message: "[skip ci] Updated translations via Crowdin"
           remote: "git@github.com:go-gitea/gitea.git"
           ssh_key: ${{ secrets.DEPLOY_KEY }}
-  crowdin-push:
-    runs-on: ubuntu-latest
-    if: github.repository == 'go-gitea/gitea'
-    steps:
-      - uses: actions/checkout@v4
-      - name: push translations to crowdin
-        uses: docker://jonasfranz/crowdin
-        env:
-          CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
-          PLUGIN_UPLOAD: true
-          PLUGIN_EXPORT_DIR: options/locale/
-          PLUGIN_IGNORE_BRANCH: true
-          PLUGIN_PROJECT_IDENTIFIER: gitea
-          PLUGIN_FILES: |
-            locale_en-US.ini: options/locale/locale_en-US.ini
-          PLUGIN_BRANCH: main
diff --git a/crowdin.yml b/crowdin.yml
new file mode 100644
index 0000000000..35a38d768c
--- /dev/null
+++ b/crowdin.yml
@@ -0,0 +1,12 @@
+project_id_env: CROWDIN_PROJECT_ID
+api_token_env: CROWDIN_KEY
+base_path: "."
+base_url: "https://api.crowdin.com"
+preserve_hierarchy: true
+files:
+  - source: "/options/locale/locale_en-US.ini"
+    translation: "/options/locale/locale_%locale%.ini"
+    type: "ini"
+    skip_untranslated_strings: true
+    export_only_approved: true
+    update_option: "update_as_unapproved"

From bcf3be3a6c2794f7a3841f9d110b14be29327e1a Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Sat, 30 Mar 2024 18:47:50 +0000
Subject: [PATCH 586/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_cs-CZ.ini | 411 +++++++++++++++++++-------------
 options/locale/locale_de-DE.ini | 206 ++++++++++++----
 options/locale/locale_el-GR.ini |  58 +----
 options/locale/locale_es-ES.ini |  58 +----
 options/locale/locale_fa-IR.ini |  43 +---
 options/locale/locale_fi-FI.ini |  34 +--
 options/locale/locale_fr-FR.ini |  61 ++---
 options/locale/locale_hu-HU.ini |  32 +--
 options/locale/locale_id-ID.ini |  27 +--
 options/locale/locale_is-IS.ini |  29 +--
 options/locale/locale_it-IT.ini |  45 +---
 options/locale/locale_ja-JP.ini |  93 ++++----
 options/locale/locale_ko-KR.ini |  31 +--
 options/locale/locale_lv-LV.ini |  58 +----
 options/locale/locale_nl-NL.ini |  45 +---
 options/locale/locale_pl-PL.ini |  42 +---
 options/locale/locale_pt-BR.ini | 116 +++++----
 options/locale/locale_pt-PT.ini | 144 +++++++----
 options/locale/locale_ru-RU.ini |  58 +----
 options/locale/locale_si-LK.ini |  42 +---
 options/locale/locale_sk-SK.ini |  35 +--
 options/locale/locale_sv-SE.ini |  35 +--
 options/locale/locale_tr-TR.ini |  62 ++---
 options/locale/locale_uk-UA.ini |  43 +---
 options/locale/locale_zh-CN.ini |  58 +----
 options/locale/locale_zh-HK.ini |  17 +-
 options/locale/locale_zh-TW.ini |  54 +----
 27 files changed, 888 insertions(+), 1049 deletions(-)

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 2a421b1172..4abf813725 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -25,6 +25,7 @@ enable_javascript=Tato stránka vyžaduje JavaScript.
 toc=Obsah
 licenses=Licence
 return_to_gitea=Vrátit se do Gitea
+more_items=Více položek
 
 username=Uživatelské jméno
 email=E-mailová adresa
@@ -75,7 +76,7 @@ collaborative=Spolupráce
 forks=Rozštěpení
 
 activities=Aktivity
-pull_requests=Požadavky na natažení
+pull_requests=Pull requesty
 issues=Úkoly
 milestones=Milníky
 
@@ -113,6 +114,7 @@ loading=Načítá se…
 error=Chyba
 error404=Stránka, kterou se snažíte zobrazit, buď <strong>neexistuje</strong>, nebo <strong>nemáte oprávnění</strong> ji zobrazit.
 go_back=Zpět
+invalid_data=Neplatná data: %v
 
 never=Nikdy
 unknown=Neznámý
@@ -142,6 +144,43 @@ confirm_delete_selected=Potvrdit odstranění všech vybraných položek?
 name=Název
 value=Hodnota
 
+filter=Filtr
+filter.clear=Vymazat filtr
+filter.is_archived=Archivováno
+filter.not_archived=Nearchivované
+filter.is_fork=Rozštěpený
+filter.not_fork=Není rozštěpený
+filter.is_mirror=Zrcadlen
+filter.not_mirror=Není zrcadleno
+filter.is_template=Šablona
+filter.not_template=Není šablona
+filter.public=Veřejná
+filter.private=Soukromý
+
+no_results_found=Nebyly nalezeny žádné výsledky.
+
+[search]
+search=Hledat...
+type_tooltip=Druh vyhledávání
+fuzzy=Fuzzy
+fuzzy_tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
+match=Shoda
+match_tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
+repo_kind=Hledat repozitáře...
+user_kind=Hledat uživatele...
+org_kind=Hledat organizace...
+team_kind=Hledat týmy...
+code_kind=Hledat kód...
+code_search_unavailable=Vyhledávání kódu není momentálně dostupné. Obraťte se na správce webu.
+code_search_by_git_grep=Aktuální výsledky vyhledávání kódu jsou poskytovány pomocí „git grep“. Pokud správce webu povolí index repozitáře, mohou být výsledky lepší.
+package_kind=Hledat balíčky...
+project_kind=Hledat projekty...
+branch_kind=Hledat větve...
+commit_kind=Hledat commity...
+runner_kind=Hledat runnery...
+no_results=Nebyly nalezeny žádné odpovídající výsledky.
+keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu.
+
 [aria]
 navbar=Navigační lišta
 footer=Patička
@@ -247,6 +286,7 @@ email_title=Nastavení e-mailu
 smtp_addr=Server SMTP
 smtp_port=Port SMTP
 smtp_from=Odeslat e-mail jako
+smtp_from_invalid=Adresa "Odeslat e-mail jako" je neplatná
 smtp_from_helper=E-mailová adresa, kterou bude Gitea používat. Zadejte běžnou e-mailovou adresu, nebo použijte formát "Jméno"<email@example.com>.
 mailer_user=Uživatelské jméno SMTP
 mailer_password=Heslo pro SMTP
@@ -306,6 +346,7 @@ env_config_keys=Konfigurace prostředí
 env_config_keys_prompt=Následující proměnné prostředí budou také použity pro váš konfigurační soubor:
 
 [home]
+nav_menu=Navigační menu
 uname_holder=Uživatelské jméno nebo e-mailová adresa
 password_holder=Heslo
 switch_dashboard_context=Přepnout kontext přehledu
@@ -315,7 +356,6 @@ collaborative_repos=Společné repozitáře
 my_orgs=Mé organizace
 my_mirrors=Má zrcadla
 view_home=Zobrazit %s
-search_repos=Nalézt repozitář…
 filter=Ostatní filtry
 filter_by_team_repositories=Filtrovat podle repozitářů týmu
 feed_of=Kanál z „%s“
@@ -336,20 +376,8 @@ issues.in_your_repos=Ve vašich repozitářích
 repos=Repozitáře
 users=Uživatelé
 organizations=Organizace
-search=Vyhledat
 go_to=Přejít na
 code=Kód
-search.type.tooltip=Druh vyhledávání
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
-search.match=Shoda
-search.match.tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
-code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu.
-repo_no_results=Nebyly nalezeny žádné odpovídající repozitáře.
-user_no_results=Nebyly nalezeni žádní odpovídající uživatelé.
-org_no_results=Nebyly nalezeny žádné odpovídající organizace.
-code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu.
-code_search_results=Výsledky hledání pro „%s“
 code_last_indexed_at=Naposledy indexováno %s
 relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty.
 relevant_repositories=Zobrazují se pouze relevantní repositáře, <a href="%s">zobrazit nefiltrované výsledky</a>.
@@ -367,7 +395,7 @@ forgot_password_title=Zapomenuté heslo
 forgot_password=Zapomenuté heslo?
 sign_up_now=Potřebujete účet? Zaregistrujte se.
 sign_up_successful=Účet byl úspěšně vytvořen. Vítejte!
-confirmation_mail_sent_prompt=Na adresu <b>%s</b> byl zaslán nový potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces registrace.
+confirmation_mail_sent_prompt_ex=Nový potvrzovací e-mail byl odeslán na <b>%s</b>. Zkontrolujte prosím svou doručenou poštu během následujících %s a dokončete proces registrace. Pokud je Vaše registrační e-mailová adresa nesprávná, můžete se znovu přihlásit a změnit ji.
 must_change_password=Aktualizujte své heslo
 allow_password_change=Vyžádat od uživatele změnu hesla (doporučeno)
 reset_password_mail_sent_prompt=Na adresu <b>%s</b> byl zaslán potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces obnovení účtu.
@@ -377,6 +405,7 @@ prohibit_login=Přihlášení zakázáno
 prohibit_login_desc=Vašemu účtu je zakázáno se přihlásit, kontaktujte prosím správce webu.
 resent_limit_prompt=Omlouváme se, ale před chvílí jste požádal o zaslání aktivačního e-mailu. Počkejte prosím 3 minuty a pak to zkuste znovu.
 has_unconfirmed_mail=Zdravím, %s, máte nepotvrzenou e-mailovou adresu (<b>%s</b>). Pokud jste nedostali e-mail pro potvrzení nebo potřebujete zaslat nový, klikněte prosím na tlačítku níže.
+change_unconfirmed_mail_address=Pokud je Vaše registrační e-mailová adresa nesprávná, můžete ji zde změnit a znovu odeslat nový potvrzovací e-mail.
 resend_mail=Klikněte zde pro odeslání aktivačního e-mailu
 email_not_associate=Tato e-mailová adresa není spojena s žádným účtem.
 send_reset_mail=Zaslat e-mail pro obnovení účtu
@@ -453,7 +482,7 @@ reset_password.text=Klikněte prosím na následující odkaz pro obnovení vaš
 
 register_success=Registrace byla úspěšná
 
-issue_assigned.pull=@%[1]s vás přiřadil/a k požadavku na natažení %[2]s repozitáři %[3]s.
+issue_assigned.pull=@%[1]s vás přiřadil/a k pull requestu %[2]s v repozitáři %[3]s.
 issue_assigned.issue=@%[1]s vás přiřadil/a k úkolu %[2]s repozitáři %[3]s.
 
 issue.x_mentioned_you=<b>@%s</b> vás zmínil/a:
@@ -463,11 +492,11 @@ issue.action.push_n=<b>@%[1]s</b> nahrál/a %[3]d commity do %[2]s
 issue.action.close=<b>@%[1]s</b> uzavřel/a #%[2]d.
 issue.action.reopen=<b>@%[1]s</b> znovu otevřel/a #%[2]d.
 issue.action.merge=<b>@%[1]s</b> sloučil/a #%[2]d do %[3]s.
-issue.action.approve=<b>@%[1]s</b> schválil/a tento požadavek na natažení.
-issue.action.reject=<b>@%[1]s</b> požadoval/a změny v tomto požadavku na natažení.
-issue.action.review=<b>@%[1]s</b> okomentoval/a tento požadavek na natažení.
-issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu z %[2]s pro tento požadavek na natažení.
-issue.action.ready_for_review=<b>@%[1]s</b> označil/a tento požadavek na natažení jako připravený ke kontrole.
+issue.action.approve=<b>@%[1]s</b> schválil/a tento pull request.
+issue.action.reject=<b>@%[1]s</b> požadoval/a změny v tomto pull requestu.
+issue.action.review=<b>@%[1]s</b> okomentoval/a tento pull request.
+issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu z %[2]s pro tento pull request.
+issue.action.ready_for_review=<b>@%[1]s</b> označil/a tento pull request jako připravený ke kontrole.
 issue.action.new=<b>@%[1]s</b> vytvořil/a #%[2]d.
 issue.in_tree_path=V %s:
 
@@ -557,6 +586,7 @@ team_name_been_taken=Název týmu je již použit.
 team_no_units_error=Povolit přístup alespoň do jedné sekce repozitáře.
 email_been_used=Tato e-mailová adresa je již používána.
 email_invalid=Emailová adresa je neplatná.
+email_domain_is_not_allowed=Doména uživatelského e-mailu <b>%s</b> je v rozporu s EMAIL_DOMAIN_ALLOWLIST nebo EMAIL_DOMAIN_BLOCKLIST. Ujistěte se, že je Vaše operace očekávána.
 openid_been_used=OpenID addresa „%s“ je již použita.
 username_password_incorrect=Uživatelské jméno nebo heslo není správné.
 password_complexity=Heslo nesplňuje požadavky na složitost:
@@ -568,6 +598,8 @@ enterred_invalid_repo_name=Zadaný název repozitáře není správný.
 enterred_invalid_org_name=Zadaný název organizace není správný.
 enterred_invalid_owner_name=Nové jméno vlastníka není správné.
 enterred_invalid_password=Zadané heslo není správné.
+unset_password=Přihlášený uživatel nenastavil heslo.
+unsupported_login_type=Typ přihlášení není podporován pro odstranění účtu.
 user_not_exist=Tento uživatel neexistuje.
 team_not_exist=Tento tým neexistuje.
 last_org_owner=Nemůžete odstranit posledního uživatele z týmu „vlastníci“. Musí existovat alespoň jeden vlastník pro organizaci.
@@ -617,6 +649,30 @@ form.name_reserved=Uživatelské jméno „%s“ je rezervováno.
 form.name_pattern_not_allowed=Vzor „%s“ není povolen v uživatelském jméně.
 form.name_chars_not_allowed=Uživatelské jméno „%s“ obsahuje neplatné znaky.
 
+block.block=Blokovat
+block.block.user=Zablokovat Uživatele
+block.block.org=Blokovat uživatele pro organizaci
+block.block.failure=Nepodařilo se zablokovat uživatele: %s
+block.unblock=Odblokovat
+block.unblock.failure=Nepodařilo se odblokovat uživatele: %s
+block.blocked=Zablokovali jste tohoto uživatele.
+block.title=Zablokovat Uživatele
+block.info=Blokování uživatele brání v interakci s repozitáři, jako je otevírání nebo komentování pull requestů nebo úkolů. Další informace o blokování uživatele.
+block.info_1=Zablokování uživatele zabrání následujícím akcím na vašem účtu a repozirářích:
+block.info_2=sledují váš účet
+block.info_3=pošle vám oznámení pomocí @zmínění vašeho uživatelského jména
+block.info_4=pozváním vás jako spolupracovníka do jejich repozitářů
+block.info_5=oblíbení, rozštěpení nebo sledování repozitářů
+block.info_6=otevření a komentování úkolů nebo pull requestů
+block.info_7=reagovat na své komentáře v úkolech nebo pull requestů
+block.user_to_block=Uživatel k blokování
+block.note=Poznámka
+block.note.title=Volitelná poznámka:
+block.note.info=Poznámka není pro blokovaného uživatele viditelná.
+block.note.edit=Upravit poznámku
+block.list=Blokovaní uživatelé
+block.list.none=Nemáte blokované žádné uživatele.
+
 [settings]
 profile=Profil
 account=Účet
@@ -761,7 +817,6 @@ gpg_invalid_token_signature=Zadaný GPG klíč, podpis a token se neshodují neb
 gpg_token_required=Musíte zadat podpis pro níže uvedený token
 gpg_token=Token
 gpg_token_help=Podpis můžete vygenerovat pomocí:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Zakódovaný podpis GPG
 key_signature_gpg_placeholder=Začíná s „-----BEGIN PGP SIGNATURE-----“
 verify_gpg_key_success=GPG klíč „%s“ byl ověřen.
@@ -833,6 +888,7 @@ select_permissions=Vyberte oprávnění
 permission_no_access=Bez přístupu
 permission_read=Přečtené
 permission_write=čtení i zápis
+access_token_desc=Vybraná oprávnění tokenu omezují autorizaci pouze na odpovídající trasy <a %s>API</a>. Přečtěte si <a %s>dokumentaci</a> pro více informací.
 at_least_one_permission=Musíte vybrat alespoň jedno oprávnění pro vytvoření tokenu
 permissions_list=Oprávnění:
 
@@ -953,8 +1009,9 @@ fork_visibility_helper=Viditelnost rozštěpeného repozitáře nemůže být zm
 fork_branch=Větev, která má být klonována pro fork
 all_branches=Všechny větve
 fork_no_valid_owners=Tento repozitář nemůže být rozštěpen, protože neexistují žádní platní vlastníci.
+fork.blocked_user=Nelze rozštěpit repozitář, protože jste blokováni majitelem repozitáře.
 use_template=Použít tuto šablonu
-clone_in_vsc=Klonovat ve VS Code
+open_with_editor=Otevřít pomocí %s
 download_zip=Stáhnout ZIP
 download_tar=Stáhnout TAR.GZ
 download_bundle=Stáhnout BUNDLE
@@ -984,11 +1041,12 @@ trust_model_helper_default=Výchozí: Použít výchozí model důvěry pro tuto
 create_repo=Vytvořit repozitář
 default_branch=Výchozí větev
 default_branch_label=výchozí
-default_branch_helper=Výchozí větev je základní větev pro požadavky na natažení a commity kódu.
+default_branch_helper=Výchozí větev je základní větev pro pull requesty a commity kódu.
 mirror_prune=Vyčistit
 mirror_prune_desc=Odstranit zastaralé reference na vzdálené sledování
 mirror_interval=Interval zrcadlení (platné časové jednotky jsou „h“, „m“ a „s“). 0 zakáže periodickou synchronizaci. (Minimální interval: %s)
 mirror_interval_invalid=Interval zrcadlení není platný.
+mirror_sync=synchronizováno
 mirror_sync_on_commit=Synchronizovat při nahrávání revizí
 mirror_address=Klonovat z URL
 mirror_address_desc=Zadejte požadované přístupové údaje do sekce Ověření.
@@ -1019,6 +1077,7 @@ delete_preexisting=Odstranit již existující soubory
 delete_preexisting_content=Odstranit soubory v %s
 delete_preexisting_success=Smazány nepřijaté soubory v %s
 blame_prior=Zobrazit blame před touto změnou
+blame.ignore_revs=Ignorování revizí v <a href="%s">.git-blame-ignorerevs</a>. Klikněte zde <a href="%s">pro obejití</a> a zobrazení normálního pohledu blame.
 blame.ignore_revs.failed=Nepodařilo se ignorovat revize v <a href="%s">.git-blame-ignore-revs</a>.
 author_search_tooltip=Zobrazí maximálně 30 uživatelů
 
@@ -1051,10 +1110,10 @@ template.issue_labels=Štítky úkolů
 template.one_item=Musíte vybrat alespoň jednu položku šablony
 template.invalid=Musíte vybrat repositář šablony
 
-archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo požadavky na natažení.
-archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo požadavky na natažení.
+archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo pull requesty.
+archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo pull requesty.
 archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly.
-archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat požadavky na natažení.
+archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat pull requesty.
 
 form.reach_limit_of_creation_1=Již jste dosáhli svůj limit %d repozitář.
 form.reach_limit_of_creation_n=Již jste dosáhli svůj limit %d repozitářů.
@@ -1075,7 +1134,7 @@ migrate_items_wiki=Wiki
 migrate_items_milestones=Milníky
 migrate_items_labels=Štítky
 migrate_items_issues=Úkoly
-migrate_items_pullrequests=Požadavky na natažení
+migrate_items_pullrequests=Pull requesty
 migrate_items_merge_requests=Sloučit požadavky
 migrate_items_releases=Vydání
 migrate_repo=Migrovat repozitář
@@ -1110,7 +1169,7 @@ migrate.migrating_milestones=Migrování milnků
 migrate.migrating_labels=Migrování štítků
 migrate.migrating_releases=Migrování vydání
 migrate.migrating_issues=Migrování úkolů
-migrate.migrating_pulls=Migrování požadavků na natažení
+migrate.migrating_pulls=Migrování pull requestů
 migrate.cancel_migrating_title=Zrušit migraci
 migrate.cancel_migrating_confirm=Chcete zrušit tuto migraci?
 
@@ -1126,6 +1185,7 @@ watch=Sledovat
 unstar=Odoblíbit
 star=Oblíbit
 fork=Rozštěpit
+action.blocked_user=Nelze provést akci, protože jste zablokování vlastníkem repozitáře.
 download_archive=Stáhnout repozitář
 more_operations=Další operace
 
@@ -1148,7 +1208,7 @@ find_tag=Najít značku
 branches=Větve
 tags=Značky
 issues=Úkoly
-pulls=Požadavky na natažení
+pulls=Pull requesty
 project_board=Projekty
 packages=Balíčky
 actions=Akce
@@ -1193,7 +1253,7 @@ vendored=Vendorováno
 generated=Generováno
 commit_graph=Graf commitů
 commit_graph.select=Vybrat větve
-commit_graph.hide_pr_refs=Skrýt požadavky na natažení
+commit_graph.hide_pr_refs=Skrýt pull requesty
 commit_graph.monochrome=Černobílé
 commit_graph.color=Barva
 commit.contained_in=Tento commit je obsažen v:
@@ -1237,7 +1297,7 @@ editor.new_patch=Nová záplata
 editor.commit_message_desc=Přidat volitelný rozšířený popis…
 editor.signoff_desc=Přidat Signed-off-by podpis přispěvatele na konec zprávy o commitu.
 editor.commit_directly_to_this_branch=Odevzdat přímo do větve <strong class="branch-name">%s</strong>.
-editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a spustit požadavek na natažení.
+editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a začít pull request.
 editor.create_new_branch_np=Vytvořte <strong>novou větev</strong> z tohoto commitu.
 editor.propose_file_change=Navrhnout změnu souboru
 editor.new_branch_name=Pojmenujte novou větev pro tento commit
@@ -1254,6 +1314,7 @@ editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již není sou
 editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není součástí tohoto repozitáře.
 editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte zde</a>, abyste je zobrazili, nebo <strong>potvrďte změny ještě jednou</strong> pro jejich přepsání.
 editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři.
+editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi začal/a s úpravami. Odevzdat do záplatové větve a poté sloučit.
 editor.commit_empty_file_header=Odevzdat prázdný soubor
 editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat?
 editor.no_changes_to_show=Žádné změny k zobrazení.
@@ -1277,9 +1338,8 @@ commits.desc=Procházet historii změn zdrojového kódu.
 commits.commits=Commity
 commits.no_commits=Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii.
 commits.nothing_to_compare=Tyto větve jsou stejné.
-commits.search=Hledání commitů…
 commits.search.tooltip=Můžete předřadit klíčová slova s „author:“, „committer:“, „after:“ nebo „before:“, např. „revert author:Alice before:2019-01-03“.
-commits.find=Vyhledat
+commits.search_branch=Tato větev
 commits.search_all=Všechny větve
 commits.author=Autor
 commits.message=Zpráva
@@ -1330,7 +1390,6 @@ projects.type.basic_kanban=Základní Kanban
 projects.type.bug_triage=Třídění chyb
 projects.template.desc=Šablona projektu
 projects.template.desc_helper=Vyberte šablonu projektu pro začátek
-projects.type.uncategorized=Nezařazené
 projects.column.edit=Upravit sloupec
 projects.column.edit_title=Název
 projects.column.new_title=Název
@@ -1338,10 +1397,7 @@ projects.column.new_submit=Vytvořit sloupec
 projects.column.new=Nový sloupec
 projects.column.set_default=Nastavit jako výchozí
 projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekategorizované úkoly a požadavky na natažení
-projects.column.unset_default=Zrušit nastavení jako výchozí
-projects.column.unset_default_desc=Zrušit nastavení tohoto sloupce jako výchozí
 projects.column.delete=Smazat sloupec
-projects.column.deletion_desc=Smazání projektového sloupce přesune všechny související problémy do kategorie „Nezařazené“. Pokračovat?
 projects.column.color=Barva
 projects.open=Otevřít
 projects.close=Zavřít
@@ -1376,6 +1432,8 @@ issues.new.assignees=Zpracovatelé
 issues.new.clear_assignees=Smazat zpracovatele
 issues.new.no_assignees=Bez zpracovatelů
 issues.new.no_reviewers=Žádní posuzovatelé
+issues.new.blocked_user=Nemůžete vytvořit úkol, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
+issues.edit.blocked_user=Nemůžete upravovat obsah, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
 issues.choose.get_started=Začínáme
 issues.choose.open_external_link=Otevřít
 issues.choose.blank=Výchozí
@@ -1453,7 +1511,6 @@ issues.filter_sort.moststars=Nejvíce hvězdiček
 issues.filter_sort.feweststars=Nejméně hvězdiček
 issues.filter_sort.mostforks=Nejvíce rozštěpení
 issues.filter_sort.fewestforks=Nejméně rozštěpení
-issues.keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu.
 issues.action_open=Otevřít
 issues.action_close=Zavřít
 issues.action_label=Štítek
@@ -1491,13 +1548,14 @@ issues.close_comment_issue=Okomentovat a zavřít
 issues.reopen_issue=Znovuotevřít
 issues.reopen_comment_issue=Okomentovat a znovuotevřít
 issues.create_comment=Okomentovat
+issues.comment.blocked_user=Nemůžete vytvořit nebo upravovat komentář, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
 issues.closed_at=`uzavřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`znovuotevřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.commit_ref_at=`odkázal na tento úkol z commitu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_issue_from=`<a href="%[3]s">odkazoval/a na tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-issues.ref_pull_from=`<a href="%[3]s">odkazoval/a na tento požadavek na natažení %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-issues.ref_closing_from=`<a href="%[3]s">odkazoval/a na požadavek na natažení %[4]s, který uzavře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na požadavek na natažení %[4]s, který znovu otevře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+issues.ref_pull_from=`<a href="%[3]s">odkazoval/a na tento pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+issues.ref_closing_from=`<a href="%[3]s">odkazoval/a na pull request %[4]s, který uzavře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na pull request %[4]s, který znovu otevře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_reopened_from=`<a href="%[3]s">znovu otevřel/a tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`z %[1]s`
@@ -1509,6 +1567,7 @@ issues.role.member=Člen
 issues.role.member_helper=Tento uživatel je členem organizace vlastnící tento repositář.
 issues.role.collaborator=Spolupracovník
 issues.role.collaborator_helper=Tento uživatel byl pozván ke spolupráci v repozitáři.
+issues.role.first_time_contributor=Přispěvatel, který přispívá poprvé
 issues.role.first_time_contributor_helper=Toto je první příspěvek tohoto uživatele do repozitáře.
 issues.role.contributor=Přispěvatel
 issues.role.contributor_helper=Tento uživatel již dříve přispíval do repozitáře.
@@ -1530,7 +1589,7 @@ issues.label_archive=Archivovat štítek
 issues.label_archived_filter=Zobrazit archivované popisky
 issues.label_archive_tooltip=Archivované štítky jsou ve výchozím nastavení vyloučeny z návrhů při hledání podle popisku.
 issues.label_exclusive_desc=Pojmenujte štítek <code>rozsah/položka</code>, aby se stal vzájemně exkluzivním s jinými štítky <code>rozsah/</code>.
-issues.label_exclusive_warning=Jakékoliv protichůdné rozsahy štítků budou odstraněny při úpravě štítků u úkolů nebo u požadavku na natažení.
+issues.label_exclusive_warning=Jakékoliv protichůdné rozsahy štítků budou odstraněny při úpravě štítků u úkolů nebo u pull requestů.
 issues.label_count=%d štítků
 issues.label_open_issues=%d otevřených úkolů
 issues.label_edit=Upravit
@@ -1626,27 +1685,27 @@ issues.dependency.remove=Odstranit
 issues.dependency.remove_info=Odstranit tuto závislost
 issues.dependency.added_dependency=`přidal/a novou závislost %s`
 issues.dependency.removed_dependency=`odstranil/a závislost %s`
-issues.dependency.pr_closing_blockedby=Uzavření tohoto požadavku na natažení je blokováno následujícími úkoly
+issues.dependency.pr_closing_blockedby=Uzavření tohoto pull requestu je blokováno následujícími úkoly
 issues.dependency.issue_closing_blockedby=Uzavření tohoto úkolu je blokováno následujícími úkoly
 issues.dependency.issue_close_blocks=Tento úkol blokuje uzavření následujících úkolů
-issues.dependency.pr_close_blocks=Tento požadavek na natažení blokuje uzavření následujících úkolů
+issues.dependency.pr_close_blocks=Tento pull request blokuje uzavření následujících úkolů
 issues.dependency.issue_close_blocked=Musíte zavřít všechny úkoly, které blokují tento úkol, aby jej bylo možné zavřít.
 issues.dependency.issue_batch_close_blocked=Nelze uzavřít úkoly, které jste vybrali, protože úkol #%d má stále otevřené závislosti
-issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento požadavek na natažení, aby jej bylo možné sloučit.
+issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento pull request, aby jej bylo možné sloučit.
 issues.dependency.blocks_short=Blokuje
 issues.dependency.blocked_by_short=Závisí na
 issues.dependency.remove_header=Odstranit závislost
 issues.dependency.issue_remove_text=Tímto krokem odeberete závislost z úkolu. Pokračovat?
-issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z požadavku na natažení. Pokračovat?
-issues.dependency.setting=Povolit závislosti pro úkoly a požadavky na natažení
+issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z pull requestu. Pokračovat?
+issues.dependency.setting=Povolit závislosti pro úkoly a pull requesty
 issues.dependency.add_error_same_issue=Úkol nemůže záviset sám na sobě.
 issues.dependency.add_error_dep_issue_not_exist=Související úkol neexistuje.
 issues.dependency.add_error_dep_not_exist=Závislost neexistuje.
 issues.dependency.add_error_dep_exists=Závislost již existuje.
 issues.dependency.add_error_cannot_create_circular=Nemůžete vytvořit závislost dvou úkolů, které se vzájemně blokují.
 issues.dependency.add_error_dep_not_same_repo=Oba úkoly musí být ve stejném repozitáři.
-issues.review.self.approval=Nemůžete schválit svůj požadavek na natažení.
-issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním požadavku na natažení.
+issues.review.self.approval=Nemůžete schválit svůj pull request.
+issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním pull requestu.
 issues.review.approve=schválil tyto změny %s
 issues.review.comment=posoudil %s
 issues.review.dismissed=zamítl/a posouzení od %s %s
@@ -1686,10 +1745,11 @@ issues.reference_link=Reference: %s
 compare.compare_base=základní
 compare.compare_head=porovnat
 
-pulls.desc=Povolit požadavky na natažení a posuzování kódu.
-pulls.new=Nový požadavek na natažení
-pulls.view=Zobrazit požadavek na natažení
-pulls.compare_changes=Nový požadavek na natažení
+pulls.desc=Povolit pull requesty a posuzování kódu.
+pulls.new=Nový pull request
+pulls.new.blocked_user=Nemůžete vytvořit pull request, protože jste zablokování vlastníkem repozitáře.
+pulls.view=Zobrazit pull request
+pulls.compare_changes=Nový pull request
 pulls.allow_edits_from_maintainers=Povolit úpravy od správců
 pulls.allow_edits_from_maintainers_desc=Uživatelé s přístupem k zápisu do základní větve mohou také nahrávat do této větve
 pulls.allow_edits_from_maintainers_err=Aktualizace se nezdařila
@@ -1704,7 +1764,6 @@ pulls.compare_compare=natáhnout z
 pulls.switch_comparison_type=Přepnout typ porovnání
 pulls.switch_head_and_base=Prohodit hlavní a základní větev
 pulls.filter_branch=Filtrovat větev
-pulls.no_results=Nebyly nalezeny žádné výsledky.
 pulls.show_all_commits=Zobrazit všechny commity
 pulls.show_changes_since_your_last_review=Zobrazit změny od vašeho posledního posouzení
 pulls.showing_only_single_commit=Zobrazuji pouze změny commitu %[1]s
@@ -1712,46 +1771,46 @@ pulls.showing_specified_commit_range=Zobrazují se pouze změny mezi %[1]s..%[2]
 pulls.select_commit_hold_shift_for_range=Vyberte commit. Podržte klávesu shift + klepněte pro výběr rozsahu
 pulls.review_only_possible_for_full_diff=Posouzení je možné pouze při zobrazení plného rozlišení
 pulls.filter_changes_by_commit=Filtrovat podle commitu
-pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení.
+pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet pull request.
 pulls.nothing_to_compare_have_tag=Vybraná větev/značka je stejná.
-pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný.
-pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
-pulls.create=Vytvořit požadavek na natažení
+pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento pull request bude prázdný.
+pulls.has_pull_request=`Pull request mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
+pulls.create=Vytvořit pull request
 pulls.title_desc=chce sloučit %[1]d commity z větve <code>%[2]s</code> do <code id="branch_target">%[3]s</code>
 pulls.merged_title_desc=sloučil %[1]d commity z větve <code>%[2]s</code> do větve <code>%[3]s</code> před %[4]s
 pulls.change_target_branch_at=`změnil/a cílovou větev z <b>%s</b> na <b>%s</b> %s`
 pulls.tab_conversation=Konverzace
 pulls.tab_commits=Commity
 pulls.tab_files=Změněné soubory
-pulls.reopen_to_merge=Prosíme, otevřete znovu tento požadavek na natažení, aby se provedlo sloučení.
-pulls.cant_reopen_deleted_branch=Tento požadavek na natažení nemůže být znovu otevřen protože větev byla smazána.
+pulls.reopen_to_merge=Prosíme, otevřete znovu tento pull request, aby se provedlo sloučení.
+pulls.cant_reopen_deleted_branch=Tento pull request nemůže být znovu otevřen protože větev byla smazána.
 pulls.merged=Sloučený
-pulls.merged_success=Požadavek na natažení byl úspěšně sloučen a uzavřen
-pulls.closed=Požadavek na natažení uzavřen
+pulls.merged_success=Pull request byl úspěšně sloučen a uzavřen
+pulls.closed=Pull request uzavřen
 pulls.manually_merged=Sloučeno ručně
 pulls.merged_info_text=Větev %s může být nyní odstraněna.
-pulls.is_closed=Požadavek na natažení byl uzavřen.
-pulls.title_wip_desc=`<a href="#">Začněte název s <strong>%s</strong></a> a zamezíte tak nechtěnému sloučení požadavku na natažení.`
-pulls.cannot_merge_work_in_progress=Tento požadavek na natažení je označen jako probíhající práce.
+pulls.is_closed=Pull request byl uzavřen.
+pulls.title_wip_desc=`<a href="#">Začněte název s <strong>%s</strong></a> a zamezíte tak nechtěnému sloučení pull requestu.`
+pulls.cannot_merge_work_in_progress=Tento pull request je označen jako probíhající práce.
 pulls.still_in_progress=Stále probíhá?
 pulls.add_prefix=Přidat prefix <strong>%s</strong>
 pulls.remove_prefix=Odstranit prefix <strong>%s</strong>
-pulls.data_broken=Tento požadavek na natažení je rozbitý kvůli chybějícím informacím o rozštěpení.
-pulls.files_conflicted=Tento požadavek na natažení obsahuje změny, které kolidují s cílovou větví.
+pulls.data_broken=Tento pull request je rozbitý kvůli chybějícím informacím o rozštěpení.
+pulls.files_conflicted=Tento pull request obsahuje změny, které kolidují s cílovou větví.
 pulls.is_checking=Právě probíhá kontrola konfliktů při sloučení. Zkuste to za chvíli.
 pulls.is_ancestor=Tato větev je již součástí cílové větve. Není co sloučit.
 pulls.is_empty=Změny na této větvi jsou již na cílové větvi. Toto bude prázdný commit.
 pulls.required_status_check_failed=Některé požadované kontroly nebyly úspěšné.
 pulls.required_status_check_missing=Některé požadované kontroly chybí.
-pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento požadavek na natažení.
-pulls.blocked_by_approvals=Tento požadavek na natažení ještě nemá dostatek schválení. Uděleno %d z %d schválení.
-pulls.blocked_by_rejection=Tento požadavek na natažení obsahuje změny požadované oficiálním posuzovatelem.
-pulls.blocked_by_official_review_requests=Tento požadavek na natažení obsahuje oficiální žádosti o posouzení.
-pulls.blocked_by_outdated_branch=Tento požadavek na natažení je zablokován, protože je zastaralý.
-pulls.blocked_by_changed_protected_files_1=Tento požadavek na natažení je zablokován, protože mění chráněný soubor:
-pulls.blocked_by_changed_protected_files_n=Tento požadavek na natažení je zablokován, protože mění chráněné soubory:
-pulls.can_auto_merge_desc=Tento požadavek na natažení může být automaticky sloučen.
-pulls.cannot_auto_merge_desc=Tento požadavek na natažení nemůže být automaticky sloučen, neboť se v něm nachází konflikty.
+pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento pull request.
+pulls.blocked_by_approvals=Tento pull request ještě nemá dostatek schválení. Uděleno %d z %d schválení.
+pulls.blocked_by_rejection=Tento pull request obsahuje změny požadované oficiálním posuzovatelem.
+pulls.blocked_by_official_review_requests=Tento pull request obsahuje oficiální žádosti o posouzení.
+pulls.blocked_by_outdated_branch=Tento pull request je zablokován, protože je zastaralý.
+pulls.blocked_by_changed_protected_files_1=Tento pull request je zablokován, protože mění chráněný soubor:
+pulls.blocked_by_changed_protected_files_n=Tento pull request je zablokován, protože mění chráněné soubory:
+pulls.can_auto_merge_desc=Tento pull request může být automaticky sloučen.
+pulls.cannot_auto_merge_desc=Tento pull request nemůže být automaticky sloučen, neboť se v něm nachází konflikty.
 pulls.cannot_auto_merge_helper=Pro vyřešení konfliktů proveďte ruční sloučení.
 pulls.num_conflicting_files_1=%d konfliktní soubor
 pulls.num_conflicting_files_n=%d konfliktních souborů
@@ -1763,20 +1822,21 @@ pulls.waiting_count_1=%d čekající posouzení
 pulls.waiting_count_n=%d čekající posouzení
 pulls.wrong_commit_id=ID commitu musí být ID commitu v cílové větvi
 
-pulls.no_merge_desc=Tento požadavek na natažení nemůže být sloučen, protože všechny možnosti repozitáře na sloučení jsou zakázány.
-pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení požadavku na natažení ručně.
-pulls.no_merge_wip=Požadavek na natažení nemůže být sloučen protože je označen jako nedokončený.
-pulls.no_merge_not_ready=Tento požadavek na natažení není připraven na sloučení, zkontrolujte stav posouzení a kontrolu stavu.
-pulls.no_merge_access=Nemáte oprávnění sloučit tento požadavek na natažení.
+pulls.no_merge_desc=Tento pull request nemůže být sloučen, protože všechny možnosti repozitáře na sloučení jsou zakázány.
+pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení pull requestu ručně.
+pulls.no_merge_wip=Pull request nemůže být sloučen protože je označen jako nedokončený.
+pulls.no_merge_not_ready=Tento pull request není připraven na sloučení, zkontrolujte stav posouzení a kontrolu stavu.
+pulls.no_merge_access=Nemáte oprávnění sloučit tento pull request.
 pulls.merge_pull_request=Vytvořit slučovací commit
 pulls.rebase_merge_pull_request=Rebase pak fast-forward
 pulls.rebase_merge_commit_pull_request=Rebase a poté vytvořit slučovací commit
 pulls.squash_merge_pull_request=Vytvořit squash commit
+pulls.fast_forward_only_merge_pull_request=Pouze fast-forward
 pulls.merge_manually=Sloučeno ručně
 pulls.merge_commit_id=ID slučovacího commitu
 pulls.require_signed_wont_sign=Větev vyžaduje podepsané commity, ale toto sloučení nebude podepsáno
 
-pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento požadavek na natažení.
+pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento pull request.
 pulls.merge_conflict=Sloučení selhalo: Došlo ke konfliktu při sloučení. Tip: Zkuste jinou strategii
 pulls.merge_conflict_summary=Chybové hlášení
 pulls.rebase_conflict=Sloučení selhalo: Došlo ke konfliktu při rebase commitu: %[1]s. Tip: Zkuste jinou strategii
@@ -1784,11 +1844,11 @@ pulls.rebase_conflict_summary=Chybové hlášení
 pulls.unrelated_histories=Sloučení selhalo: Hlavní a základní revize nesdílí společnou historii. Tip: Zkuste jinou strategii
 pulls.merge_out_of_date=Sloučení selhalo: Základ byl aktualizován při generování sloučení. Tip: Zkuste to znovu.
 pulls.head_out_of_date=Sloučení selhalo: Hlavní revize byla aktualizován při generování sloučení. Tip: Zkuste to znovu.
-pulls.has_merged=Chyba: Požadavek na natažení byl sloučen, nelze znovu sloučit nebo změnit cílovou větev.
+pulls.has_merged=Chyba: Pull request byl sloučen, nelze znovu sloučit nebo změnit cílovou větev.
 pulls.push_rejected=Sloučení selhalo: Nahrání bylo zamítnuto. Zkontrolujte háčky Gitu pro tento repozitář.
 pulls.push_rejected_summary=Úplná zpráva o odmítnutí
 pulls.push_rejected_no_message=Sloučení se nezdařilo: Nahrání bylo odmítnuto, ale nebyla nalezena žádná vzdálená zpráva.<br>Zkontrolujte háčky gitu pro tento repozitář
-pulls.open_unmerged_pull_exists=`Nemůžete provést operaci znovuotevření protože je tu čekající požadavek na natažení (#%d) s identickými vlastnostmi.`
+pulls.open_unmerged_pull_exists=`Nemůžete provést operaci znovuotevření protože je tu čekající pull request (#%d) s identickými vlastnostmi.`
 pulls.status_checking=Některé kontroly jsou nedořešeny
 pulls.status_checks_success=Všechny kontroly byly úspěšné
 pulls.status_checks_warning=Některé kontroly nahlásily varování
@@ -1803,30 +1863,32 @@ pulls.update_branch_rebase=Aktualizovat větev pomocí rebase
 pulls.update_branch_success=Aktualizace větve byla úspěšná
 pulls.update_not_allowed=Nemáte oprávnění aktualizovat větev
 pulls.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi
-pulls.close=Zavřít požadavek na natažení
-pulls.closed_at=`uzavřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-pulls.reopened_at=`znovuotevřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.close=Zavřít pull request
+pulls.closed_at=`uzavřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.reopened_at=`znovuotevřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.cmd_instruction_hint=`Zobrazit <a class="show-instruction">instrukce příkazové řádky</a>.`
 pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny.
 pulls.cmd_instruction_merge_title=Sloučit
 pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea.
 pulls.clear_merge_message=Vymazat zprávu o sloučení
+pulls.clear_merge_message_hint=Vymazání zprávy o sloučení odstraní pouze obsah zprávy a ponechá generované přídavky gitu jako "Co-AuthoreBy …".
 
 pulls.auto_merge_button_when_succeed=(Když kontroly uspějí)
 pulls.auto_merge_when_succeed=Automaticky sloučit, když všechny kontroly uspějí
-pulls.auto_merge_newly_scheduled=Požadavek na natažení byl naplánován na sloučení, jakmile všechny kontroly uspějí.
-pulls.auto_merge_has_pending_schedule=%[1]s naplánoval/a tento požadavek na natažení pro automatické sloučení, když všechny kontroly uspějí v %[2]s.
+pulls.auto_merge_newly_scheduled=Pull request byl naplánován na sloučení, jakmile všechny kontroly uspějí.
+pulls.auto_merge_has_pending_schedule=%[1]s naplánoval/a tento pull request pro automatické sloučení, když všechny kontroly uspějí v %[2]s.
 
 pulls.auto_merge_cancel_schedule=Zrušit automatické sloučení
-pulls.auto_merge_not_scheduled=Tento požadavek na natažení není naplánován na automatické sloučení.
-pulls.auto_merge_canceled_schedule=Automatické sloučení bylo zrušeno pro tento požadavek na natažení.
+pulls.auto_merge_not_scheduled=Tento pull request není naplánován na automatické sloučení.
+pulls.auto_merge_canceled_schedule=Automatické sloučení bylo zrušeno pro tento pull request.
 
-pulls.auto_merge_newly_scheduled_comment=`požadavek na automatické sloučení tohoto požadavku na natažení je naplánován, když všechny kontroly uspějí %[1]s`
-pulls.auto_merge_canceled_schedule_comment=`zrušil/a automatické sloučení tohoto požadavku na natažení, když všechny kontroly uspějí %[1]s`
+pulls.auto_merge_newly_scheduled_comment=`požadavek na automatické sloučení tohoto pull requestu je naplánován, když všechny kontroly uspějí %[1]s`
+pulls.auto_merge_canceled_schedule_comment=`zrušil/a automatické sloučení tohoto pull requestu, když všechny kontroly uspějí %[1]s`
 
-pulls.delete.title=Odstranit tento požadavek na natažení?
-pulls.delete.text=Opravdu chcete tento požadavek na natažení smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.)
+pulls.delete.title=Odstranit tento pull request?
+pulls.delete.text=Opravdu chcete tento pull request smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.)
 
+pulls.recently_pushed_new_branches=Nahráli jste větev <strong>%[1]s</strong> %[2]s
 
 pull.deleted_branch=(odstraněno):%s
 
@@ -1871,7 +1933,7 @@ signing.wont_sign.parentsigned=Commit nebude podepsán, protože nadřazený com
 signing.wont_sign.basesigned=Sloučení nebude podepsáno, protože základní commit není podepsaný.
 signing.wont_sign.headsigned=Sloučení nebude podepsáno, protože hlavní revize není podepsána.
 signing.wont_sign.commitssigned=Sloučení nebude podepsáno, protože všechny přidružené revize nejsou podepsány.
-signing.wont_sign.approved=Sloučení nebude podepsáno, protože požadavek na natažení není schválen.
+signing.wont_sign.approved=Sloučení nebude podepsáno, protože pull request není schválen.
 signing.wont_sign.not_signed_in=Nejste přihlášeni.
 
 ext_wiki=Přístup k externí Wiki
@@ -1905,6 +1967,9 @@ wiki.page_name_desc=Zadejte název této Wiki stránky. Některé speciální n
 wiki.original_git_entry_tooltip=Zobrazit originální Git soubor namísto použití přátelského odkazu.
 
 activity=Aktivita
+activity.navbar.code_frequency=Frekvence kódu
+activity.navbar.contributors=Přispěvatelé
+activity.navbar.recent_commits=Nedávné commity
 activity.period.filter_label=Období:
 activity.period.daily=1 den
 activity.period.halfweekly=3 dny
@@ -1914,16 +1979,16 @@ activity.period.quarterly=3 měsíce
 activity.period.semiyearly=6 měsíců
 activity.period.yearly=1 rok
 activity.overview=Přehled
-activity.active_prs_count_1=<strong>%d</strong> aktivní požadavek na natažení
-activity.active_prs_count_n=<strong>%d</strong> aktivní požadavky na natažení
-activity.merged_prs_count_1=Sloučený požadavek na natažení
-activity.merged_prs_count_n=Sloučené požadavky na natažení
-activity.opened_prs_count_1=Navrhovaný požadavek na natažení
-activity.opened_prs_count_n=Navrhované požadavky na natažení
+activity.active_prs_count_1=<strong>%d</strong> aktivní pull request
+activity.active_prs_count_n=<strong>%d</strong> aktivních pull requestů
+activity.merged_prs_count_1=Sloučený pull request
+activity.merged_prs_count_n=Sloučené pull requesty
+activity.opened_prs_count_1=Navrhovaný pull request
+activity.opened_prs_count_n=Navrhované pull requesty
 activity.title.user_1=%d uživatel
 activity.title.user_n=%d uživatelů
-activity.title.prs_1=%d Požadavek na natažení
-activity.title.prs_n=%d Požadavků na natažení
+activity.title.prs_1=%d pull request
+activity.title.prs_n=%d pull requestů
 activity.title.prs_merged_by=%s sloučil %s
 activity.title.prs_opened_by=%s navrhl %s
 activity.merged_prs_label=Sloučený
@@ -1942,7 +2007,7 @@ activity.new_issues_count_n=Nové úkoly
 activity.new_issue_label=Otevřený
 activity.title.unresolved_conv_1=%d nevyřešená konverzace
 activity.title.unresolved_conv_n=%d nevyřešených konverzací
-activity.unresolved_conv_desc=Tyto nedávno změněné úkolu a požadavky na natažení ještě nebyly vyřešeny.
+activity.unresolved_conv_desc=Tyto nedávno změněné úkolu a pull requestu ještě nebyly vyřešeny.
 activity.unresolved_conv_label=Otevřít
 activity.title.releases_1=%d Vydání
 activity.title.releases_n=%d Vydání
@@ -1972,17 +2037,8 @@ activity.git_stats_deletion_n=%d odebrání
 
 contributors.contribution_type.filter_label=Typ příspěvku:
 contributors.contribution_type.commits=Commity
-
-search=Vyhledat
-search.search_repo=Hledat repozitář
-search.type.tooltip=Druh vyhledávání
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
-search.match=Shoda
-search.match.tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
-search.results=Výsledky hledání „%s“ v <a href="%s">%s</a>
-search.code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu.
-search.code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu.
+contributors.contribution_type.additions=Přidání
+contributors.contribution_type.deletions=Odstranění
 
 settings=Nastavení
 settings.desc=Nastavení je místo, kde můžete měnit nastavení repozitáře
@@ -2000,10 +2056,13 @@ settings.mirror_settings=Nastavení zrcadla
 settings.mirror_settings.docs=Nastavte repozitář pro automatickou synchronizaci commitů, značek a větví s jiným repozitářem.
 settings.mirror_settings.docs.disabled_pull_mirror.instructions=Nastavte váš projekt pro automatické nahrávání commitů, značek a větví do jiného repozitáře. Správce webu zakázal zrcadla pro natažení.
 settings.mirror_settings.docs.disabled_push_mirror.instructions=Nastavte svůj projekt pro automatické natažení commitů, značek a větví z jiného repozitáře.
+settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=Právě teď to lze provést pouze v menu "Nová migrace". Pro více informací prosím konzultujte:
+settings.mirror_settings.docs.disabled_push_mirror.info=Push zrcadla byla zakázána administrátorem vašeho webu.
 settings.mirror_settings.docs.no_new_mirrors=Váš repozitář zrcadlí změny do nebo z jiného repozitáře. Mějte prosím na paměti, že v tuto chvíli nemůžete vytvořit žádná nová zrcadla.
 settings.mirror_settings.docs.can_still_use=I když nemůžete upravit stávající zrcadla nebo vytvořit nová, stále můžete použít své stávající zrcadlo.
 settings.mirror_settings.docs.more_information_if_disabled=Více informací o zrcadlech pro nahrání a natažení naleznete zde:
 settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře?
+settings.mirror_settings.docs.doc_link_pull_section=sekci "stahovat ze vzdáleného úložiště" v dokumentaci.
 settings.mirror_settings.docs.pulling_remote_title=Stažení ze vzdáleného úložiště
 settings.mirror_settings.mirrored_repository=Zrcadlený repozitář
 settings.mirror_settings.direction=Směr
@@ -2016,6 +2075,8 @@ settings.mirror_settings.push_mirror.add=Přidat zrcadlo pro nahrání
 settings.mirror_settings.push_mirror.edit_sync_time=Upravit interval synchronizace zrcadla
 
 settings.sync_mirror=Synchronizovat nyní
+settings.pull_mirror_sync_in_progress=V tuto chvíli probíhá nahrávání změn ze vzdáleného %s.
+settings.push_mirror_sync_in_progress=Probíhá nahrávání změn do vzdáleného %s.
 settings.site=Webová stránka
 settings.update_settings=Aktualizovat nastavení
 settings.update_mirror_settings=Aktualizovat nastavení zrcadla
@@ -2025,6 +2086,8 @@ settings.branches.add_new_rule=Přidat nové pravidlo
 settings.advanced_settings=Pokročilá nastavení
 settings.wiki_desc=Povolit Wiki repozitáře
 settings.use_internal_wiki=Používat vestavěnou Wiki
+settings.default_wiki_branch_name=Výchozí název větve Wiki
+settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila.
 settings.use_external_wiki=Používat externí Wiki
 settings.external_wiki_url=URL externí Wiki
 settings.external_wiki_url_error=URL externí wiki platné URL.
@@ -2046,15 +2109,19 @@ settings.tracker_issue_style.regexp_pattern_desc=První zachycená skupina bude
 settings.tracker_url_format_desc=Použijte zástupné symboly <code>{user}</code>, <code>{repo}</code> a <code>{index}</code> pro uživatelské jméno, jméno repozitáře a číslo úkolu.
 settings.enable_timetracker=Povolit sledování času
 settings.allow_only_contributors_to_track_time=Povolit sledování času pouze přispěvatelům
-settings.pulls_desc=Povolit požadavky na natažení
+settings.pulls_desc=Povolit pull requesty repozitáře
 settings.pulls.ignore_whitespace=Ignorovat bílé znaky při konfliktech
 settings.pulls.enable_autodetect_manual_merge=Povolit autodetekci ručních sloučení (Poznámka: V některých zvláštních případech může dojít k nesprávnému rozhodnutí)
-settings.pulls.allow_rebase_update=Povolit aktualizaci větve požadavku na natažení pomocí rebase
-settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat větev požadavku na natažení po jeho sloučení
+settings.pulls.allow_rebase_update=Povolit aktualizaci větve pull requestu pomocí rebase
+settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat větev pull requestu po jeho sloučení
 settings.pulls.default_allow_edits_from_maintainers=Ve výchozím nastavení povolit úpravy od správců
 settings.releases_desc=Povolit vydání v repozitáři
 settings.packages_desc=Povolit registr balíčků repozitáře
 settings.projects_desc=Povolit projekty v repozitáři
+settings.projects_mode_desc=Režim projektů (druhy projektů k zobrazení)
+settings.projects_mode_repo=Pouze projekty repozitáře
+settings.projects_mode_owner=Pouze projekty uživatele nebo organizace
+settings.projects_mode_all=Všechny projekty
 settings.actions_desc=Povolit akce repozitáře
 settings.admin_settings=Nastavení správce
 settings.admin_enable_health_check=Povolit kontrolu stavu repozitáře (git fsck)
@@ -2080,6 +2147,7 @@ settings.convert_fork_succeed=Rozštěpení bylo překonvertován na běžný re
 settings.transfer=Předat vlastnictví
 settings.transfer.rejected=Převod repozitáře byl zamítnut.
 settings.transfer.success=Převod repozitáře byl úspěšný.
+settings.transfer.blocked_user=Nelze převést repozitář, protože jste blokování novým vlastníkem.
 settings.transfer_abort=Zrušit převod
 settings.transfer_abort_invalid=Nemůžete zrušit neexistující převod repozitáře.
 settings.transfer_abort_success=Převod repozitáře do %s byl úspěšně zrušen.
@@ -2125,11 +2193,11 @@ settings.add_collaborator_success=Spolupracovník byl přidán.
 settings.add_collaborator_inactive_user=Nelze přidat neaktivního uživatele jako spolupracovníka.
 settings.add_collaborator_owner=Vlastníka nelze přidat jako spolupracovníka.
 settings.add_collaborator_duplicate=Spolupracovník je již přidán k tomuto repozitáři.
+settings.add_collaborator.blocked_user=Spolupracovník je zablokován vlastníkem repozitáře nebo naopak.
 settings.delete_collaborator=Odstranit
 settings.collaborator_deletion=Odstranit spolupracovníka
 settings.collaborator_deletion_desc=Odstranění spolupracovníka zruší jeho přístup do tohoto repozitáře. Pokračovat?
 settings.remove_collaborator_success=Spolupracovník byl smazán.
-settings.search_user_placeholder=Hledat uživatele…
 settings.org_not_allowed_to_be_collaborator=Organizace nemůže být přidána jako spolupracovník.
 settings.change_team_access_not_allowed=Změna přístupu týmu k repozitáře se omezuje na vlastníka organizace
 settings.team_not_in_organization=Tým není ve stejné organizaci jako repozitář
@@ -2137,7 +2205,6 @@ settings.teams=Týmy
 settings.add_team=Přidat tým
 settings.add_team_duplicate=Tým již má repozitář
 settings.add_team_success=Tým má nyní přístup k repozitáři.
-settings.search_team=Vyhledat tým…
 settings.change_team_permission_tip=Oprávnění týmu je nastaveno na stránce nastavení týmu a nelze je změnit pro každý repozitář
 settings.delete_team_tip=Tento tým má přístup ke všem repositářům a nemůže být odstraněn
 settings.remove_team_success=Přístup týmu k repozitáři byl odstraněn.
@@ -2203,22 +2270,25 @@ settings.event_issue_milestone=Úkolu přidán milník
 settings.event_issue_milestone_desc=Úkolu přidán nebo odebrán milník.
 settings.event_issue_comment=Komentář k úkolu
 settings.event_issue_comment_desc=Komentář úkolu přidán, upraven nebo smazán.
-settings.event_header_pull_request=Události požadavku na natažení
-settings.event_pull_request=Požadavek na stažení
-settings.event_pull_request_desc=Požadavek na natažení otevřen, uzavřen, znovu otevřen nebo upraven.
-settings.event_pull_request_assign=Požadavek na natažení přiřazen
-settings.event_pull_request_assign_desc=Požadavek na natažení přiřazen nebo nepřiřazen.
-settings.event_pull_request_label=Požadavek na natažení oštítkován
-settings.event_pull_request_label_desc=Štítky požadavku na natažení aktualizovány nebo vymazány.
-settings.event_pull_request_milestone=Požadavku na natažení přidán milník
-settings.event_pull_request_milestone_desc=Požadavku na natažení přidán nebo odebrán milník.
-settings.event_pull_request_comment=Požadavek na natažení okomentován
-settings.event_pull_request_comment_desc=Komentář požadavku na natažení vytvořen, upraven nebo odstraněn.
-settings.event_pull_request_review=Požadavek na natažení přezkoumán
-settings.event_pull_request_review_desc=Požadavek na natažení schválen, odmítnut nebo zkontrolován.
-settings.event_pull_request_sync=Požadavek na natažení synchronizován
-settings.event_pull_request_sync_desc=Požadavek na natažení synchronizován.
-settings.event_pull_request_review_request=Vyžádán požadavek na natažení
+settings.event_header_pull_request=Události pull requestu
+settings.event_pull_request=Pull request
+settings.event_pull_request_desc=Pull request otevřen, uzavřen, znovu otevřen nebo upraven.
+settings.event_pull_request_assign=Pull request přiřazen
+settings.event_pull_request_assign_desc=Pull request přiřazen nebo nepřiřazen.
+settings.event_pull_request_label=Pull request oštítkován
+settings.event_pull_request_label_desc=Štítky pull requestu aktualizovány nebo vymazány.
+settings.event_pull_request_milestone=Přidán milník pull requestu
+settings.event_pull_request_milestone_desc=Přidán nebo odebrán milník pull requestu.
+settings.event_pull_request_comment=Pull request okomentován
+settings.event_pull_request_comment_desc=Komentář pull requestu vytvořen, upraven nebo odstraněn.
+settings.event_pull_request_review=Pull request posouzen
+settings.event_pull_request_review_desc=Pull request schválen, odmítnut nebo zkontrolován.
+settings.event_pull_request_sync=Pull request synchronizován
+settings.event_pull_request_sync_desc=Pull request synchronizován.
+settings.event_pull_request_review_request=Požádáno o posouzení pull requestu
+settings.event_pull_request_review_request_desc=Přidána nebo ostraněna žádnost o kontrolu pull requestu.
+settings.event_pull_request_approvals=Schválení pull requestu
+settings.event_pull_request_merge=Sloučení pull requestu
 settings.event_package=Balíček
 settings.event_package_desc=Balíček vytvořen nebo odstraněn v repozitáři.
 settings.branch_filter=Filtr větví
@@ -2282,32 +2352,34 @@ settings.protect_disable_push_desc=Žádné nahrávání do této větve nebude
 settings.protect_enable_push=Povolit nahrávání
 settings.protect_enable_push_desc=Každý, kdo má přístup k zápisu, bude moci nahrávat do této větve (ale ne vynucená nahrávání).
 settings.protect_enable_merge=Povolit sloučení
+settings.protect_enable_merge_desc=Každému, kdo má přístup k zápisu, bude povoleno sloučit pull requesty do této větve.
 settings.protect_whitelist_committers=Povolit nahrání jen vyjmenovaným
 settings.protect_whitelist_committers_desc=Pouze povolení uživatelé budou moci nahrávat do této větve (ale ne vynucení nahrávání).
 settings.protect_whitelist_deploy_keys=Povolit nahrání klíčům pro nasazení s přístupem pro zápis.
 settings.protect_whitelist_users=Povolení uživatelé pro nahrávání:
-settings.protect_whitelist_search_users=Hledat uživatele…
 settings.protect_whitelist_teams=Povolené týmy pro nahrávání:
-settings.protect_whitelist_search_teams=Vyhledat týmy…
 settings.protect_merge_whitelist_committers=Povolit vyjmenovaným slučování
-settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným uživatelům nebo týmům slučovat požadavky na natažení do této větve.
+settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným uživatelům nebo týmům slučovat pull requesty do této větve.
 settings.protect_merge_whitelist_users=Povolení uživatelé pro slučování:
 settings.protect_merge_whitelist_teams=Povolené týmy pro slučování:
 settings.protect_check_status_contexts=Povolit kontrolu stavu
 settings.protect_status_check_patterns=Vzorce kontroly stavu:
+settings.protect_status_check_patterns_desc=Zadejte vzory pro určení, které kontroly stavu musí projít před sloučením větví do větve, která odpovídá tomuto pravidlu. Každý řádek určuje vzor. Vzory nemohou být prázdné.
 settings.protect_check_status_contexts_desc=Požadovat kontrolu stavu před sloučením. Vyberte, jaké kontroly stavu musí projít před tím, než je možné větev sloučit do větve, která vyhovuje tomuto pravidlu. Pokud je povoleno, revize musí být nejprve nahrány do jiné větve, projít kontrolou stavu, a následné sloučeny nebo přímo nahrány do větve, která vyhovuje tomuto pravidlu. Pokud nejsou vybrány žádné kontexty, musí být poslední potvrzení úspěšné bez ohledu na kontext.
 settings.protect_check_status_contexts_list=Kontroly stavu pro tento repozitář zjištěné během posledního týdne
 settings.protect_status_check_matched=Odpovídá
 settings.protect_invalid_status_check_pattern=Neplatný vzor kontroly stavu: „%s“.
 settings.protect_no_valid_status_check_patterns=Žádné platné vzory kontroly stavu.
 settings.protect_required_approvals=Požadovaná schválení:
-settings.protect_required_approvals_desc=Umožnit sloučení pouze požadavkům na natažení s dostatečným pozitivním hodnocením.
+settings.protect_required_approvals_desc=Umožnit sloučení pouze pull requestů s dostatečným pozitivním hodnocením.
 settings.protect_approvals_whitelist_enabled=Omezit schválení na povolené uživatele nebo týmy
 settings.protect_approvals_whitelist_enabled_desc=Do požadovaných schválení se započítají pouze posouzení od povolených uživatelů nebo týmů. Bez seznamu povolených se započítává schválení od kohokoli s právem zápisu.
 settings.protect_approvals_whitelist_users=Povolení posuzovatelé:
 settings.protect_approvals_whitelist_teams=Povolené týmy pro posuzování:
 settings.dismiss_stale_approvals=Odmítnout nekvalitní schválení
-settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto požadavku na natažení, všechna stará schválení budou zamítnuta.
+settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto pull requestu, všechna stará schválení budou zamítnuta.
+settings.ignore_stale_approvals=Ignorovat zastaralá schválení
+settings.ignore_stale_approvals_desc=Nezapočítávejte schválení, která byla provedena u starších commitů (zastaralých recenzí), do počtu schválení, která má PR. Pokud jsou zastaralá hodnocení již zamítnuta, je to irelevantní.
 settings.require_signed_commits=Vyžadovat podepsané revize
 settings.require_signed_commits_desc=Odmítnout nahrání do této větve pokud nejsou podepsaná nebo jsou neověřitelná.
 settings.protect_branch_name_pattern=Vzor jména chráněných větví
@@ -2328,9 +2400,9 @@ settings.block_rejected_reviews=Blokovat sloučení při zamítavých posouzení
 settings.block_rejected_reviews_desc=Slučování nebude možné, pokud o změny požádají oficiální posuzovatelé, i když je k dispozici dostatek schválení.
 settings.block_on_official_review_requests=Blokovat sloučení při oficiální žádosti o posouzení
 settings.block_on_official_review_requests_desc=Slučování nebude možné, pokud mají oficiální požadavek na posouzení, i když mají k dispozici dostatek schválení.
-settings.block_outdated_branch=Blokovat sloučení, pokud je požadavek na natažení zastaralý
+settings.block_outdated_branch=Blokovat sloučení, pokud je pull request zastaralý
 settings.block_outdated_branch_desc=Slučování nebude možné, pokud je hlavní větev za základní větví.
-settings.default_branch_desc=Vybrat výchozí větev repozitáře pro požadavky na natažení a revize kódu:
+settings.default_branch_desc=Vybrat výchozí větev repozitáře pro pull requesty a revize kódu:
 settings.merge_style_desc=Sloučit styly
 settings.default_merge_style_desc=Výchozí styl sloučení pro požadavky na natažení:
 settings.choose_branch=Vyberte větev…
@@ -2357,15 +2429,16 @@ settings.matrix.room_id=ID místnosti
 settings.matrix.message_type=Typ zprávy
 settings.archive.button=Archivovat repozitář
 settings.archive.header=Archivovat tento repozitář
-settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo žádosti o natažení.
+settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo pull requesty.
 settings.archive.success=Repozitář byl úspěšně archivován.
 settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů.
 settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář.
 settings.archive.branchsettings_unavailable=Nastavení větví není dostupné, pokud je repozitář archivovaný.
 settings.archive.tagsettings_unavailable=Nastavení značek není k dispozici, pokud je repozitář archivován.
+settings.archive.mirrors_unavailable=Zrcadla nejsou k dispozici, pokud je repozitář archivován.
 settings.unarchive.button=Obnovit repozitář
 settings.unarchive.header=Obnovit tento repozitář
-settings.unarchive.text=Obnovení repozitáře vrátí možnost přijímání commitů a nahrávání. Stejně tak se obnoví i možnost zadávání nových úkolů a požadavků na natažení.
+settings.unarchive.text=Obnovení repozitáře vrátí možnost přijímání commitů a nahrávání. Stejně tak se obnoví i možnost zadávání nových úkolů a pull requestů.
 settings.unarchive.success=Repozitář byl úspěšně obnoven.
 settings.unarchive.error=Nastala chyba při obnovování repozitáře. Prohlédněte si záznam pro více detailů.
 settings.update_avatar_success=Avatar repozitáře byl aktualizován.
@@ -2446,9 +2519,9 @@ diff.review.header=Odeslat posouzení
 diff.review.placeholder=Posoudit komentář
 diff.review.comment=Okomentovat
 diff.review.approve=Schválit
-diff.review.self_reject=Autoři požadavků na natažení nemohou požadovat změny na svém vlastním požadavku na natažení
+diff.review.self_reject=Autoři pull requestu nemohou požadovat změny na svém vlastním pull requestu
 diff.review.reject=Požadovat změny
-diff.review.self_approve=Autoři požadavku na natažení nemohou schválit svůj vlastní požadavek na natažení
+diff.review.self_approve=Autoři pull requestu nemohou schválit svůj vlastní pull request
 diff.committed_by=odevzdal
 diff.protected=Chráněno
 diff.image.side_by_side=Vedle sebe
@@ -2529,7 +2602,6 @@ branch.default_deletion_failed=Větev „%s“ je výchozí větev. Nelze ji ods
 branch.restore=Obnovit větev „%s“
 branch.download=Stáhnout větev „%s“
 branch.rename=Přejmenovat větev „%s“
-branch.search=Hledat větev
 branch.included_desc=Tato větev je součástí výchozí větve
 branch.included=Zahrnuje
 branch.create_new_branch=Vytvořit větev z větve:
@@ -2560,13 +2632,16 @@ find_file.no_matching=Nebyl nalezen žádný odpovídající soubor
 error.csv.too_large=Tento soubor nelze vykreslit, protože je příliš velký.
 error.csv.unexpected=Tento soubor nelze vykreslit, protože obsahuje neočekávaný znak na řádku %d ve sloupci %d.
 error.csv.invalid_field_count=Soubor nelze vykreslit, protože má nesprávný počet polí na řádku %d.
+error.broken_git_hook=Git háčky tohoto repozitáře se zdají být rozbité. Postupujte prosím podle <a target="_blank" rel="noreferrer" href="%s">dokumentace</a>, abyste je opravili, a poté nahrajte nějaké commity pro obnovení stavu.
 
 [graphs]
 component_loading=Načítání %s...
 component_loading_failed=Nelze načíst %s
 component_loading_info=Může to chvíli trvat…
 component_failed_to_load=Došlo k neočekávané chybě.
+code_frequency.what=frekvence kódu
 contributors.what=příspěvky
+recent_commits.what=nedávné commity
 
 [org]
 org_name_holder=Název organizace
@@ -2672,7 +2747,6 @@ teams.write_permission_desc=Členství v tom týmu poskytuje právo <strong>záp
 teams.admin_permission_desc=Členství v tom týmu poskytuje právo <strong>správce</strong>: členové mohou číst z, nahrávat do a přidávat spolupracovníky do repozitářů týmu.
 teams.create_repo_permission_desc=Navíc tento tým uděluje oprávnění <strong>vytvořit repozitář</strong>: členové mohou vytvářet nové repozitáře v organizaci.
 teams.repositories=Repozitáře týmu
-teams.search_repo_placeholder=Hledat repozitář…
 teams.remove_all_repos_title=Odstranit všechny repozitáře týmu
 teams.remove_all_repos_desc=Tímto odeberete všechny repozitáře z týmu.
 teams.add_all_repos_title=Přidat všechny repozitáře
@@ -2681,6 +2755,7 @@ teams.add_nonexistent_repo=Repositář, který se snažíte přidat, neexistuje.
 teams.add_duplicate_users=Uživatel je již členem týmu.
 teams.repos.none=Tento tým nemůže přistoupit k žádným repozitářům.
 teams.members.none=Žádní členové v tomto týmu.
+teams.members.blocked_user=Nelze přidat uživatele, protože je zablokován organizací.
 teams.specific_repositories=Konkrétní repozitáře
 teams.specific_repositories_helper=Členové budou mít přístup pouze do repozitářů výslovně přidaných do týmu. Výběrem tohoto <strong>nebudou</strong> automaticky odstraněny již přidané repozitáře pomocí <i>Všechny repozitáře</i>.
 teams.all_repositories=Všechny repozitáře
@@ -2694,6 +2769,7 @@ teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže.
 
 [admin]
 dashboard=Přehled
+self_check=Samokontrola
 identity_access=Identita a přístup
 users=Uživatelské účty
 organizations=Organizace
@@ -2703,6 +2779,8 @@ integrations=Integrace
 authentication=Zdroje ověření
 emails=Uživatelské e-maily
 config=Nastavení
+config_summary=Souhrn
+config_settings=Nastavení
 notices=Systémová oznámení
 monitor=Sledování
 first_page=První
@@ -2737,6 +2815,7 @@ dashboard.delete_repo_archives.started=Spuštěna úloha smazání všech archiv
 dashboard.delete_missing_repos=Smazat všechny repozitáře, které nemají Git soubory
 dashboard.delete_missing_repos.started=Spuštěna úloha mazání všech repozitářů, které nemají Git soubory.
 dashboard.delete_generated_repository_avatars=Odstranit vygenerované avatary repozitářů
+dashboard.sync_repo_branches=Synchronizovat chybějící větve z git dat do databází
 dashboard.sync_repo_tags=Synchronizovat značky z git dat do databáze
 dashboard.update_mirrors=Aktualizovat zrcadla
 dashboard.repo_health_check=Kontrola stavu všech repozitářů
@@ -2752,6 +2831,7 @@ dashboard.reinit_missing_repos=Znovu inicializovat všechny chybějící repozit
 dashboard.sync_external_users=Synchronizovat externí uživatelská data
 dashboard.cleanup_hook_task_table=Vyčistit tabulku hook_task
 dashboard.cleanup_packages=Vyčistit prošlé balíčky
+dashboard.cleanup_actions=Vyčištění prošlých záznamů a artefaktů z akcí
 dashboard.server_uptime=Doba provozu serveru
 dashboard.current_goroutine=Aktuální Goroutines
 dashboard.current_memory_usage=Aktuální využití paměti
@@ -2877,9 +2957,6 @@ repos.unadopted.no_more=Nebyly nalezeny žádné další nepřijaté repositář
 repos.owner=Vlastník
 repos.name=Název
 repos.private=Soukromý
-repos.watches=Sledovače
-repos.stars=Oblíbení
-repos.forks=Rozštěpení
 repos.issues=Úkoly
 repos.size=Velikost
 repos.lfs_size=Velikost LFS
@@ -2888,6 +2965,7 @@ packages.package_manage_panel=Správa balíčků
 packages.total_size=Celková velikost: %s
 packages.unreferenced_size=Neodkazovaná velikost: %s
 packages.cleanup=Vyčistit prošlá data
+packages.cleanup.success=Úspěšné vyčištění dat, jejichž platnost vypršela
 packages.owner=Vlastník
 packages.creator=Tvůrce
 packages.name=Název
@@ -2898,10 +2976,12 @@ packages.size=Velikost
 packages.published=Publikováno
 
 defaulthooks=Výchozí webové háčky
+defaulthooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde jsou výchozí a budou zkopírovány do všech nových repozitářů. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">průvodci webovými háčky</a>.
 defaulthooks.add_webhook=Přidat výchozí webový háček
 defaulthooks.update_webhook=Aktualizovat výchozí webový háček
 
 systemhooks=Systémové webové háčky
+systemhooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde budou vykonány na všech repozitářích systému, proto prosím zvažte jakékoli důsledky, které to může mít na výkon. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">průvodci webovými háčky</a>.
 systemhooks.add_webhook=Přidat systémový webový háček
 systemhooks.update_webhook=Aktualizovat systémový webový háček
 
@@ -2979,6 +3059,7 @@ auths.oauth2_required_claim_value_helper=Nastavte tuto hodnotu pro omezení při
 auths.oauth2_group_claim_name=Název tvrzení poskytující názvy skupin pro tento zdroj. (nepovinné)
 auths.oauth2_admin_group=Hodnota tvrzení pro skupinu uživatelů administrátorů. (Volitelné - vyžaduje název tvrzení výše)
 auths.oauth2_restricted_group=Hodnota tvrzení pro skupinu omezených uživatelů. (Volitelné - vyžaduje název tvrzení výše)
+auths.oauth2_map_group_to_team=Mapa uváděných skupin do organizačních týmů. (Volitelné - vyžaduje výše uvedené jméno)
 auths.oauth2_map_group_to_team_removal=Odebrat uživatele z synchronizovaných týmů, pokud uživatel nepatří do odpovídající skupiny.
 auths.enable_auto_register=Povolit zaregistrování se
 auths.sspi_auto_create_users=Automaticky vytvářet uživatele
@@ -3000,7 +3081,7 @@ auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci po
 auths.tip.dropbox=Vytvořte novou aplikaci na https://www.dropbox.com/developers/apps
 auths.tip.facebook=Registrujte novou aplikaci na https://developers.facebook.com/apps a přidejte produkt „Facebook Login“
 auths.tip.github=Registrujte novou OAuth aplikaci na https://github.com/settings/applications/new
-auths.tip.gitlab=Registrujte novou aplikaci na https://gitlab.com/profile/applications
+auths.tip.gitlab_new=Zaregistrujte novou aplikaci na https://gitlab.com/-/profile/applications
 auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzole na https://console.developers.google.com/
 auths.tip.openid_connect=Použijte OpenID URL pro objevování spojení (<server>/.well-known/openid-configuration) k nastavení koncových bodů
 auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená
@@ -3136,6 +3217,7 @@ config.picture_config=Nastavení obrázku a avataru
 config.picture_service=Služba ikon uživatelů
 config.disable_gravatar=Zakázat službu Gravatar
 config.enable_federated_avatar=Povolit avatary z veřejných zdrojů
+config.open_with_editor_app_help=Editory "Otevřít" v nabídce klon. Ponecháte-li prázdné, bude použito výchozí. Pro zobrazení výchozího nastavení rozbalte.
 
 config.git_config=Konfigurace Gitu
 config.git_disable_diff_highlight=Zakázat zvýraznění syntaxe v rozdílovém zobrazení
@@ -3150,6 +3232,7 @@ config.git_pull_timeout=Časový limit operace stažení
 config.git_gc_timeout=Časový limit operace GC
 
 config.log_config=Nastavení logů
+config.logger_name_fmt=Logger: %s
 config.disabled_logger=Zakázané
 config.access_log_mode=Režim logování přístupu
 config.access_log_template=Šablona záznamu přístupu
@@ -3168,6 +3251,7 @@ monitor.execute_times=Vykonání
 monitor.process=Spuštěné procesy
 monitor.stacktrace=Výpisy zásobníku
 monitor.processes_count=%d procesů
+monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu
 monitor.desc=Popis
 monitor.start=Čas zahájení
 monitor.execute_time=Doba provádění
@@ -3187,6 +3271,7 @@ monitor.queue.maxnumberworkers=Maximální počet workerů
 monitor.queue.numberinqueue=Číslo ve frontě
 monitor.queue.review_add=Posoudit / přidat workery
 monitor.queue.settings.title=Nastavení fondu
+monitor.queue.settings.desc=Fondy se dynamicky zvětšují v závislosti na blokování jejich pracovních front.
 monitor.queue.settings.maxnumberworkers=Maximální počet workerů
 monitor.queue.settings.maxnumberworkers.placeholder=V současné době %[1]d
 monitor.queue.settings.maxnumberworkers.error=Maximální počet workerů musí být číslo
@@ -3224,13 +3309,13 @@ commit_repo=nahrál/a do <a href="%[2]s">%[3]s</a> v <a href="%[1]s">%[4]s</a>
 create_issue=`otevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
 close_issue=`uzavřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
 reopen_issue=`znovuotevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
-create_pull_request=`vytvořil/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-close_pull_request=`uzavřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-reopen_pull_request=`znovuotevřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
+create_pull_request=`vytvořil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+close_pull_request=`uzavřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+reopen_pull_request=`znovuotevřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
 comment_issue=`okomentoval/a problém <a href="%[1]s">%[3]s#%[2]s</a>`
-comment_pull=`okomentoval/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-merge_pull_request=`sloučil/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-auto_merge_pull_request=`automaticky sloučen požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
+comment_pull=`okomentoval/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+merge_pull_request=`sloučil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+auto_merge_pull_request=`automaticky sloučen pull request <a href="%[1]s">%[3]s#%[2]s</a>`
 transfer_repo=předal/a repozitář <code>%s</code> uživateli/organizaci <a href="%s">%s</a>
 push_tag=nahrál/a značku <a href="%[2]s">%[3]s</a> do <a href="%[1]s">%[4]s</a>
 delete_tag=smazal/a značku %[2]s z <a href="%[1]s">%[3]s</a>
@@ -3431,6 +3516,7 @@ owner.settings.cargo.initialize.description=Pro použití Cargo registru je zapo
 owner.settings.cargo.initialize.error=Nepodařilo se inicializovat Cargo index: %v
 owner.settings.cargo.initialize.success=Index Cargo byl úspěšně vytvořen.
 owner.settings.cargo.rebuild=Znovu vytvořit Index
+owner.settings.cargo.rebuild.description=Obnova může být užitečná, pokud index není synchronizován s uloženými balíčky Cargo.
 owner.settings.cargo.rebuild.error=Obnovení Cargo indexu se nezdařilo: %v
 owner.settings.cargo.rebuild.success=Cargo Index byl úspěšně obnoven.
 owner.settings.cleanuprules.title=Spravovat pravidla pro čištění
@@ -3523,8 +3609,10 @@ runners.reset_registration_token_success=Registrační token runneru byl úspě
 runs.all_workflows=Všechny pracovní postupy
 runs.commit=Commit
 runs.scheduled=Naplánováno
+runs.pushed_by=náhrán
 runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s
 runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s
+runs.no_job_without_needs=Pracovní postup musí obsahovat alespoň jednu úlohu bez závislostí.
 runs.actor=Aktér
 runs.status=Status
 runs.actors_no_select=Všichni aktéři
@@ -3542,6 +3630,7 @@ workflow.enable=Povolit pracovní postup
 workflow.enable_success=Pracovní postup „%s“ byl úspěšně aktivován.
 workflow.disabled=Pracovní postup je zakázán.
 
+need_approval_desc=Potřebujete schválení pro spuštění pracovních postupů pro rozštěpený pull request.
 
 variables=Proměnné
 variables.management=Správa proměnných
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index fa10bfcb11..1dacb0e0ee 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -17,6 +17,7 @@ template=Template
 language=Sprache
 notifications=Benachrichtigungen
 active_stopwatch=Aktive Zeiterfassung
+tracked_time_summary=Zusammenfassung der erfassten Zeit basierend auf Filtern der Issue-Liste
 create_new=Erstellen…
 user_profile_and_more=Profil und Einstellungen…
 signed_in_as=Angemeldet als
@@ -24,6 +25,7 @@ enable_javascript=Diese Website benötigt JavaScript.
 toc=Inhaltsverzeichnis
 licenses=Lizenzen
 return_to_gitea=Zurück zu Gitea
+more_items=Weitere Einträge
 
 username=Benutzername
 email=E-Mail-Adresse
@@ -90,6 +92,7 @@ remove=Löschen
 remove_all=Alle entfernen
 remove_label_str=Element "%s " entfernen
 edit=Bearbeiten
+view=Anzeigen
 
 enabled=Aktiviert
 disabled=Deaktiviert
@@ -99,7 +102,7 @@ copy=Kopieren
 copy_url=URL kopieren
 copy_hash=Hash kopieren
 copy_content=Inhalt kopieren
-copy_branch=Branchenname kopieren
+copy_branch=Branchennamen kopieren
 copy_success=Kopiert!
 copy_error=Kopieren fehlgeschlagen
 copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden
@@ -111,6 +114,7 @@ loading=Laden…
 error=Fehler
 error404=Die Seite, die Du versuchst aufzurufen, <strong>existiert nicht</strong> oder <strong>Du bist nicht berechtigt</strong>, diese anzusehen.
 go_back=Zurück
+invalid_data=Ungültige Daten: %v
 
 never=Niemals
 unknown=Unbekannt
@@ -121,6 +125,7 @@ pin=Anheften
 unpin=Loslösen
 
 artifacts=Artefakte
+confirm_delete_artifact=Bist du sicher, dass du das Artefakt '%s' löschen möchtest?
 
 archived=Archiviert
 
@@ -139,6 +144,43 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen?
 name=Name
 value=Wert
 
+filter=Filter
+filter.clear=Filter leeren
+filter.is_archived=Archiviert
+filter.not_archived=Nicht archiviert
+filter.is_fork=Fork
+filter.not_fork=Kein Fork
+filter.is_mirror=Gespiegelt
+filter.not_mirror=Nicht gespiegelt
+filter.is_template=Template
+filter.not_template=Kein Template
+filter.public=Öffentlich
+filter.private=Privat
+
+no_results_found=Es wurden keine Ergebnisse gefunden.
+
+[search]
+search=Suche ...
+type_tooltip=Suchmodus
+fuzzy=Ähnlich
+fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind
+match=Genau
+match_tooltip=Nur genau zum Suchbegriff passende Ergebnisse einbeziehen
+repo_kind=Repositories durchsuchen ...
+user_kind=Benutzer durchsuchen ...
+org_kind=Organisationen durchsuchen ...
+team_kind=Teams durchsuchen ...
+code_kind=Code durchsuchen ...
+code_search_unavailable=Zurzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
+code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" bereitgestellt. Es könnte bessere Ergebnisse geben, wenn der Website-Administrator den Repository-Indexer aktiviert.
+package_kind=Pakete durchsuchen ...
+project_kind=Projekte durchsuchen ...
+branch_kind=Branches durchsuchen ...
+commit_kind=Commits durchsuchen ...
+runner_kind=Runner durchsuchen ...
+no_results=Es wurden keine passenden Ergebnisse gefunden.
+keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
+
 [aria]
 navbar=Navigationsleiste
 footer=Fußzeile
@@ -244,6 +286,7 @@ email_title=E-Mail-Einstellungen
 smtp_addr=SMTP-Host
 smtp_port=SMTP-Port
 smtp_from=E-Mail senden als
+smtp_from_invalid=Die „E-Mail senden als“ Adresse ist ungültig
 smtp_from_helper=E-Mail-Adresse, die von Gitea genutzt werden soll. Bitte gib die E-Mail-Adresse im Format „"Name" <email@example.com>“ ein.
 mailer_user=SMTP-Benutzername
 mailer_password=SMTP-Passwort
@@ -303,6 +346,7 @@ env_config_keys=Umgebungskonfiguration
 env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet:
 
 [home]
+nav_menu=Navigationsmenü
 uname_holder=E-Mail-Adresse oder Benutzername
 password_holder=Passwort
 switch_dashboard_context=Kontext der Übersichtsseite wechseln
@@ -312,7 +356,6 @@ collaborative_repos=Gemeinschaftliche Repositories
 my_orgs=Meine Organisationen
 my_mirrors=Meine Mirrors
 view_home=%s ansehen
-search_repos=Finde ein Repository…
 filter=Andere Filter
 filter_by_team_repositories=Nach Team-Repositories filtern
 feed_of=`Feed von "%s"`
@@ -333,20 +376,8 @@ issues.in_your_repos=Eigene Repositories
 repos=Repositories
 users=Benutzer
 organizations=Organisationen
-search=Suche
 go_to=Gehe zu
 code=Code
-search.type.tooltip=Suchmodus
-search.fuzzy=Ähnlich
-search.fuzzy.tooltip=Zeige auch Ergebnisse, die dem Suchbegriff ähneln
-search.match=Genau
-search.match.tooltip=Zeige nur Ergebnisse, die exakt mit dem Suchbegriff übereinstimmen
-code_search_unavailable=Derzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
-repo_no_results=Keine passenden Repositories gefunden.
-user_no_results=Keine passenden Benutzer gefunden.
-org_no_results=Keine passenden Organisationen gefunden.
-code_no_results=Es konnte kein passender Code für deinen Suchbegriff gefunden werden.
-code_search_results=`Suchergebnisse für "%s"`
 code_last_indexed_at=Zuletzt indexiert %s
 relevant_repositories_tooltip=Repositories, die Forks sind oder die kein Thema, kein Symbol und keine Beschreibung haben, werden ausgeblendet.
 relevant_repositories=Es werden nur relevante Repositories angezeigt, <a href="%s">ungefilterte Ergebnisse anzeigen</a>.
@@ -359,11 +390,12 @@ disable_register_prompt=Die Registrierung ist deaktiviert. Bitte wende dich an d
 disable_register_mail=E-Mail-Bestätigung bei der Registrierung ist deaktiviert.
 manual_activation_only=Kontaktiere den Website-Administrator, um die Aktivierung abzuschließen.
 remember_me=Dieses Gerät speichern
+remember_me.compromised=Das Login-Token ist nicht mehr gültig, was auf ein kompromittiertes Konto hindeuten kann. Bitte überprüfe dein Konto auf ungewöhnliche Aktivitäten.
 forgot_password_title=Passwort vergessen
 forgot_password=Passwort vergessen?
 sign_up_now=Noch kein Konto? Jetzt registrieren.
 sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen!
-confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die Registrierung abzuschließen.
+confirmation_mail_sent_prompt_ex=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>gesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern.
 must_change_password=Aktualisiere dein Passwort
 allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen)
 reset_password_mail_sent_prompt=Eine Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb von %s, um den Wiederherstellungsprozess abzuschließen.
@@ -373,6 +405,7 @@ prohibit_login=Anmelden verboten
 prohibit_login_desc=Die Anmeldung mit diesem Konto ist nicht gestattet. Bitte kontaktiere den Administrator.
 resent_limit_prompt=Du hast bereits eine Aktivierungs-E-Mail angefordert. Bitte warte 3 Minuten und probiere es dann nochmal.
 has_unconfirmed_mail=Hallo %s, du hast eine unbestätigte E-Mail-Adresse (<b>%s</b>). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue senden möchtest, klicke bitte auf den folgenden Button.
+change_unconfirmed_mail_address=Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du sie hier ändern und eine neue Bestätigungs-E-Mail senden.
 resend_mail=Aktivierungs-E-Mail erneut verschicken
 email_not_associate=Diese E-Mail-Adresse ist mit keinem Konto verknüpft.
 send_reset_mail=Wiederherstellungs-E-Mail senden
@@ -420,6 +453,7 @@ authorization_failed_desc=Die Autorisierung ist fehlgeschlagen, da wir eine ung
 sspi_auth_failed=SSPI-Authentifizierung fehlgeschlagen
 password_pwned=Das von dir gewählte Passwort befindet sich auf einer <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">List gestohlener Passwörter</a>, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern.
 password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden
+last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben.
 
 [mail]
 view_it_on=Auf %s ansehen
@@ -552,6 +586,7 @@ team_name_been_taken=Der Teamname ist bereits vergeben.
 team_no_units_error=Das Team muss auf mindestens einen Bereich Zugriff haben.
 email_been_used=Die E-Mail-Adresse wird bereits verwendet.
 email_invalid=Die E-Mail-Adresse ist ungültig.
+email_domain_is_not_allowed=Die Domain der Benutzer-E-Mail <b>%s</b> steht im Widerspruch zu EMAIL_DOMAIN_ALLOWLIST oder EMAIL_DOMAIN_BLOCKLIST. Bitte stelle sicher, dass deine Operation erwartet ist.
 openid_been_used=Die OpenID-Adresse "%s" wird bereits verwendet.
 username_password_incorrect=Benutzername oder Passwort ist falsch.
 password_complexity=Das Passwort erfüllt nicht die Komplexitätsanforderungen:
@@ -563,6 +598,8 @@ enterred_invalid_repo_name=Der eingegebenen Repository-Name ist falsch.
 enterred_invalid_org_name=Der eingegebene Organisation-Name ist falsch.
 enterred_invalid_owner_name=Der Name des neuen Besitzers ist ungültig.
 enterred_invalid_password=Das eingegebene Passwort ist falsch.
+unset_password=Der Login-Benutzer hat das Passwort nicht gesetzt.
+unsupported_login_type=Der Anmeldetyp wird zum Löschen des Kontos nicht unterstützt.
 user_not_exist=Dieser Benutzer ist nicht vorhanden.
 team_not_exist=Dieses Team existiert nicht.
 last_org_owner=Du kannst den letzten Benutzer nicht aus dem 'Besitzer'-Team entfernen. Es muss mindestens einen Besitzer in einer Organisation geben.
@@ -585,6 +622,7 @@ org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete,
 
 target_branch_not_exist=Der Ziel-Branch existiert nicht.
 
+admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte.
 
 [user]
 change_avatar=Profilbild ändern…
@@ -611,6 +649,30 @@ form.name_reserved=Der Benutzername "%s" ist reserviert.
 form.name_pattern_not_allowed=Das Muster "%s" ist nicht in einem Benutzernamen erlaubt.
 form.name_chars_not_allowed=Benutzername "%s" enthält ungültige Zeichen.
 
+block.block=Sperren
+block.block.user=Benutzer sperren
+block.block.org=Benutzer für Organisation sperren
+block.block.failure=Fehler beim Sperren des Benutzers: %s
+block.unblock=Entsperren
+block.unblock.failure=Fehler beim Entsperren des Benutzers: %s
+block.blocked=Du hast diesen Benutzer gesperrt.
+block.title=Einen Benutzer sperren
+block.info=Das Blockieren eines Benutzers hindert ihn daran, mit Repositories zu interagieren, wie zum Beispiel das Öffnen oder Kommentieren von Pull Requests oder Issues. Erfahre mehr über das Blockieren eines Benutzers.
+block.info_1=Das Blockieren eines Benutzers verhindert folgende Aktionen auf deinem Konto und deinen Repositories:
+block.info_2=deinem Konto folgen
+block.info_3=dir Benachrichtigungen durch @Erwähnung deines Benutzernamens senden
+block.info_4=dich als Mitarbeiter in deren Repositories einladen
+block.info_5=Repositories favorisieren, forken oder beobachten
+block.info_6=Issues oder Pull Requests öffnen und kommentieren
+block.info_7=auf deine Kommentare in Issues oder Pull Requests reagieren
+block.user_to_block=Zu sperrender Benutzer
+block.note=Anmerkung
+block.note.title=Optionale Anmerkung:
+block.note.info=Die Anmerkung ist für den blockierten Benutzer nicht sichtbar.
+block.note.edit=Anmerkung bearbeiten
+block.list=Gesperrte Benutzer
+block.list.none=Du hast noch keine Benutzer gesperrt.
+
 [settings]
 profile=Profil
 account=Account
@@ -755,7 +817,6 @@ gpg_invalid_token_signature=Der GPG Key, die Signatur, und das Token stimmen nic
 gpg_token_required=Du musst eine Signatur für das folgende Token angeben
 gpg_token=Token
 gpg_token_help=Du kannst eine Signatur wie folgt generieren:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=GPG Textsignatur (armored signature)
 key_signature_gpg_placeholder=Beginnt mit '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=GPG-Schlüssel "%s" wurde verifiziert.
@@ -863,6 +924,7 @@ revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die A
 revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen.
 
 twofa_desc=Zwei-Faktor-Authentifizierung trägt zu einer höheren Accountsicherheit bei.
+twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen.
 twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung <strong>eingeschaltet</strong>.
 twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet.
 twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren
@@ -885,6 +947,8 @@ webauthn_register_key=Sicherheitsschlüssel hinzufügen
 webauthn_nickname=Nickname
 webauthn_delete_key=Sicherheitsschlüssel entfernen
 webauthn_delete_key_desc=Wenn du einen Sicherheitsschlüssel entfernst, kannst du dich nicht mehr mit ihm anmelden. Fortfahren?
+webauthn_key_loss_warning=Wenn du deine Sicherheitsschlüssel verlierst, verlierst du den Zugriff auf dein Konto.
+webauthn_alternative_tip=Möglicherweise möchtest du eine zusätzliche Authentifizierungsmethode konfigurieren.
 
 manage_account_links=Verknüpfte Accounts verwalten
 manage_account_links_desc=Diese externen Accounts sind mit deinem Gitea-Account verknüpft.
@@ -921,6 +985,7 @@ visibility.private=Privat
 visibility.private_tooltip=Sichtbar nur für Mitglieder von Organisationen, denen du beigetreten bist
 
 [repo]
+new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des Änderungsverlaufs. Schon woanders vorhanden? <a href="%s">Migration eines Repositorys.</a>
 owner=Besitzer
 owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist.
 repo_name=Repository-Name
@@ -944,8 +1009,9 @@ fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht
 fork_branch=Branch, der zum Fork geklont werden soll
 all_branches=Alle Branches
 fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind.
+fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest.
 use_template=Dieses Template verwenden
-clone_in_vsc=In VS Code klonen
+open_with_editor=Mit %s öffnen
 download_zip=ZIP herunterladen
 download_tar=TAR.GZ herunterladen
 download_bundle=BUNDLE herunterladen
@@ -961,6 +1027,8 @@ issue_labels_helper=Wähle ein Issue-Label-Set.
 license=Lizenz
 license_helper=Wähle eine Lizenz aus.
 license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">eine Lizenz wählen</a>.
+object_format=Objektformat
+object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel.
 readme=README
 readme_helper=Wähle eine README-Vorlage aus.
 readme_helper_desc=Hier kannst du eine komplette Beschreibung für dein Projekt schreiben.
@@ -978,6 +1046,7 @@ mirror_prune=Entfernen
 mirror_prune_desc=Entferne veraltete remote-tracking Referenzen
 mirror_interval=Mirror-Intervall (gültige Zeiteinheiten sind 'h', 'm', 's'). 0 deaktiviert die regelmäßige Synchronisation. (Minimales Intervall: %s)
 mirror_interval_invalid=Das Spiegel-Intervall ist ungültig.
+mirror_sync=synchronisiert
 mirror_sync_on_commit=Synchronisieren, wenn Commits gepusht wurden
 mirror_address=Klonen via URL
 mirror_address_desc=Gib alle erforderlichen Anmeldedaten im Abschnitt "Authentifizierung" ein.
@@ -995,6 +1064,7 @@ watchers=Beobachter
 stargazers=Favorisiert von
 stars_remove_warning=Dies wird alle Sterne aus diesem Repository entfernen.
 forks=Forks
+stars=Favoriten
 reactions_more=und %d weitere
 unit_disabled=Der Administrator hat diesen Repository-Bereich deaktiviert.
 language_other=Andere
@@ -1028,6 +1098,7 @@ desc.public=Öffentlich
 desc.template=Template
 desc.internal=Intern
 desc.archived=Archiviert
+desc.sha256=SHA256
 
 template.items=Template-Elemente
 template.git_content=Git Inhalt (Standardbranch)
@@ -1115,6 +1186,7 @@ watch=Beobachten
 unstar=Favorit entfernen
 star=Favorisieren
 fork=Fork
+action.blocked_user=Die Aktion kann nicht ausgeführt werden, da du vom Repository-Eigentümer blockiert wurdest.
 download_archive=Repository herunterladen
 more_operations=Weitere Operationen
 
@@ -1178,6 +1250,8 @@ audio_not_supported_in_browser=Dein Browser unterstützt den HTML5 'audio'-Tag n
 stored_lfs=Gespeichert mit Git LFS
 symbolic_link=Softlink
 executable_file=Ausführbare Datei
+vendored=Vendor
+generated=Generiert
 commit_graph=Commit graph
 commit_graph.select=Branches auswählen
 commit_graph.hide_pr_refs=Pull-Requests ausblenden
@@ -1241,6 +1315,8 @@ editor.file_editing_no_longer_exists=Die bearbeitete Datei "%s" existiert nicht
 editor.file_deleting_no_longer_exists=Die zu löschende Datei "%s" existiert nicht mehr in diesem Repository.
 editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. <a target="_blank" rel="noopener noreferrer" href="%s">Hier klicken</a>, um die Änderungen anzusehen, oder <strong>Änderungen erneut comitten</strong>, um sie zu überschreiben.
 editor.file_already_exists=Eine Datei mit dem Namen '%s' existiert bereits in diesem Repository.
+editor.commit_id_not_matching=Die Commit-ID stimmt nicht mit der ID überein, bei welcher du mit der Bearbeitung begonnen hast. Commite in einen Patch-Branch und merge daraufhin.
+editor.push_out_of_date=Der Push scheint veraltet zu sein.
 editor.commit_empty_file_header=Leere Datei committen
 editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren?
 editor.no_changes_to_show=Keine Änderungen vorhanden.
@@ -1264,9 +1340,8 @@ commits.desc=Durchsuche die Quellcode-Änderungshistorie.
 commits.commits=Commits
 commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien.
 commits.nothing_to_compare=Diese Branches sind auf demselben Stand.
-commits.search=Commits durchsuchen…
 commits.search.tooltip=Du kannst Suchbegriffen "author:", " committer:", "after:", oder " before:" voranstellen, z.B. "revert author:Alice before:2019-04-01".
-commits.find=Suchen
+commits.search_branch=Dieser Branch
 commits.search_all=Alle Branches
 commits.author=Autor
 commits.message=Nachricht
@@ -1317,7 +1392,6 @@ projects.type.basic_kanban=Einfaches Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Projektvorlage
 projects.template.desc_helper=Wähle eine Projektvorlage aus, um loszulegen
-projects.type.uncategorized=Nicht kategorisiert
 projects.column.edit=Spalte bearbeiten
 projects.column.edit_title=Name
 projects.column.new_title=Name
@@ -1325,10 +1399,8 @@ projects.column.new_submit=Spalte erstellen
 projects.column.new=Neue Spalte
 projects.column.set_default=Als Standard verwenden
 projects.column.set_default_desc=Diese Spalte als Standard für unkategorisierte Issues und Pull Requests festlegen
-projects.column.unset_default=Standard entfernen
-projects.column.unset_default_desc=Diese Spalte als Standard entfernen
 projects.column.delete=Spalte löschen
-projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues nach 'Nicht kategorisiert' verschoben. Fortfahren?
+projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle Einträge in die Standard-Spalte verschoben. Fortfahren?
 projects.column.color=Farbe
 projects.open=Öffnen
 projects.close=Schließen
@@ -1363,6 +1435,8 @@ issues.new.assignees=Zuständig
 issues.new.clear_assignees=Zuständige entfernen
 issues.new.no_assignees=Niemand zuständig
 issues.new.no_reviewers=Keine Reviewer
+issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
+issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
 issues.choose.get_started=Los geht's
 issues.choose.open_external_link=Öffnen
 issues.choose.blank=Standard
@@ -1440,7 +1514,6 @@ issues.filter_sort.moststars=Meiste Favoriten
 issues.filter_sort.feweststars=Wenigste Favoriten
 issues.filter_sort.mostforks=Meiste Forks
 issues.filter_sort.fewestforks=Wenigste Forks
-issues.keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
 issues.action_open=Öffnen
 issues.action_close=Schließen
 issues.action_label=Label
@@ -1478,6 +1551,7 @@ issues.close_comment_issue=Kommentieren und schließen
 issues.reopen_issue=Wieder öffnen
 issues.reopen_comment_issue=Kommentieren und wieder öffnen
 issues.create_comment=Kommentieren
+issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
 issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
 issues.reopened_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
 issues.commit_ref_at=`hat dieses Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> aus einem Commit referenziert`
@@ -1676,6 +1750,7 @@ compare.compare_head=vergleichen
 
 pulls.desc=Pull-Requests und Code-Reviews aktivieren.
 pulls.new=Neuer Pull-Request
+pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
 pulls.view=Pull-Request ansehen
 pulls.compare_changes=Neuer Pull-Request
 pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben
@@ -1692,7 +1767,6 @@ pulls.compare_compare=pullen von
 pulls.switch_comparison_type=Vergleichstyp wechseln
 pulls.switch_head_and_base=Head und Base vertauschen
 pulls.filter_branch=Branch filtern
-pulls.no_results=Keine Ergebnisse verfügbar.
 pulls.show_all_commits=Alle Commits anzeigen
 pulls.show_changes_since_your_last_review=Zeige Änderungen seit deinem letzten Review
 pulls.showing_only_single_commit=Nur Änderungen aus Commit %[1]s werden angezeigt
@@ -1701,6 +1775,7 @@ pulls.select_commit_hold_shift_for_range=Commit auswählen. Halte Shift + klicke
 pulls.review_only_possible_for_full_diff=Ein Review ist nur möglich, wenn das vollständige Diff angezeigt wird
 pulls.filter_changes_by_commit=Nach Commit filtern
 pulls.nothing_to_compare=Diese Branches sind identisch. Es muss kein Pull-Request erstellt werden.
+pulls.nothing_to_compare_have_tag=Der ausgewählte Branch und Tag sind gleich.
 pulls.nothing_to_compare_and_allow_empty_pr=Diese Branches sind gleich. Der Pull-Request wird leer sein.
 pulls.has_pull_request=`Es existiert bereits ein Pull-Request zwischen diesen beiden Branches: <a href="%[1]s">%[2]s#%[3]d</a>`
 pulls.create=Pull-Request erstellen
@@ -1759,6 +1834,7 @@ pulls.merge_pull_request=Merge Commit erstellen
 pulls.rebase_merge_pull_request=Rebasen und dann fast-forwarden
 pulls.rebase_merge_commit_pull_request=Rebasen und dann mergen
 pulls.squash_merge_pull_request=Squash Commit erstellen
+pulls.fast_forward_only_merge_pull_request=Nur Fast-forward
 pulls.merge_manually=Manuell mergen
 pulls.merge_commit_id=Der Mergecommit ID
 pulls.require_signed_wont_sign=Der Branch erfordert einen signierten Commit, aber dieser Merge wird nicht signiert
@@ -1783,6 +1859,8 @@ pulls.status_checks_failure=Einige Prüfungen sind fehlgeschlagen
 pulls.status_checks_error=Einige Checks meldeten Fehler
 pulls.status_checks_requested=Erforderlich
 pulls.status_checks_details=Details
+pulls.status_checks_hide_all=Alle Prüfungen ausblenden
+pulls.status_checks_show_all=Alle Prüfungen anzeigen
 pulls.update_branch=Branch durch Mergen aktualisieren
 pulls.update_branch_rebase=Branch durch Rebase aktualisieren
 pulls.update_branch_success=Branch-Aktualisierung erfolgreich
@@ -1791,6 +1869,11 @@ pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits
 pulls.close=Pull-Request schließen
 pulls.closed_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
 pulls.reopened_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
+pulls.cmd_instruction_hint=`Zeige <a class="show-instruction">Kommandozeilenanweisungen</a>.`
+pulls.cmd_instruction_checkout_title=Checkout
+pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
+pulls.cmd_instruction_merge_title=Mergen
+pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren.
 pulls.clear_merge_message=Merge-Nachricht löschen
 pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten.
 
@@ -1888,6 +1971,10 @@ wiki.page_name_desc=Gib einen Namen für diese Wiki-Seite ein. Spezielle Namen s
 wiki.original_git_entry_tooltip=Originale Git-Datei anstatt eines benutzerfreundlichen Links anzeigen.
 
 activity=Aktivität
+activity.navbar.pulse=Puls
+activity.navbar.code_frequency=Code-Frequenz
+activity.navbar.contributors=Mitwirkende
+activity.navbar.recent_commits=Neueste Commits
 activity.period.filter_label=Zeitraum:
 activity.period.daily=1 Tag
 activity.period.halfweekly=3 Tage
@@ -1953,18 +2040,10 @@ activity.git_stats_and_deletions=und
 activity.git_stats_deletion_1=%d Löschung
 activity.git_stats_deletion_n=%d Löschungen
 
+contributors.contribution_type.filter_label=Beitragstyp:
 contributors.contribution_type.commits=Commits
-
-search=Suchen
-search.search_repo=Repository durchsuchen
-search.type.tooltip=Suchmodus
-search.fuzzy=Ähnlich
-search.fuzzy.tooltip=Zeige auch Ergebnisse, die dem Suchbegriff ähneln
-search.match=Genau
-search.match.tooltip=Zeige nur Ergebnisse, die exakt mit dem Suchbegriff übereinstimmen
-search.results=Suchergebnisse für „%s“ in <a href="%s"> %s</a>
-search.code_no_results=Es konnte kein passender Code für deinen Suchbegriff gefunden werden.
-search.code_search_unavailable=Derzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
+contributors.contribution_type.additions=Ergänzungen
+contributors.contribution_type.deletions=Löschungen
 
 settings=Einstellungen
 settings.desc=In den Einstellungen kannst du die Einstellungen des Repositories anpassen
@@ -1992,6 +2071,7 @@ settings.mirror_settings.docs.doc_link_title=Wie spiegele ich Repositories?
 settings.mirror_settings.docs.doc_link_pull_section=den Abschnitt "Von einem entfernten Repository pullen" in der Dokumentation.
 settings.mirror_settings.docs.pulling_remote_title=Aus einem Remote-Repository pullen
 settings.mirror_settings.mirrored_repository=Gespiegeltes Repository
+settings.mirror_settings.pushed_repository=Gepushtes Repository
 settings.mirror_settings.direction=Richtung
 settings.mirror_settings.direction.pull=Pull
 settings.mirror_settings.direction.push=Push
@@ -2013,6 +2093,8 @@ settings.branches.add_new_rule=Neue Regel hinzufügen
 settings.advanced_settings=Erweiterte Einstellungen
 settings.wiki_desc=Repository-Wiki aktivieren
 settings.use_internal_wiki=Eingebautes Wiki verwenden
+settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch
+settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen.
 settings.use_external_wiki=Externes Wiki verwenden
 settings.external_wiki_url=Externe Wiki-URL
 settings.external_wiki_url_error=Die externe Wiki-URL ist ungültig.
@@ -2043,6 +2125,10 @@ settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern
 settings.releases_desc=Repository-Releases aktivieren
 settings.packages_desc=Repository Packages Registry aktivieren
 settings.projects_desc=Repository-Projekte aktivieren
+settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen)
+settings.projects_mode_repo=Nur Repo-Projekte
+settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte
+settings.projects_mode_all=Alle Projekte
 settings.actions_desc=Repository-Actions aktivieren
 settings.admin_settings=Administratoreinstellungen
 settings.admin_enable_health_check=Repository-Health-Checks aktivieren (git fsck)
@@ -2068,6 +2154,7 @@ settings.convert_fork_succeed=Der Fork wurde in ein normales Repository konverti
 settings.transfer=Besitz übertragen
 settings.transfer.rejected=Repository-Übertragung wurde abgelehnt.
 settings.transfer.success=Repository-Übertragung war erfolgreich.
+settings.transfer.blocked_user=Das Repository kann nicht übertragen werden, da du vom Repository-Eigentümer blockiert wurdest.
 settings.transfer_abort=Übertragung abbrechen
 settings.transfer_abort_invalid=Du kannst nur eingeleitete Repository-Übertragung abbrechen.
 settings.transfer_abort_success=Die Repository-Übertragung zu %s wurde abgebrochen.
@@ -2113,11 +2200,11 @@ settings.add_collaborator_success=Der Mitarbeiter wurde hinzugefügt.
 settings.add_collaborator_inactive_user=Inaktive Benutzer können nicht als Mitarbeiter hinzufügt werden.
 settings.add_collaborator_owner=Besitzer können nicht als Mitarbeiter hinzugefügt werden.
 settings.add_collaborator_duplicate=Der Mitarbeiter ist bereits zu diesem Repository hinzugefügt.
+settings.add_collaborator.blocked_user=Der Mitwirkende wurde vom Eigentümer des Repositories blockiert oder umgekehrt.
 settings.delete_collaborator=Entfernen
 settings.collaborator_deletion=Mitarbeiter entfernen
 settings.collaborator_deletion_desc=Nach dem Löschen wird dieser Mitarbeiter keinen Zugriff mehr auf dieses Repository haben. Fortfahren?
 settings.remove_collaborator_success=Der Mitarbeiter wurde entfernt.
-settings.search_user_placeholder=Benutzer suchen…
 settings.org_not_allowed_to_be_collaborator=Organisationen können nicht als Mitarbeiter hinzugefügt werden.
 settings.change_team_access_not_allowed=Nur der Besitzer der Organisation kann die Zugangsrechte des Teams ändern
 settings.team_not_in_organization=Das Team ist nicht in der gleichen Organisation wie das Repository
@@ -2125,7 +2212,6 @@ settings.teams=Teams
 settings.add_team=Team hinzufügen
 settings.add_team_duplicate=Das Team ist dem Repository schon zugeordnet
 settings.add_team_success=Das Team hat nun Zugriff auf das Repository.
-settings.search_team=Team suchen…
 settings.change_team_permission_tip=Die Team-Berechtigung ist auf der Team-Einstellungsseite festgelegt und kann nicht für ein Repository geändert werden
 settings.delete_team_tip=Dieses Team hat Zugriff auf alle Repositories und kann nicht entfernt werden
 settings.remove_team_success=Der Zugriff des Teams auf das Repository wurde zurückgezogen.
@@ -2278,9 +2364,7 @@ settings.protect_whitelist_committers=Schütze gewhitelistete Commiter
 settings.protect_whitelist_committers_desc=Jeder, der auf der Whitelist steht, darf in diesen Branch pushen (aber kein Force-Push).
 settings.protect_whitelist_deploy_keys=Deploy-Schlüssel mit Schreibzugriff zum Pushen whitelisten.
 settings.protect_whitelist_users=Nutzer, die pushen dürfen:
-settings.protect_whitelist_search_users=Benutzer suchen…
 settings.protect_whitelist_teams=Teams, die pushen dürfen:
-settings.protect_whitelist_search_teams=Teams suchen…
 settings.protect_merge_whitelist_committers=Merge-Whitelist aktivieren
 settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Pull-Requests in diesen Branch zu mergen.
 settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen:
@@ -2301,9 +2385,12 @@ settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer:
 settings.protect_approvals_whitelist_teams=Freigeschaltete Teams:
 settings.dismiss_stale_approvals=Entferne alte Genehmigungen
 settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt.
+settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren
+settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits erteilt wurden (veraltete Genehmigungen), nicht bei der Anzahl an Genehmigungen mitzählen. Irrelevant, falls veraltete Genehmigungen bereits verworfen wurden.
 settings.require_signed_commits=Signierte Commits erforderlich
 settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind.
 settings.protect_branch_name_pattern=Muster für geschützte Branchnamen
+settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe <a href="https://github.com/gobwas/glob">die Dokumentation</a> für die Muster-Syntax. Beispiele: main, release/**
 settings.protect_patterns=Muster
 settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt):
 settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> Dokumentation zur Mustersyntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2355,6 +2442,7 @@ settings.archive.error=Beim Versuch, das Repository zu archivieren, ist ein Fehl
 settings.archive.error_ismirror=Du kannst keinen Repo-Mirror archivieren.
 settings.archive.branchsettings_unavailable=Branch-Einstellungen sind nicht verfügbar wenn das Repo archiviert ist.
 settings.archive.tagsettings_unavailable=Tag Einstellungen sind nicht verfügbar, wenn das Repo archiviert wurde.
+settings.archive.mirrors_unavailable=Mirrors sind nicht verfügbar, wenn das Repository archiviert ist.
 settings.unarchive.button=Archivieren rückgängig machen
 settings.unarchive.header=Archivieren dieses Repositories rückgängig machen
 settings.unarchive.text=Durch das Aufheben der Archivierung kann das Repo wieder Commits und Pushes sowie neue Issues und Pull-Requests empfangen.
@@ -2521,7 +2609,6 @@ branch.default_deletion_failed=Branch "%s" kann nicht gelöscht werden, da diese
 branch.restore=Branch "%s" wiederherstellen
 branch.download=Branch "%s" herunterladen
 branch.rename=Branch "%s" umbenennen
-branch.search=Branch suchen
 branch.included_desc=Dieser Branch ist im Standard-Branch enthalten
 branch.included=Enthalten
 branch.create_new_branch=Branch aus Branch erstellen:
@@ -2552,8 +2639,16 @@ find_file.no_matching=Keine passende Datei gefunden
 error.csv.too_large=Diese Datei kann nicht gerendert werden, da sie zu groß ist.
 error.csv.unexpected=Diese Datei kann nicht gerendert werden, da sie ein unerwartetes Zeichen in Zeile %d und Spalte %d enthält.
 error.csv.invalid_field_count=Diese Datei kann nicht gerendert werden, da sie eine falsche Anzahl an Feldern in Zeile %d hat.
+error.broken_git_hook=Git-Hooks dieses Repositories scheinen defekt zu sein. Bitte folge der <a target="_blank" rel="noreferrer" href="%s">Dokumentation</a>, um dies zu beheben, pushe dann ein paar Commits und den Status zu aktualisieren.
 
 [graphs]
+component_loading=%s werden geladen ...
+component_loading_failed=%s konnten nicht geladen werden
+component_loading_info=Dies kann ein wenig dauern …
+component_failed_to_load=Ein unerwarteter Fehler ist aufgetreten.
+code_frequency.what=Code-Frequenz
+contributors.what=Beiträge
+recent_commits.what=Neueste Commits
 
 [org]
 org_name_holder=Name der Organisation
@@ -2659,7 +2754,6 @@ teams.write_permission_desc=Dieses Team hat <strong>Schreibzugriff</strong>: Mit
 teams.admin_permission_desc=Dieses Team hat <strong>Adminzugriff</strong>: Mitglieder dieses Teams können Team-Repositories ansehen, auf sie pushen und Mitarbeiter hinzufügen.
 teams.create_repo_permission_desc=Zusätzlich erteilt dieses Team die Berechtigung <strong>Repository erstellen</strong>: Mitglieder können neue Repositories in der Organisation erstellen.
 teams.repositories=Team-Repositories
-teams.search_repo_placeholder=Repository durchsuchen…
 teams.remove_all_repos_title=Alle Team-Repositories entfernen
 teams.remove_all_repos_desc=Dies entfernt alle Repositories von dem Team.
 teams.add_all_repos_title=Alle Repositories hinzufügen
@@ -2668,6 +2762,7 @@ teams.add_nonexistent_repo=Das Repository, das du hinzufügen möchtest, existie
 teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied.
 teams.repos.none=Dieses Team hat Zugang zu keinem Repository.
 teams.members.none=Keine Mitglieder in diesem Team.
+teams.members.blocked_user=Der Benutzer kann nicht hinzugefügt werden, da er von der Organisation blockiert wurde.
 teams.specific_repositories=Bestimmte Repositories
 teams.specific_repositories_helper=Mitglieder haben nur Zugriff auf Repositories, die explizit dem Team hinzugefügt wurden. Wenn Du diese Option wählst, werden Repositories, die bereits mit <i>Alle Repositories</i> hinzugefügt wurden, <strong>nicht</strong> automatisch entfernt.
 teams.all_repositories=Alle Repositories
@@ -2681,6 +2776,7 @@ teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Tea
 
 [admin]
 dashboard=Dashboard
+self_check=Selbstprüfung
 identity_access=Identität & Zugriff
 users=Benutzerkonten
 organizations=Organisationen
@@ -2691,6 +2787,8 @@ integrations=Integrationen
 authentication=Authentifizierungsquellen
 emails=Benutzer E-Mails
 config=Konfiguration
+config_summary=Übersicht
+config_settings=Einstellungen
 notices=Systemmitteilungen
 monitor=Monitoring
 first_page=Erste
@@ -2726,6 +2824,7 @@ dashboard.delete_missing_repos=Alle Repository-Datensätze mit verloren gegangen
 dashboard.delete_missing_repos.started=Alle Repositories löschen, die den Git-File-Task nicht gestartet haben.
 dashboard.delete_generated_repository_avatars=Generierte Repository-Avatare löschen
 dashboard.sync_repo_branches=Fehlende Branches aus den Git-Daten in die Datenbank synchronisieren
+dashboard.sync_repo_tags=Tags von Git-Daten in die Datenbank synchronisieren
 dashboard.update_mirrors=Mirrors aktualisieren
 dashboard.repo_health_check=Healthchecks für alle Repositories ausführen
 dashboard.check_repo_stats=Überprüfe alle Repository-Statistiken
@@ -2780,6 +2879,7 @@ dashboard.stop_endless_tasks=Endlose Aufgaben stoppen
 dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen
 dashboard.start_schedule_tasks=Terminierte Aufgaben starten
 dashboard.sync_branch.started=Synchronisierung der Branches gestartet
+dashboard.sync_tag.started=Tag-Synchronisierung gestartet
 dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen
 
 users.user_manage_panel=Benutzerkontenverwaltung
@@ -2851,6 +2951,7 @@ emails.updated=E-Mail aktualisiert
 emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: %v
 emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet.
 emails.change_email_header=E-Mail-Eigenschaften aktualisieren
+emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest?
 
 orgs.org_manage_panel=Organisationsverwaltung
 orgs.name=Name
@@ -2864,9 +2965,6 @@ repos.unadopted.no_more=Keine weiteren nicht übernommenen Repositories gefunden
 repos.owner=Besitzer
 repos.name=Name
 repos.private=Privat
-repos.watches=Beobachtungen
-repos.stars=Favoriten
-repos.forks=Forks
 repos.issues=Issues
 repos.size=Größe
 repos.lfs_size=LFS-Größe
@@ -2875,6 +2973,7 @@ packages.package_manage_panel=Paketverwaltung
 packages.total_size=Gesamtgröße: %s
 packages.unreferenced_size=Nicht referenzierte Größe: %s
 packages.cleanup=Veraltete Daten löschen
+packages.cleanup.success=Abgelaufene Daten erfolgreich bereinigt
 packages.owner=Besitzer
 packages.creator=Ersteller
 packages.name=Name
@@ -2990,7 +3089,7 @@ auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 cli
 auths.tip.dropbox=Erstelle eine neue App auf https://www.dropbox.com/developers/apps.
 auths.tip.facebook=Erstelle eine neue Anwendung auf https://developers.facebook.com/apps und füge das Produkt „Facebook Login“ hinzu.
 auths.tip.github=Erstelle unter https://github.com/settings/applications/new eine neue OAuth-Anwendung.
-auths.tip.gitlab=Erstelle unter https://gitlab.com/profile/applications eine neue Anwendung.
+auths.tip.gitlab_new=Erstelle eine neue Anwendung unter https://gitlab.com/-/profile/applications
 auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter https://console.developers.google.com/
 auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL (<server>/.well-known/openid-configuration), um die Endpunkte zu spezifizieren
 auths.tip.twitter=Gehe auf https://dev.twitter.com/apps, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist
@@ -3126,6 +3225,7 @@ config.picture_config=Bild-und-Profilbild-Konfiguration
 config.picture_service=Bilderservice
 config.disable_gravatar=Gravatar deaktivieren
 config.enable_federated_avatar=Föderierte Profilbilder einschalten
+config.open_with_editor_app_help=Die „Öffnen mit“-Editoren für das Klon-Menü. Falls leer, wird die Standardeinstellung verwendet. Erweitern, um die Standardeinstellung zu sehen.
 
 config.git_config=Git-Konfiguration
 config.git_disable_diff_highlight=Diff-Syntaxhervorhebung ausschalten
@@ -3204,6 +3304,12 @@ notices.desc=Beschreibung
 notices.op=Aktion
 notices.delete_success=Diese Systemmeldung wurde gelöscht.
 
+self_check.no_problem_found=Bisher wurde kein Problem festgestellt.
+self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s
+self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren.
+self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen.
+self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben.
+self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden.
 
 [action]
 create_repo=hat das Repository <a href="%s">%s</a> erstellt
@@ -3391,6 +3497,7 @@ rpm.distros.suse=auf SUSE-basierten Distributionen
 rpm.install=Nutze folgenden Befehl, um das Paket zu installieren:
 rpm.repository=Repository-Informationen
 rpm.repository.architectures=Architekturen
+rpm.repository.multiple_groups=Dieses Paket ist in mehreren Gruppen verfügbar.
 rubygems.install=Um das Paket mit gem zu installieren, führe den folgenden Befehl aus:
 rubygems.install2=oder füg es zum Gemfile hinzu:
 rubygems.dependencies.runtime=Laufzeitabhängigkeiten
@@ -3516,12 +3623,18 @@ runs.commit=Commit
 runs.scheduled=Geplant
 runs.pushed_by=gepusht von
 runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s
+runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s
+runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten.
 runs.actor=Initiator
 runs.status=Status
 runs.actors_no_select=Alle Initiatoren
 runs.status_no_select=Alle Status
 runs.no_results=Keine passenden Ergebnisse gefunden.
+runs.no_workflows=Es gibt noch keine Workflows.
+runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">die Schnellstart-Anleitung</a>.
+runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der <a target="_blank" rel="noopener noreferrer" href="%s"> Dokumentation</a>.
 runs.no_runs=Der Workflow hat noch keine Ausführungen.
+runs.empty_commit_message=(leere Commit-Nachricht)
 
 workflow.disable=Workflow deaktivieren
 workflow.disable_success=Workflow '%s' erfolgreich deaktiviert.
@@ -3538,6 +3651,7 @@ variables.none=Es gibt noch keine Variablen.
 variables.deletion=Variable entfernen
 variables.deletion.description=Das Entfernen einer Variable ist dauerhaft und kann nicht rückgängig gemacht werden. Fortfahren?
 variables.description=Variablen werden an bestimmte Aktionen übergeben und können nicht anderweitig gelesen werden.
+variables.id_not_exist=Eine Variable mit ID %d existiert nicht.
 variables.edit=Variable bearbeiten
 variables.deletion.failed=Fehler beim Entfernen der Variable.
 variables.deletion.success=Die Variable wurde entfernt.
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 2662a49cea..1199d84581 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -141,6 +141,15 @@ confirm_delete_selected=Επιβεβαιώνετε τη διαγραφή όλω
 name=Όνομα
 value=Τιμή
 
+filter=Φίλτρο
+filter.is_archived=Αρχειοθετήθηκε
+filter.is_template=Πρότυπο
+filter.public=Δημόσιος
+filter.private=Ιδιωτικό
+
+
+[search]
+
 [aria]
 navbar=Γραμμή Πλοήγησης
 footer=Υποσέλιδο
@@ -314,7 +323,6 @@ collaborative_repos=Συνεργατικά Αποθετήρια
 my_orgs=Οι Οργανισμοί Μου
 my_mirrors=Τα Αντίγραφα Μου
 view_home=Προβολή %s
-search_repos=Βρείτε ένα αποθετήριο…
 filter=Άλλα Φίλτρα
 filter_by_team_repositories=Φιλτράρισμα ανά αποθετήρια ομάδας
 feed_of=`Τροφοδοσία του "%s"`
@@ -335,20 +343,8 @@ issues.in_your_repos=Στα αποθετήρια σας
 repos=Αποθετήρια
 users=Χρήστες
 organizations=Οργανισμοί
-search=Αναζήτηση
 go_to=Μετάβαση σε
 code=Κώδικας
-search.type.tooltip=Τύπος αναζήτησης
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Συμπερίληψη και των αποτελεσμάτων που είναι πλησιέστερα με τον όρο αναζήτησης
-search.match=Ταίριασμα
-search.match.tooltip=Συμπερίληψη μόνο των αποτελεσμάτων που ταιριάζουν ακριβώς με τον όρο αναζήτησης
-code_search_unavailable=Η αναζήτηση κώδικα δεν είναι διαθέσιμη αυτή τη στιγμή. Παρακαλώ επικοινωνήστε με το διαχειριστή.
-repo_no_results=Δεν βρέθηκαν αποθετήρια που να ταιρίαζουν με τα κριτήρια.
-user_no_results=Δεν βρέθηκαν χρήστες που να ταιριάζουν με τα κριτήρια.
-org_no_results=Δεν βρέθηκαν οργανισμοί που να ταιριάζουν με τα κριτήρια.
-code_no_results=Δεν βρέθηκε πηγαίος κώδικας που να ταιριάζει με τον όρο αναζήτησης.
-code_search_results=`Αποτελέσματα αναζήτησης για "%s"`
 code_last_indexed_at=Τελευταίο δημιουργία ευρετηρίου στις %s
 relevant_repositories_tooltip=Τα αποθετήρια που είναι forks ή που δεν έχουν θέμα, εικονίδιο και περιγραφή είναι κρυμμένα.
 relevant_repositories=Εμφανίζονται μόνο τα σχετικά αποθετήρια, <a href="%s">εμφάνιση χωρίς φίλτρο</a>.
@@ -366,7 +362,6 @@ forgot_password_title=Ξέχασα Τον Κωδικό Πρόσβασης
 forgot_password=Ξεχάσατε τον κωδικό πρόσβασης;
 sign_up_now=Χρειάζεστε λογαριασμό; Εγγραφείτε τώρα.
 sign_up_successful=Ο λογαριασμός δημιουργήθηκε επιτυχώς. Καλώς ορίσατε!
-confirmation_mail_sent_prompt=Ένα νέο email επιβεβαίωσης έχει σταλεί στο <b>%s</b>. Παρακαλώ ελέγξτε τα εισερχόμενα σας μέσα στις επόμενες %s για να ολοκληρώσετε τη διαδικασία εγγραφής.
 must_change_password=Ενημερώστε τον κωδικό πρόσβασης σας
 allow_password_change=Απαιτείται από το χρήστη να αλλάξει τον κωδικό πρόσβασης (συνιστόμενο)
 reset_password_mail_sent_prompt=Ένα email επιβεβαίωσης έχει σταλεί στο <b>%s</b>. Παρακαλώ ελέγξτε τα εισερχόμενα σας στις επόμενες %s για να ολοκληρώσετε τη διαδικασία ανάκτησης λογαριασμού.
@@ -614,6 +609,7 @@ form.name_reserved=Το όνομα χρήστη "%s" είναι δεσμευμέ
 form.name_pattern_not_allowed=Το μοτίβο "%s" δεν επιτρέπεται μέσα σε ένα όνομα χρήστη.
 form.name_chars_not_allowed=Το όνομα χρήστη "%s" περιέχει μη έγκυρους χαρακτήρες.
 
+
 [settings]
 profile=Προφίλ
 account=Λογαριασμός
@@ -758,7 +754,6 @@ gpg_invalid_token_signature=Το κλειδί GPG, η υπογραφή και τ
 gpg_token_required=Πρέπει να δώσετε μια υπογραφή για το παρακάτω διακριτικό
 gpg_token=Διακριτικό
 gpg_token_help=Μπορείτε να δημιουργήσετε μια υπογραφή χρησιμοποιώντας:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Θωρακισμένη υπογραφή GPG
 key_signature_gpg_placeholder=Ξεκινά με '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=Το κλειδί GPG "%s" επαληθεύτηκε.
@@ -952,7 +947,6 @@ fork_branch=Κλάδος που θα κλωνοποιηθεί στο fork
 all_branches=Όλοι οι κλάδοι
 fork_no_valid_owners=Αυτό το αποθετήριο δεν μπορεί να γίνει fork επειδή δεν υπάρχουν έγκυροι ιδιοκτήτες.
 use_template=Χρήση αυτού του πρότυπου
-clone_in_vsc=Κλωνοποίηση στο VS Code
 download_zip=Λήψη ZIP
 download_tar=Λήψη TAR.GZ
 download_bundle=Κατεβάστε Το ΔΕΜΑ
@@ -1271,9 +1265,7 @@ commits.desc=Δείτε το ιστορικό αλλαγών του πηγαίο
 commits.commits=Υποβολές
 commits.no_commits=Δεν υπάρχουν κοινές υποβολές. Τα "%s" και "%s" έχουν εντελώς διαφορετικές ιστορίες.
 commits.nothing_to_compare=Αυτοί οι κλάδοι είναι όμοιοι.
-commits.search=Αναζήτηση υποβολών…
 commits.search.tooltip=Μπορείτε να προθέτετε τις λέξεις-κλειδιά με "author:", "committer:", "after:", ή "before:", π.χ. "επαναφορά author:Alice before:2019-01-13".
-commits.find=Αναζήτηση
 commits.search_all=Όλοι Οι Κλάδοι
 commits.author=Συγγραφέας
 commits.message=Μήνυμα
@@ -1324,7 +1316,6 @@ projects.type.basic_kanban=Βασικό Kanban
 projects.type.bug_triage=Διαλογή Σφαλμάτων
 projects.template.desc=Πρότυπο έργου
 projects.template.desc_helper=Επιλέξτε ένα πρότυπο έργου για να ξεκινήσετε
-projects.type.uncategorized=Χωρίς Κατηγορία
 projects.column.edit=Επεξεργασία Στήλης
 projects.column.edit_title=Όνομα
 projects.column.new_title=Όνομα
@@ -1332,10 +1323,7 @@ projects.column.new_submit=Δημιουργία Στήλης
 projects.column.new=Νέα Στήλη
 projects.column.set_default=Ορισμός Προεπιλογής
 projects.column.set_default_desc=Ορίστε αυτή τη στήλη ως προεπιλογή για ζητήματα και pull requests χωρίς κατηγορία
-projects.column.unset_default=Αφαίρεση Προεπιλογής
-projects.column.unset_default_desc=Αφαίρεση της προεπιλογής αυτής της στήλης
 projects.column.delete=Διαγραφή Στήλης
-projects.column.deletion_desc=Η διαγραφή μιας στήλης έργου μετακινεί όλα τα συναφή ζητήματα σε 'Χωρίς Κατηγορία'. Συνέχεια;
 projects.column.color=Έγχρωμο
 projects.open=Άνοιγμα
 projects.close=Κλείσιμο
@@ -1447,7 +1435,6 @@ issues.filter_sort.moststars=Περισσότερα αστέρια
 issues.filter_sort.feweststars=Λιγότερα αστέρια
 issues.filter_sort.mostforks=Περισσότερα forks
 issues.filter_sort.fewestforks=Λιγότερα forks
-issues.keyword_search_unavailable=Η αναζήτηση μέσω λέξεων κλειδιών δεν είναι διαθέσιμη. Παρακαλώ επικοινωνήστε με το διαχειριστή.
 issues.action_open=Άνοιγμα
 issues.action_close=Κλείσιμο
 issues.action_label=Σήμα
@@ -1699,7 +1686,6 @@ pulls.compare_compare=τράβηγμα από
 pulls.switch_comparison_type=Αλλαγή τύπου σύγκρισης
 pulls.switch_head_and_base=Αλλαγή κεφαλής και βάσης
 pulls.filter_branch=Φιλτράρισμα κλάδου
-pulls.no_results=Δεν βρέθηκαν αποτελέσματα.
 pulls.show_all_commits=Εμφάνιση όλων των υποβολών
 pulls.show_changes_since_your_last_review=Εμφάνιση αλλαγών από την τελευταία αξιολόγηση
 pulls.showing_only_single_commit=Εμφάνιση μόνο αλλαγών της υποβολής %[1]s
@@ -1969,17 +1955,6 @@ activity.git_stats_deletion_n=%d διαγραφές
 
 contributors.contribution_type.commits=Υποβολές
 
-search=Αναζήτηση
-search.search_repo=Αναζήτηση αποθετηρίου
-search.type.tooltip=Τύπος αναζήτησης
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Συμπερίληψη και των αποτελεσμάτων που είναι πλησιέστερα με τον όρο αναζήτησης
-search.match=Ταίριασμα
-search.match.tooltip=Συμπερίληψη μόνο των αποτελεσμάτων που ταιριάζουν ακριβώς με τον όρο αναζήτησης
-search.results=Αποτελέσματα αναζήτησης για "%s" σε <a href="%s">%s</a>
-search.code_no_results=Δεν βρέθηκε πηγαίος κώδικας που να ταιριάζει με τον όρο αναζήτησης.
-search.code_search_unavailable=Η αναζήτηση κώδικα δεν είναι διαθέσιμη αυτή τη στιγμή. Παρακαλώ επικοινωνήστε με το διαχειριστή.
-
 settings=Ρυθμίσεις
 settings.desc=Στις Ρυθμίσεις μπορείτε να διαχειριστείτε τις ρυθμίσεις για το αποθετήριο
 settings.options=Αποθετήριο
@@ -2057,6 +2032,7 @@ settings.pulls.default_allow_edits_from_maintainers=Να επιτρέποντα
 settings.releases_desc=Ενεργοποίηση Κυκλοφοριών Αποθετηρίου
 settings.packages_desc=Ενεργοποίηση Μητρώου Πακέτων Αποθετηρίου
 settings.projects_desc=Ενεργοποίηση Έργων Αποθετηρίου
+settings.projects_mode_all=Όλα τα έργα
 settings.actions_desc=Ενεργοποίηση Δράσεων Αποθετηρίου
 settings.admin_settings=Ρυθμίσεις Διαχειριστή
 settings.admin_enable_health_check=Ενεργοποίηση Ελέγχων Υγείας του Αποθετηρίου (git fsck)
@@ -2131,7 +2107,6 @@ settings.delete_collaborator=Αφαίρεση
 settings.collaborator_deletion=Αφαίρεση Συνεργάτη
 settings.collaborator_deletion_desc=Η κατάργηση ενός συνεργάτη θα ανακαλέσει την πρόσβασή τους σε αυτό το αποθετήριο. Συνέχεια;
 settings.remove_collaborator_success=Ο συνεργάτης έχει αφαιρεθεί.
-settings.search_user_placeholder=Αναζήτηση χρήστη…
 settings.org_not_allowed_to_be_collaborator=Οι οργανισμοί δεν μπορούν να προστεθούν ως συνεργάτης.
 settings.change_team_access_not_allowed=Η αλλαγή της πρόσβασης ομάδας για το αποθετήριο έχει περιοριστεί στον ιδιοκτήτη του οργανισμού
 settings.team_not_in_organization=Η ομάδα δεν είναι στον ίδιο οργανισμό με το αποθετήριο
@@ -2139,7 +2114,6 @@ settings.teams=Ομάδες
 settings.add_team=Προσθήκη Ομάδας
 settings.add_team_duplicate=Η ομάδα έχει ήδη το αποθετήριο
 settings.add_team_success=Η ομάδα έχει πλέον πρόσβαση στο αποθετήριο.
-settings.search_team=Αναζήτηση Ομάδας…
 settings.change_team_permission_tip=Τα δικαιώματα της ομάδας έχουν οριστεί στη σελίδα ρυθμίσεων της ομάδας και δεν μπορούν να αλλάξουν ανά αποθετήριο
 settings.delete_team_tip=Αυτή η ομάδα έχει πρόσβαση σε όλα τα αποθετήρια και δεν μπορεί να αφαιρεθεί
 settings.remove_team_success=Έχει αφαιρεθεί η πρόσβαση της ομάδας στο αποθετήριο.
@@ -2292,9 +2266,7 @@ settings.protect_whitelist_committers=Περιορισμός του Push στη
 settings.protect_whitelist_committers_desc=Μόνο χρήστες ή ομάδες στη λίστα θα επιτρέπεται να κάνουν push σε αυτόν τον κλάδο (αλλά όχι να κάνουν force push).
 settings.protect_whitelist_deploy_keys=Έγκριση κλειδιών διάθεσης με πρόσβαση εγγραφής για ώθηση.
 settings.protect_whitelist_users=Λίστα χρηστών που επιτρέπεται να κάνουν push:
-settings.protect_whitelist_search_users=Αναζήτηση χρηστών…
 settings.protect_whitelist_teams=Λίστα ομάδων που επιτρέπεται να κάνουν push:
-settings.protect_whitelist_search_teams=Αναζήτηση ομάδων…
 settings.protect_merge_whitelist_committers=Ενεργοποίηση Λίστας Συγχώνευσης
 settings.protect_merge_whitelist_committers_desc=Επιτρέψτε μόνο σε χρήστες ή ομάδες στη λίστα να συγχωνεύσουν pull requests σε αυτό το κλάδο.
 settings.protect_merge_whitelist_users=Λίστα επιτρεπόμενων χρηστών για συγχώνευση:
@@ -2536,7 +2508,6 @@ branch.default_deletion_failed=Ο κλάδος "%s" είναι ο προεπιλ
 branch.restore=`Επαναφορά του Κλάδου "%s"`
 branch.download=`Λήψη του Κλάδου "%s"`
 branch.rename=`Μετονομασία Κλάδου "%s"`
-branch.search=Αναζήτηση Κλάδου
 branch.included_desc=Αυτός ο κλάδος είναι μέρος του προεπιλεγμένου κλάδου
 branch.included=Περιλαμβάνεται
 branch.create_new_branch=Δημιουργία κλάδου από κλάδο:
@@ -2674,7 +2645,6 @@ teams.write_permission_desc=Αυτή η ομάδα χορηγεί πρόσβασ
 teams.admin_permission_desc=Αυτή η ομάδα παρέχει πρόσβαση <strong>Διαχειριστή</strong>: τα μέλη μπορούν να διαβάσουν, να κάνουν push και να προσθέσουν συνεργάτες στα αποθετήρια της ομάδας.
 teams.create_repo_permission_desc=Επιπλέον, αυτή η ομάδα χορηγεί άδεια <strong>Δημιουργία αποθετηρίου</strong>: τα μέλη μπορούν να δημιουργήσουν νέα αποθετήρια στον οργανισμό.
 teams.repositories=Αποθετήρια Ομάδας
-teams.search_repo_placeholder=Αναζήτηση αποθετηρίου…
 teams.remove_all_repos_title=Αφαίρεση όλων των αποθετηρίων της ομάδας
 teams.remove_all_repos_desc=Αυτό θα αφαιρέσει όλα τα αποθετήρια από την ομάδα.
 teams.add_all_repos_title=Προσθήκη όλων των αποθετηρίων
@@ -2706,6 +2676,8 @@ integrations=Ενσωματώσεις
 authentication=Πηγές Ταυτοποίησης
 emails=Email Χρήστη
 config=Διαμόρφωση
+config_summary=Περίληψη
+config_settings=Ρυθμίσεις
 notices=Ειδοποιήσεις Συστήματος
 monitor=Παρακολούθηση
 first_page=Πρώτο
@@ -2880,9 +2852,6 @@ repos.unadopted.no_more=Δεν βρέθηκαν μη υιοθετημένα απ
 repos.owner=Ιδιοκτήτης
 repos.name=Όνομα
 repos.private=Ιδιωτικό
-repos.watches=Παρακολουθήσεις
-repos.stars=Αστέρια
-repos.forks=Forks
 repos.issues=Ζητήματα
 repos.size=Μέγεθος
 repos.lfs_size=Μέγεθος LFS
@@ -3007,7 +2976,6 @@ auths.tip.nextcloud=`Καταχωρήστε ένα νέο καταναλωτή O
 auths.tip.dropbox=Δημιουργήστε μια νέα εφαρμογή στο https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Καταχωρήστε μια νέα εφαρμογή στο https://developers.facebook.com/apps και προσθέστε το προϊόν "Facebook Login"`
 auths.tip.github=Καταχωρήστε μια νέα εφαρμογή OAuth στο https://github.com/settings/applications/new
-auths.tip.gitlab=Καταχωρήστε μια νέα εφαρμογή στο https://gitlab.com/profile/applications
 auths.tip.google_plus=Αποκτήστε τα διαπιστευτήρια πελάτη OAuth2 από την κονσόλα API της Google στο https://console.developers.google.com/
 auths.tip.openid_connect=Χρησιμοποιήστε το OpenID Connect Discovery URL (<server>/.well known/openid-configuration) για να καθορίσετε τα τελικά σημεία
 auths.tip.twitter=Πηγαίνετε στο https://dev.twitter.com/apps, δημιουργήστε μια εφαρμογή και βεβαιωθείτε ότι η επιλογή “Allow this application to be used to Sign in with Twitter” είναι ενεργοποιημένη
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index c013927157..ce50b71ec4 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -139,6 +139,15 @@ confirm_delete_selected=¿Borrar todos los elementos seleccionados?
 name=Nombre
 value=Valor
 
+filter=Filtro
+filter.is_archived=Archivado
+filter.is_template=Plantilla
+filter.public=Público
+filter.private=Privado
+
+
+[search]
+
 [aria]
 navbar=Barra de navegación
 footer=Pie
@@ -312,7 +321,6 @@ collaborative_repos=Repositorios colaborativos
 my_orgs=Mis organizaciones
 my_mirrors=Mis réplicas
 view_home=Ver %s
-search_repos=Buscar un repositorio…
 filter=Otros filtros
 filter_by_team_repositories=Filtrar por repositorios de equipo
 feed_of=`Suministro de noticias de "%s"`
@@ -333,20 +341,8 @@ issues.in_your_repos=En tus repositorios
 repos=Repositorios
 users=Usuarios
 organizations=Organizaciones
-search=Buscar
 go_to=Ir a
 code=Código
-search.type.tooltip=Tipo de búsqueda
-search.fuzzy=Parcial
-search.fuzzy.tooltip=Incluye los resultados que también coincidan con el término de búsqueda
-search.match=Coincidir
-search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto
-code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio.
-repo_no_results=No se ha encontrado ningún repositorio coincidente.
-user_no_results=No se ha encontrado ningún usuario coincidente.
-org_no_results=No se ha encontrado ninguna organización coincidente.
-code_no_results=No se ha encontrado código de fuente que coincida con su término de búsqueda.
-code_search_results=Resultados de búsqueda para «%s»
 code_last_indexed_at=Indexado por última vez %s
 relevant_repositories_tooltip=Repositorios que son bifurcaciones o que no tienen ningún tema, ningún icono, y ninguna descripción están ocultos.
 relevant_repositories=Solo se muestran repositorios relevantes, <a href="%s">mostrar resultados sin filtrar</a>.
@@ -363,7 +359,6 @@ forgot_password_title=He olvidado mi contraseña
 forgot_password=¿Has olvidado tu contraseña?
 sign_up_now=¿Necesitas una cuenta? Regístrate ahora.
 sign_up_successful=La cuenta se ha creado correctamente. ¡Bienvenido!
-confirmation_mail_sent_prompt=Un nuevo correo de confirmación se ha enviado a <b>%s</b>. Comprueba tu bandeja de entrada en las siguientes %s para completar el registro.
 must_change_password=Actualizar su contraseña
 allow_password_change=Obligar al usuario a cambiar la contraseña (recomendado)
 reset_password_mail_sent_prompt=Un correo de confirmación se ha enviado a <b>%s</b>. Compruebe su bandeja de entrada en las siguientes %s para completar el proceso de recuperación de la cuenta.
@@ -611,6 +606,7 @@ form.name_reserved=El nombre de usuario "%s" está reservado.
 form.name_pattern_not_allowed=El patrón "%s" no está permitido en un nombre de usuario.
 form.name_chars_not_allowed=El nombre de usuario "%s" contiene caracteres no válidos.
 
+
 [settings]
 profile=Perfil
 account=Cuenta
@@ -755,7 +751,6 @@ gpg_invalid_token_signature=La clave GPG proporcionada, la firma y el token no c
 gpg_token_required=Debe proporcionar una firma para el token de abajo
 gpg_token=Token
 gpg_token_help=Puede generar una firma de la siguiente manera:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Firma GPG armadura
 key_signature_gpg_placeholder=Comienza con '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=La clave GPG "%s" ha sido verificada.
@@ -945,7 +940,6 @@ fork_branch=Rama a clonar en la bifurcación
 all_branches=Todas las ramas
 fork_no_valid_owners=Este repositorio no puede ser bifurcado porque no hay propietarios válidos.
 use_template=Utilizar esta plantilla
-clone_in_vsc=Clonar en VS Code
 download_zip=Descargar ZIP
 download_tar=Descargar TAR.GZ
 download_bundle=Descargar BUNDLE
@@ -1264,9 +1258,7 @@ commits.desc=Ver el historial de cambios de código fuente.
 commits.commits=Commits
 commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes.
 commits.nothing_to_compare=Estas ramas son iguales.
-commits.search=Buscar commits…
 commits.search.tooltip=Puede prefijar palabras clave con "author:", "committer:", "after:", o "before:", p. ej., "revertir author:Alice before:2019-01-13".
-commits.find=Buscar
 commits.search_all=Todas las Ramas
 commits.author=Autor
 commits.message=Mensaje
@@ -1317,7 +1309,6 @@ projects.type.basic_kanban=Kanban básico
 projects.type.bug_triage=Prueba de error
 projects.template.desc=Plantilla del proyecto
 projects.template.desc_helper=Seleccione una plantilla de proyecto para empezar
-projects.type.uncategorized=Sin categorizar
 projects.column.edit=Editar columna
 projects.column.edit_title=Nombre
 projects.column.new_title=Nombre
@@ -1325,10 +1316,7 @@ projects.column.new_submit=Crear columna
 projects.column.new=Nueva columna
 projects.column.set_default=Establecer como predeterminado
 projects.column.set_default_desc=Establecer esta columna como predeterminada para incidencias no categorizadas y pulls
-projects.column.unset_default=Anular valor predeterminado
-projects.column.unset_default_desc=Anular esta columna como la predeterminada
 projects.column.delete=Borrar columna
-projects.column.deletion_desc=Eliminar una columna del proyecto mueve todos los problemas relacionados a 'Sin categorizar'. ¿Continuar?
 projects.column.color=Color
 projects.open=Abrir
 projects.close=Cerrar
@@ -1440,7 +1428,6 @@ issues.filter_sort.moststars=Mas estrellas
 issues.filter_sort.feweststars=Menor número de estrellas
 issues.filter_sort.mostforks=La mayoría de forks
 issues.filter_sort.fewestforks=Menor número de forks
-issues.keyword_search_unavailable=La búsqueda por palabra clave no está disponible actualmente. Por favor, contacte con el administrador de su sitio.
 issues.action_open=Abrir
 issues.action_close=Cerrar
 issues.action_label=Etiqueta
@@ -1692,7 +1679,6 @@ pulls.compare_compare=recuperar de
 pulls.switch_comparison_type=Cambiar tipo de comparación
 pulls.switch_head_and_base=Intercambiar cabeza y base
 pulls.filter_branch=Filtrar rama
-pulls.no_results=Sin resultados.
 pulls.show_all_commits=Mostrar todos los commits
 pulls.show_changes_since_your_last_review=Mostrar cambios desde tu última revisión
 pulls.showing_only_single_commit=Mostrando solo los cambios del commit %[1]s
@@ -1955,17 +1941,6 @@ activity.git_stats_deletion_n=%d eliminaciones
 
 contributors.contribution_type.commits=Commits
 
-search=Buscar
-search.search_repo=Buscar repositorio
-search.type.tooltip=Tipo de búsqueda
-search.fuzzy=Parcial
-search.fuzzy.tooltip=Incluye los resultados que también coinciden aproximadamente con el término de búsqueda
-search.match=Coincidir
-search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto
-search.results=Resultados de la búsqueda para "%s" en <a href="%s">%s</a>
-search.code_no_results=No se ha encontrado código de fuente que coincida con su término de búsqueda.
-search.code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio.
-
 settings=Configuración
 settings.desc=La configuración es donde puede administrar la configuración del repositorio
 settings.options=Repositorio
@@ -2043,6 +2018,7 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir ediciones de manten
 settings.releases_desc=Activar lanzamientos del repositorio
 settings.packages_desc=Habilitar registro de paquetes de repositorio
 settings.projects_desc=Activar Proyectos de Repositorio
+settings.projects_mode_all=Todos los proyectos
 settings.actions_desc=Activar Acciones del repositorio
 settings.admin_settings=Ajustes de administrador
 settings.admin_enable_health_check=Activar cheques de estado de salud del repositorio (git fsck)
@@ -2117,7 +2093,6 @@ settings.delete_collaborator=Eliminar
 settings.collaborator_deletion=Eliminar colaborador
 settings.collaborator_deletion_desc=Eliminar un colaborador revocará su acceso a este repositorio. ¿Continuar?
 settings.remove_collaborator_success=El colaborador ha sido eliminado.
-settings.search_user_placeholder=Buscar usuario…
 settings.org_not_allowed_to_be_collaborator=Las organizaciones no pueden ser añadidas como colaboradoras.
 settings.change_team_access_not_allowed=Cambiar el acceso del equipo al repositorio se ha restringido al propietario de la organización
 settings.team_not_in_organization=El equipo no pertenece a la misma organización que el repositorio
@@ -2125,7 +2100,6 @@ settings.teams=Equipos
 settings.add_team=Añadir equipo
 settings.add_team_duplicate=El equipo ya tiene acceso al repositorio
 settings.add_team_success=Ahora el equipo ya tiene acceso al repositorio.
-settings.search_team=Buscar equipos…
 settings.change_team_permission_tip=El permiso del equipo está establecido en la página de configuración del equipo y no puede ser cambiado por repositorio
 settings.delete_team_tip=Este equipo tiene acceso a todos los repositorios y no puede ser eliminado
 settings.remove_team_success=Se ha eliminado el acceso del equipo al repositorio.
@@ -2278,9 +2252,7 @@ settings.protect_whitelist_committers=Hacer push restringido a la lista blanca
 settings.protect_whitelist_committers_desc=Sólo se permitirá a los usuarios o equipos de la lista blanca hacer push a esta rama (pero no forzar push).
 settings.protect_whitelist_deploy_keys=Lista blanca de claves de despliegue con acceso de escritura a push.
 settings.protect_whitelist_users=Usuarios en la lista blanca para hacer push:
-settings.protect_whitelist_search_users=Buscar usuarios…
 settings.protect_whitelist_teams=Equipos en la lista blanca para hacer push:
-settings.protect_whitelist_search_teams=Buscar equipos…
 settings.protect_merge_whitelist_committers=Activar lista blanca para fusionar
 settings.protect_merge_whitelist_committers_desc=Permitir a los usuarios o equipos de la lista a fusionar peticiones pull dentro de esta rama.
 settings.protect_merge_whitelist_users=Usuarios en la lista blanca para fusionar:
@@ -2521,7 +2493,6 @@ branch.default_deletion_failed=La rama "%s" es la rama por defecto. No se puede
 branch.restore=`Restaurar rama "%s"`
 branch.download=`Descargar rama "%s"`
 branch.rename=`Renombrar rama "%s"`
-branch.search=Buscar rama
 branch.included_desc=Esta rama forma parte de la predeterminada
 branch.included=Incluida
 branch.create_new_branch=Crear rama desde la rama:
@@ -2659,7 +2630,6 @@ teams.write_permission_desc=Este equipo tiene permisos de <strong>Escritura</str
 teams.admin_permission_desc=Este equipo tiene permisos de <strong>Administración</strong>: los miembros pueden ver, hacer push y añadir colaboradores a los repositorios del equipo.
 teams.create_repo_permission_desc=Adicionalmente, este equipo concede permiso <strong>Crear repositorio</strong>: los miembros pueden crear nuevos repositorios en la organización.
 teams.repositories=Repositorios del equipo
-teams.search_repo_placeholder=Buscar repositorio…
 teams.remove_all_repos_title=Eliminar todos los repositorios del equipo
 teams.remove_all_repos_desc=Esto eliminará todos los repositorios del equipo.
 teams.add_all_repos_title=Añadir todos los repositorios
@@ -2691,6 +2661,8 @@ integrations=Integraciones
 authentication=Orígenes de autenticación
 emails=Correos de usuario
 config=Configuración
+config_summary=Resumen
+config_settings=Configuración
 notices=Notificaciones del sistema
 monitor=Monitorización
 first_page=Primera
@@ -2864,9 +2836,6 @@ repos.unadopted.no_more=No se encontraron más repositorios no adoptados
 repos.owner=Propietario
 repos.name=Nombre
 repos.private=Privado
-repos.watches=Vigilantes
-repos.stars=Estrellas
-repos.forks=Forks
 repos.issues=Incidencias
 repos.size=Tamaño
 repos.lfs_size=Tamaño LFS
@@ -2990,7 +2959,6 @@ auths.tip.nextcloud=`Registre un nuevo consumidor OAuth en su instancia usando e
 auths.tip.dropbox=Crear nueva aplicación en https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Registre una nueva aplicación en https://developers.facebook.com/apps y agregue el producto "Facebook Login"`
 auths.tip.github=Registre una nueva aplicación OAuth en https://github.com/settings/applications/new
-auths.tip.gitlab=Registrar nueva solicitud en https://gitlab.com/profile/applications
 auths.tip.google_plus=Obtener credenciales de cliente OAuth2 desde la consola API de Google en https://console.developers.google.com/
 auths.tip.openid_connect=Use el OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) para especificar los puntos finales
 auths.tip.twitter=Ir a https://dev.twitter.com/apps, crear una aplicación y asegurarse de que la opción "Permitir que esta aplicación sea usada para iniciar sesión con Twitter" está activada
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index d2db7a20e9..31122841a7 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -100,6 +100,15 @@ concept_user_organization=سازمان
 
 name=نام
 
+filter=فیلتر
+filter.is_archived=بایگانی شده
+filter.is_template=قالب
+filter.public=عمومی
+filter.private=خصوصی
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -233,7 +242,6 @@ collaborative_repos=مخازن همکاری
 my_orgs=سازمان های من
 my_mirrors=قرینه‌های من
 view_home=نمایش %s
-search_repos=یافتن مخزن…
 filter=فیلترهای دیگر
 filter_by_team_repositories=فیلتر کردن با مخازن تیم‌ها
 feed_of=`خوراک از "%s"`
@@ -254,14 +262,7 @@ issues.in_your_repos=در مخازن شما
 repos=مخازن
 users=کاربران
 organizations=سازمان ها
-search=جستجو
 code=کد
-search.fuzzy=نادقیق
-search.match=تطابق
-repo_no_results=مخزنی مطابق با این مورد یافت نشد.
-user_no_results=کاربری مطابق با این مورد یافت نشد.
-org_no_results=سازمانی مطابق با این مورد یافت نشد.
-code_no_results=کد منبعی مطابق با جستجوی شما یافت نشد.
 code_last_indexed_at=آخرین به روزرسانی در %s
 
 [auth]
@@ -274,7 +275,6 @@ remember_me=این دستگاه را بخاطر بسپار
 forgot_password_title=گذرواژه خود را فراموش کرده ام
 forgot_password=گذرواژه خود را فراموش کرده‌اید؟
 sign_up_now=نیاز به یک حساب دارید؟ هم‌اکنون ثبت نام کنید.
-confirmation_mail_sent_prompt=ایمیل تاییدیه جدیدی به <b>%s</b> ارسال شد. لطفا صندوق ورودی خود را در %d ساعت آینده برای تکمیل فرایند ثبت نام بررسی کنید.
 must_change_password=گذرواژه خود را به روز کنید
 allow_password_change=نیاز به کاربر برای تغییرگذرواژه (توصیه می شود)
 reset_password_mail_sent_prompt=ایمیل تاییدیه جدیدی به <b>%s</b> ارسال شد. لطفا صندوق ورودی خود را در %s آینده برای فرآیند بازیابی حساب کاربری خود بررسی کنید.
@@ -480,6 +480,7 @@ user_bio=زندگی‌نامه
 disabled_public_activity=این کاربر نمایش عمومی فعالیت های خود را غیرفعال کرده است.
 
 
+
 [settings]
 profile=نمایه
 account=حساب کاربری
@@ -591,7 +592,6 @@ gpg_invalid_token_signature=کلید GPG ارائه شده، امضا و ژتو
 gpg_token_required=باید یک امضا برای ژتون زیر ارائه کنید
 gpg_token=توکن
 gpg_token_help=با این میتوانید یک امضاء بسازید:
-gpg_token_code=‪echo "%s" | gpg -a --default-key %s --detach-sig‬
 gpg_token_signature=امضای GPG زره‌پوش
 key_signature_gpg_placeholder=با '-----BEGIN PGP SIGNATURE-----' شروع می‌شود
 ssh_key_verified=کلید تأیید شده
@@ -730,7 +730,6 @@ fork_repo=انشعاب از مخزن
 fork_from=انشعاب از
 fork_visibility_helper=نمایان بودن مخزن منشعب شده غیر قابل تغییر است.
 use_template=استفاده از این الگو
-clone_in_vsc=کلون کردن در VS Code
 download_zip=دانلود ZIP
 download_tar=دانلود TAR.GZ
 download_bundle=بارگیری باندل
@@ -972,8 +971,6 @@ editor.require_signed_commit=شاخه یک کامیت امضا شده لازم 
 commits.desc=تاریخچه تغییرات کد منبع را مرور کنید.
 commits.commits=کامیت‌ها
 commits.nothing_to_compare=این شاخه ها برابرند.
-commits.search=جست‌وجو کامیت‌ها…
-commits.find=جستجو
 commits.search_all=همه شاخه ها
 commits.author=مولف
 commits.message=پیام
@@ -1010,7 +1007,6 @@ projects.type.basic_kanban=پایه بر اساس سیستم کانبان (یک
 projects.type.bug_triage=اشکال Triage
 projects.template.desc=قالب پروژه
 projects.template.desc_helper=برای شروع یک قالب پروژه را انتخاب کنید
-projects.type.uncategorized=دسته‌بندی نشده
 projects.column.edit_title=نام
 projects.column.new_title=نام
 projects.column.color=رنگ
@@ -1301,7 +1297,6 @@ pulls.compare_compare=واکشی از
 pulls.switch_comparison_type=سوئیچ نوع مقایسه
 pulls.switch_head_and_base=سر و پایه سوئیچ
 pulls.filter_branch=صافی شاخه
-pulls.no_results=هیچ نتیجه‌ای یافت نشد.
 pulls.nothing_to_compare=این شاخه‎ها یکی هستند. نیازی به تقاضای واکشی نیست.
 pulls.nothing_to_compare_and_allow_empty_pr=این شاخه ها برابر هستند. این PR خالی خواهد بود.
 pulls.has_pull_request=`A درخواست pull بین این شاخه ها از قبل وجود دارد: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1501,13 +1496,6 @@ activity.git_stats_deletion_n=%d مذحوف
 
 contributors.contribution_type.commits=کامیت‌ها
 
-search=جستجو
-search.search_repo=جستجوی مخزن
-search.fuzzy=درهم
-search.match=مطابق
-search.results=نتیجه جستجو برای "%s" در <a href="%s">%s</a>
-search.code_no_results=کد منبعی مطابق با جستجوی شما یافت نشد.
-
 settings=تنظيمات
 settings.desc=تنظیمات جایی است که شما می‌توانید تنظیمات مخزن خود را مدیریت کنید
 settings.options=مخزن
@@ -1623,7 +1611,6 @@ settings.delete_collaborator=حذف
 settings.collaborator_deletion=حذف‌کردن همکار
 settings.collaborator_deletion_desc=حذف یک همکار از مخزن دسترسی‌های آنها را را مجدد لغو می‌کند. آیا ادامه می‌دهید؟
 settings.remove_collaborator_success=همكار حذف شد.
-settings.search_user_placeholder=جستجوی کاربر…
 settings.org_not_allowed_to_be_collaborator=سازمان ها را نمیتوان به عنوان همکار افزود.
 settings.change_team_access_not_allowed=تغییر دسترسی های تیم برای این مخزن توسط مالک ارگان محدود شده است
 settings.team_not_in_organization=تیم همانند ارگان برای این مخزن نیست
@@ -1631,7 +1618,6 @@ settings.teams=تیم ها
 settings.add_team=افزودن تیم
 settings.add_team_duplicate=تیم پیش از این مخزن داشته
 settings.add_team_success=تیم هم‌اکنون به مخزن دسترسی دارد.
-settings.search_team=جستجوی تیم…
 settings.change_team_permission_tip=دسترسی تیم در صفحه تنظیمات تیم انجام شده و برای هر مخزن نمی تواند تغییر یابد
 settings.delete_team_tip=این تیم به تمامی مخازن دسترسی دارد و نمی تواند حذف شود
 settings.remove_team_success=دسترسی تیم به مخزن حذف شد.
@@ -1748,9 +1734,7 @@ settings.protect_whitelist_committers=لیست سفید برای درج محدو
 settings.protect_whitelist_committers_desc=فقط به کاربران یا تیم‌های موجود لیست سفید برای درج در این شاخه اجازه خواهند داشت (اما نه درج اجباری).
 settings.protect_whitelist_deploy_keys=فهرست سفید کلیدهای استقرار با دسترسی نوشتن برای push کردن.
 settings.protect_whitelist_users=کاربران لیست سفید برای درج در مخزن:
-settings.protect_whitelist_search_users=جستجوی کاربر…
 settings.protect_whitelist_teams=تیم‌های لیست سفید برای درج در مخزن:
-settings.protect_whitelist_search_teams=جستجوی تیم ها…
 settings.protect_merge_whitelist_committers=فعال کردن لیست سفید ادغام
 settings.protect_merge_whitelist_committers_desc=اجازه به کاربران یا تیم‌های موجود لیست سفید برای تقاضا ادغام واکشی در این شاخه.
 settings.protect_merge_whitelist_users=کاربران لیست سفید برای ادغام:
@@ -2047,7 +2031,6 @@ teams.write_permission_desc=این تیم دسترسی <strong>نوشتن</stron
 teams.admin_permission_desc=این تیم دسترسی <strong>نوشتن</strong> خواهد داشت: اعضا خواهند توانست مخازن تیم را خوانده ، تغییراتی در آنها اعمال کرده و یا همکارانشان را به مخازن اضافه نمایند.
 teams.create_repo_permission_desc=علاوه بر این ، این تیم اجازه <strong> ساخت مخزن </strong> دسترسی : اعضا می توانند مخازن جدیدی را در سازمان ایجاد کنند.
 teams.repositories=مخازن تیم
-teams.search_repo_placeholder=جستجوی مخزن...
 teams.remove_all_repos_title=حذف تمام مخازن تیم
 teams.remove_all_repos_desc=با این کار همه مخازن از تیم حذف می شوند.
 teams.add_all_repos_title=افزودن همه مخازن
@@ -2072,6 +2055,8 @@ hooks=وب هوک ها
 authentication=منابع احراز هویت
 emails=ایمیل های کاربر
 config=پیکربندی
+config_summary=چکیده
+config_settings=تنظيمات
 notices=هشدارهای سامانه
 monitor=نظارت
 first_page=نخستین
@@ -2220,9 +2205,6 @@ repos.unadopted.no_more=هیچ مخزن تایید نشده دیگری یافت
 repos.owner=مالک
 repos.name=نام
 repos.private=خصوصی
-repos.watches=تماشا شده
-repos.stars=ستاره ها
-repos.forks=انشعاب‌ها
 repos.issues=مسائل
 repos.size=اندازه
 
@@ -2321,7 +2303,6 @@ auths.tip.nextcloud=با استفاده از منوی زیر "تنظیمات ->
 auths.tip.dropbox=یک برنامه جدید در https://www.dropbox.com/developers/apps بسازید
 auths.tip.facebook=`یک برنامه جدید در https://developers.facebook.com/apps بسازید برای ورود از طریق فیس بوک قسمت محصولات "Facebook Login"`
 auths.tip.github=یک برنامه OAuth جدید در https://github.com/settings/applications/new ثبت کنید
-auths.tip.gitlab=ثبت یک برنامه جدید در https://gitlab.com/profile/applications
 auths.tip.google_plus=اطلاعات مربوط به مشتری OAuth2 را از کلاینت API Google در https://console.developers.google.com/
 auths.tip.openid_connect=برای مشخص کردن نقاط پایانی از آدرس OpenID Connect Discovery URL (<server> /.well-known/openid-configuration) استفاده کنید.
 auths.tip.twitter=به https://dev.twitter.com/apps بروید ، برنامه ای ایجاد کنید و اطمینان حاصل کنید که گزینه "اجازه استفاده از این برنامه برای ورود به سیستم با Twitter" را فعال کنید
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index ab0dcc443d..00581f49fc 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -114,6 +114,15 @@ concept_user_organization=Organisaatio
 
 name=Nimi
 
+filter=Suodata
+filter.is_archived=Arkistoidut
+filter.is_template=Malli
+filter.public=Julkinen
+filter.private=Yksityinen
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -243,7 +252,6 @@ collaborative_repos=Yhteistyö repot
 my_orgs=Organisaationi
 my_mirrors=Peilini
 view_home=Näytä %s
-search_repos=Etsi repo…
 filter=Muut suodattimet
 filter_by_team_repositories=Suodata tiimin repojen mukaan
 feed_of=`Syöte "%s"`
@@ -264,13 +272,7 @@ issues.in_your_repos=Repoissasi
 repos=Repot
 users=Käyttäjät
 organizations=Organisaatiot
-search=Hae
 code=Koodi
-search.match=Osuma
-repo_no_results=Vastaavia repoja ei löydy.
-user_no_results=Vastaavia käyttäjiä ei löytynyt.
-org_no_results=Ei löytynyt vastaavia organisaatioita.
-code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt.
 code_last_indexed_at=Viimeksi indeksoitu %s
 
 [auth]
@@ -283,7 +285,6 @@ remember_me=Muista tämä laite
 forgot_password_title=Unohtuiko salasana
 forgot_password=Unohtuiko salasana?
 sign_up_now=Tarvitsetko tilin? Rekisteröidy nyt.
-confirmation_mail_sent_prompt=Uusi varmistussähköposti on lähetetty osoitteeseen <b>%s</b>, ole hyvä ja tarkista saapuneet seuraavan %s tunnin sisällä saadaksesi rekisteröintiprosessin valmiiksi.
 must_change_password=Vaihda salasanasi
 allow_password_change=Vaadi käyttäjää vaihtamaan salasanansa (suositeltava)
 reset_password_mail_sent_prompt=Varmistussähköposti on lähetetty osoitteeseen <b>%s</b>. Tarkista saapuneet seuraavan %s tunnin sisällä saadaksesi tilin palauttamisen valmiiksi.
@@ -440,6 +441,7 @@ unfollow=Lopeta seuraaminen
 user_bio=Elämäkerta
 
 
+
 [settings]
 profile=Profiili
 account=Tili
@@ -555,7 +557,6 @@ gpg_key_verify=Vahvista
 gpg_token_required=Sinun täytyy antaa allekirjoitus alla olevalle pääsymerkille
 gpg_token=Pääsymerkki
 gpg_token_help=Voit luoda allekirjoituksen käyttäen:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Panssaroitu GPG-allekirjoitus
 key_signature_gpg_placeholder=Alkaa sanoilla '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Vahvistettu avain
@@ -657,7 +658,6 @@ visibility_helper_forced=Sivuston ylläpitäjä pakottaa uudet repot olemaan yks
 fork_repo=Forkkaa repo
 fork_from=Forkkaa lähteestä
 fork_visibility_helper=Forkatun repon näkyvyyttä ei voi muuttaa.
-clone_in_vsc=Kloonaa VS Codessa
 download_zip=Lataa ZIP
 download_tar=Lataa TAR.GZ
 repo_desc=Kuvaus
@@ -779,7 +779,6 @@ editor.require_signed_commit=Haara vaatii vahvistetun commitin
 
 commits.commits=Commitit
 commits.nothing_to_compare=Nämä haarat vastaavat toisiaan.
-commits.find=Haku
 commits.search_all=Kaikki haarat
 commits.author=Tekijä
 commits.message=Viesti
@@ -806,7 +805,6 @@ projects.edit=Muokkaa projektia
 projects.modify=Päivitä projekti
 projects.type.basic_kanban=Yksinkertainen Kanban
 projects.template.desc=Malli
-projects.type.uncategorized=Luokittelematon
 projects.column.edit_title=Nimi
 projects.column.new_title=Nimi
 projects.open=Avaa
@@ -983,7 +981,6 @@ pulls.has_viewed_file=Katsottu
 pulls.viewed_files_label=%[1]d / %[2]d tiedostoa katsottu
 pulls.compare_compare=vedä kohteesta
 pulls.filter_branch=Suodata branch
-pulls.no_results=Tuloksia ei löytynyt.
 pulls.nothing_to_compare=Nämä haarat vastaavat toisiaan. Ei ole tarvetta luoda vetopyyntöä.
 pulls.nothing_to_compare_and_allow_empty_pr=Nämä haarat vastaavat toisiaan. Vetopyyntö tulee olemaan tyhjä.
 pulls.has_pull_request=`Vetopyyntö haarojen välillä on jo olemassa: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1077,10 +1074,6 @@ activity.git_stats_deletion_n=%d poistoa
 
 contributors.contribution_type.commits=Commitit
 
-search=Haku
-search.match=Osuma
-search.code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt.
-
 settings=Asetukset
 settings.options=Repo
 settings.collaboration.admin=Ylläpitäjä
@@ -1119,7 +1112,6 @@ settings.delete_desc=Repon poistaminen on pysyvä eikä voi peruuttaa.
 settings.delete_notices_1=- Tätä toimintoa <strong>EI VOI</strong> peruuttaa myöhemmin.
 settings.update_settings_success=Repon asetukset on päivitetty.
 settings.delete_collaborator=Poista
-settings.search_user_placeholder=Etsi käyttäjä…
 settings.teams=Tiimit
 settings.add_team=Lisää tiimi
 settings.add_webhook=Lisää webkoukku
@@ -1203,7 +1195,6 @@ settings.branch_protection=Haaran '<b>%s</b>' suojaus
 settings.protect_this_branch=Ota haaran suojaus käyttöön
 settings.protect_whitelist_deploy_keys=Lisää julkaisuavaimet sallittujen listalle mahdollistaaksesi repohin kirjoituksen.
 settings.protect_whitelist_users=Lista käyttäjistä joilla työntö oikeus:
-settings.protect_whitelist_search_users=Etsi käyttäjiä…
 settings.protect_merge_whitelist_committers_desc=Salli vain listaan merkittyjen käyttäjien ja tiimien yhdistää vetopyynnöt tähän haaraan.
 settings.protect_merge_whitelist_users=Lista käyttäjistä joilla yhdistämis-oikeus:
 settings.protect_required_approvals=Vaadittavat hyväksynnät:
@@ -1407,6 +1398,8 @@ repositories=Repot
 authentication=Todennuslähteet
 emails=Käyttäjien sähköpostit
 config=Asetukset
+config_summary=Yhteenveto
+config_settings=Asetukset
 notices=Järjestelmän ilmoitukset
 monitor=Valvonta
 first_page=Ensimmäinen
@@ -1508,9 +1501,6 @@ repos.repo_manage_panel=Repojen hallinta
 repos.owner=Omistaja
 repos.name=Nimi
 repos.private=Yksityinen
-repos.watches=Tarkkailijat
-repos.stars=Tähdet
-repos.forks=Haarat
 repos.issues=Ongelmat
 repos.size=Koko
 
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 20ef954cd2..062c818bd4 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -142,6 +142,15 @@ confirm_delete_selected=Êtes-vous sûr de vouloir supprimer tous les éléments
 name=Nom
 value=Valeur
 
+filter=Filtrer
+filter.is_archived=Archivé
+filter.is_template=Modèle
+filter.public=Public
+filter.private=Privé
+
+
+[search]
+
 [aria]
 navbar=Barre de navigation
 footer=Pied de page
@@ -315,7 +324,6 @@ collaborative_repos=Dépôts collaboratifs
 my_orgs=Mes organisations
 my_mirrors=Mes miroirs
 view_home=Voir %s
-search_repos=Trouver un dépôt …
 filter=Autres filtres
 filter_by_team_repositories=Dépôts filtrés par équipe
 feed_of=Flux de « %s »
@@ -336,20 +344,8 @@ issues.in_your_repos=Dans vos dépôts
 repos=Dépôts
 users=Utilisateurs
 organizations=Organisations
-search=Rechercher
 go_to=Atteindre
 code=Code
-search.type.tooltip=Type de recherche
-search.fuzzy=Approximative
-search.fuzzy.tooltip=Inclure également les résultats proches de la recherche
-search.match=Exacte
-search.match.tooltip=Inclure uniquement les résultats exacts
-code_search_unavailable=Actuellement, la recherche de code n'est pas disponible. Veuillez contacter l'administrateur de votre site.
-repo_no_results=Aucun dépôt correspondant n'a été trouvé.
-user_no_results=Aucun utilisateur correspondant n'a été trouvé.
-org_no_results=Aucune organisation correspondante n'a été trouvée.
-code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
-code_search_results=Résultats de la recherche pour « %s »
 code_last_indexed_at=Dernière indexation %s
 relevant_repositories_tooltip=Les dépôts qui sont des forks ou qui n'ont aucun sujet, aucune icône et aucune description sont cachés.
 relevant_repositories=Seuls les dépôts pertinents sont affichés, <a href="%s">afficher les résultats non filtrés</a>.
@@ -367,7 +363,6 @@ forgot_password_title=Mot de passe oublié
 forgot_password=Mot de passe oublié ?
 sign_up_now=Pas de compte ? Inscrivez-vous maintenant.
 sign_up_successful=Le compte a été créé avec succès. Bienvenue !
-confirmation_mail_sent_prompt=Un nouveau mail de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans les prochaines %s pour valider votre enregistrement.
 must_change_password=Réinitialisez votre mot de passe
 allow_password_change=Demande à l'utilisateur de changer son mot de passe (recommandé)
 reset_password_mail_sent_prompt=Un mail de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans les prochaines %s pour terminer la procédure de récupération du compte.
@@ -617,6 +612,7 @@ form.name_reserved=Le nom d’utilisateur "%s" est réservé.
 form.name_pattern_not_allowed=Le motif « %s » n’est pas autorisé dans un nom de d'utilisateur.
 form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides.
 
+
 [settings]
 profile=Profil
 account=Compte
@@ -761,7 +757,6 @@ gpg_invalid_token_signature=La clé GPG, la signature et le jeton fournis ne cor
 gpg_token_required=Vous devez fournir une signature pour le jeton ci-dessous
 gpg_token=Jeton
 gpg_token_help=Vous pouvez générer une signature en utilisant :
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Signature GPG renforcée
 key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=La clé GPG "%s" a été vérifiée.
@@ -955,7 +950,6 @@ fork_branch=Branche à cloner sur la bifurcation
 all_branches=Toutes les branches
 fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il n’a pas de propriétaire valide.
 use_template=Utiliser ce modèle
-clone_in_vsc=Cloner dans VS Code
 download_zip=Télécharger le ZIP
 download_tar=Télécharger le TAR.GZ
 download_bundle=Télécharger le BUNDLE
@@ -971,6 +965,8 @@ issue_labels_helper=Sélectionner un jeu de label.
 license=Licence
 license_helper=Sélectionner une licence
 license_helper_desc=Une licence réglemente ce que les autres peuvent ou ne peuvent pas faire avec votre code. Vous ne savez pas laquelle est la bonne pour votre projet ? Comment <a target="_blank" rel="noopener noreferrer" href="%s">choisir une licence</a>.
+object_format=Format d'objet
+object_format_helper=Format d’objet pour ce dépôt. Ne peut être modifié plus tard. SHA1 est le plus compatible.
 readme=LISEZMOI
 readme_helper=Choisissez un modèle de fichier LISEZMOI.
 readme_helper_desc=Le README est l'endroit idéal pour décrire votre projet et accueillir des contributeurs.
@@ -1278,9 +1274,7 @@ commits.desc=Naviguer dans l'historique des modifications.
 commits.commits=Révisions
 commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents.
 commits.nothing_to_compare=Ces branches sont égales.
-commits.search=Rechercher des révisions…
 commits.search.tooltip=Vous pouvez utiliser les mots-clés "author:", "committer:", "after:", ou "before:" pour filtrer votre recherche, ex.: "revert author:Alice before:2019-01-13".
-commits.find=Chercher
 commits.search_all=Toutes les branches
 commits.author=Auteur
 commits.message=Message
@@ -1331,7 +1325,6 @@ projects.type.basic_kanban=Kanban basique
 projects.type.bug_triage=Bug à trier
 projects.template.desc=Modèle de projet
 projects.template.desc_helper=Sélectionnez un modèle de projet pour débuter
-projects.type.uncategorized=Non catégorisé
 projects.column.edit=Modifier la colonne
 projects.column.edit_title=Nom
 projects.column.new_title=Nom
@@ -1339,10 +1332,7 @@ projects.column.new_submit=Créer une colonne
 projects.column.new=Nouvelle colonne
 projects.column.set_default=Définir par défaut
 projects.column.set_default_desc=Les tickets et demandes d’ajout non-catégorisés seront placés dans cette colonne.
-projects.column.unset_default=Défaire par défaut
-projects.column.unset_default_desc=Les tickets et demandes d'ajouts non-catégorisés seront placés dans une colonne idoine.
 projects.column.delete=Supprimer la colonne
-projects.column.deletion_desc=La suppression d'une colonne de projet déplace tous les tickets liés à 'Non catégorisé'. Continuer ?
 projects.column.color=Couleur
 projects.open=Ouvrir
 projects.close=Fermer
@@ -1454,7 +1444,6 @@ issues.filter_sort.moststars=Favoris (décroissant)
 issues.filter_sort.feweststars=Favoris (croissant)
 issues.filter_sort.mostforks=Bifurcations (décroissant)
 issues.filter_sort.fewestforks=Bifurcations (croissant)
-issues.keyword_search_unavailable=La recherche par mot clé n'est pas disponible. Veuillez contacter l'administrateur de votre instance Gitea.
 issues.action_open=Ouvrir
 issues.action_close=Fermer
 issues.action_label=Label
@@ -1706,7 +1695,6 @@ pulls.compare_compare=tirer les modifications depuis
 pulls.switch_comparison_type=Changer le type de comparaison
 pulls.switch_head_and_base=Passez de head à base
 pulls.filter_branch=Filtre de branche
-pulls.no_results=Aucun résultat trouvé.
 pulls.show_all_commits=Afficher toutes les révisions
 pulls.show_changes_since_your_last_review=Affiche les modifications depuis votre dernière évaluation.
 pulls.showing_only_single_commit=Affiche uniquement les changements de la révision %[1]s
@@ -1982,17 +1970,6 @@ contributors.contribution_type.commits=Révisions
 contributors.contribution_type.additions=Ajouts
 contributors.contribution_type.deletions=Suppressions
 
-search=Chercher
-search.search_repo=Rechercher dans le dépôt
-search.type.tooltip=Type de recherche
-search.fuzzy=Approximative
-search.fuzzy.tooltip=Inclure également les résultats proches de la recherche
-search.match=Exacte
-search.match.tooltip=Inclure uniquement les résultats exacts
-search.results=Résultats de la recherche « %s » dans <a href="%s"> %s</a>
-search.code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
-search.code_search_unavailable=Actuellement, la recherche de code n'est pas disponible. Veuillez contacter l'administrateur de votre site.
-
 settings=Paramètres
 settings.desc=Les paramètres sont l'endroit où gérer les options du dépôt
 settings.options=Dépôt
@@ -2019,6 +1996,7 @@ settings.mirror_settings.docs.doc_link_title=Comment mettre en miroir les dépô
 settings.mirror_settings.docs.doc_link_pull_section=la section « Pulling from a remote repository » de la documentation.
 settings.mirror_settings.docs.pulling_remote_title=Tirer depuis un dépôt distant
 settings.mirror_settings.mirrored_repository=Dépôt en miroir
+settings.mirror_settings.pushed_repository=Dépôt sortant
 settings.mirror_settings.direction=Direction
 settings.mirror_settings.direction.pull=Tirer
 settings.mirror_settings.direction.push=Soumission
@@ -2070,6 +2048,7 @@ settings.pulls.default_allow_edits_from_maintainers=Autoriser les modifications
 settings.releases_desc=Activer les publications du dépôt
 settings.packages_desc=Activer le registre des paquets du dépôt
 settings.projects_desc=Activer les projets de dépôt
+settings.projects_mode_all=Tous les projets
 settings.actions_desc=Activer les actions du dépôt
 settings.admin_settings=Paramètres administrateur
 settings.admin_enable_health_check=Activer les vérifications de santé du dépôt (git fsck)
@@ -2144,7 +2123,6 @@ settings.delete_collaborator=Supprimer
 settings.collaborator_deletion=Supprimer le collaborateur
 settings.collaborator_deletion_desc=La suppression d'un collaborateur révoque son accès à ce dépôt. Continuer ?
 settings.remove_collaborator_success=Le collaborateur a été retiré.
-settings.search_user_placeholder=Rechercher un utilisateur…
 settings.org_not_allowed_to_be_collaborator=Les organisations ne peuvent être ajoutées en tant que collaborateur.
 settings.change_team_access_not_allowed=La modification de l'accès de l'équipe au dépôt a été limitée au propriétaire de l'organisation
 settings.team_not_in_organization=L'équipe n'est pas dans la même organisation que le dépôt
@@ -2152,7 +2130,6 @@ settings.teams=Équipes
 settings.add_team=Ajouter une équipe
 settings.add_team_duplicate=L'équipe a déjà le dépôt
 settings.add_team_success=L'équipe a maintenant accès au dépôt.
-settings.search_team=Rechercher une équipe…
 settings.change_team_permission_tip=La permission de l'équipe est définie sur la page de configuration de l'équipe et ne peut pas être modifiée par dépôt
 settings.delete_team_tip=Cette équipe a accès à tous les dépôts et ne peut pas être supprimée
 settings.remove_team_success=L'accès de l'équipe au dépôt a été supprimé.
@@ -2305,9 +2282,7 @@ settings.protect_whitelist_committers=Liste blanche des soumissions
 settings.protect_whitelist_committers_desc=Seuls les utilisateurs ou les équipes autorisés pourront soumettre sur cette branche (sans forcer).
 settings.protect_whitelist_deploy_keys=Mettez les clés de déploiement sur liste blanche avec accès en écriture pour soumettre.
 settings.protect_whitelist_users=Utilisateurs sur liste blanche :
-settings.protect_whitelist_search_users=Rechercher des utilisateurs…
 settings.protect_whitelist_teams=Équipes sur liste blanche :
-settings.protect_whitelist_search_teams=Rechercher des équipes…
 settings.protect_merge_whitelist_committers=Activer la liste blanche pour la fusion
 settings.protect_merge_whitelist_committers_desc=N'autoriser que les utilisateurs et les équipes en liste blanche d'appliquer les demandes de fusion sur cette branche.
 settings.protect_merge_whitelist_users=Utilisateurs en liste blanche de fusion :
@@ -2552,7 +2527,6 @@ branch.default_deletion_failed=La branche "%s" est la branche par défaut. Elle
 branch.restore=`Restaurer la branche "%s"`
 branch.download=`Télécharger la branche "%s"`
 branch.rename=`Renommer la branche "%s"`
-branch.search=Rechercher une branche
 branch.included_desc=Cette branche fait partie de la branche par défaut
 branch.included=Incluses
 branch.create_new_branch=Créer une branche à partir de la branche :
@@ -2695,7 +2669,6 @@ teams.write_permission_desc=Cette équipe permet l'accès en <strong>écriture</
 teams.admin_permission_desc=Cette équipe permet l'accès <strong>administrateur</strong> : les membres peuvent voir, participer et ajouter des collaborateurs à ses dépôts.
 teams.create_repo_permission_desc=De plus, cette équipe accorde la permission <strong>Créer un dépôt</strong> : les membres peuvent créer de nouveaux dépôts dans l'organisation.
 teams.repositories=Dépôts de l'Équipe
-teams.search_repo_placeholder=Rechercher dans le dépôt…
 teams.remove_all_repos_title=Supprimer tous les dépôts de l'équipe
 teams.remove_all_repos_desc=Ceci supprimera tous les dépôts de l'équipe.
 teams.add_all_repos_title=Ajouter tous les dépôts
@@ -2728,6 +2701,8 @@ integrations=Intégrations
 authentication=Sources d'authentification
 emails=Emails de l'utilisateur
 config=Configuration
+config_summary=Résumé
+config_settings=Paramètres
 notices=Informations
 monitor=Surveillance
 first_page=Première
@@ -2904,9 +2879,6 @@ repos.unadopted.no_more=Aucun dépôt dépossédé trouvé.
 repos.owner=Propriétaire
 repos.name=Nom
 repos.private=Privé
-repos.watches=Suivi par
-repos.stars=Votes
-repos.forks=Bifurcations
 repos.issues=Tickets
 repos.size=Taille
 repos.lfs_size=Taille LFS
@@ -3031,7 +3003,6 @@ auths.tip.nextcloud=`Enregistrez un nouveau consommateur OAuth sur votre instanc
 auths.tip.dropbox=Créez une nouvelle application sur https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Enregistrez une nouvelle application sur https://developers.facebook.com/apps et ajoutez le produit "Facebook Login"`
 auths.tip.github=Créez une nouvelle application OAuth sur https://github.com/settings/applications/new
-auths.tip.gitlab=Créez une nouvelle application sur https://gitlab.com/profile/applications
 auths.tip.google_plus=Obtenez des identifiants OAuth2 sur la console API de Google (https://console.developers.google.com/)
 auths.tip.openid_connect=Utilisez l'URL de découvert OpenID (<server>/.well-known/openid-configuration) pour spécifier les points d'accès
 auths.tip.twitter=Rendez-vous sur https://dev.twitter.com/apps, créez une application et assurez-vous que l'option "Autoriser l'application à être utilisée avec Twitter Connect" est activée
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 901690d9a0..93e3b42115 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -90,6 +90,14 @@ concept_user_organization=Szervezet
 
 name=Név
 
+filter.is_archived=Archivált
+filter.is_template=Sablon
+filter.public=Nyilvános
+filter.private=Privát
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -207,7 +215,6 @@ collaborative_repos=Együttműködési tárolók
 my_orgs=Szervezeteim
 my_mirrors=Tükreim
 view_home=Nézet %s
-search_repos=Tároló keresés…
 
 show_archived=Archivált
 
@@ -222,12 +229,7 @@ issues.in_your_repos=A tárolóidban
 repos=Tárolók
 users=Felhasználók
 organizations=Szervezetek
-search=Keresés
 code=Kód
-repo_no_results=Nincs ilyen tároló.
-user_no_results=Nincs ilyen felhasználó.
-org_no_results=Nincs ilyen szervezet.
-code_no_results=Nincs találat a keresési kifejezésedre.
 code_last_indexed_at=Utoljára indexelve: %s
 
 [auth]
@@ -240,7 +242,6 @@ remember_me=Eszköz megjegyzése
 forgot_password_title=Elfelejtett jelszó
 forgot_password=Elfelejtette a jelszavát?
 sign_up_now=Szeretne bejelentkezni? Regisztráljon most.
-confirmation_mail_sent_prompt=Új megerősítő email lett küldve ide: <b>%s</b>. Ellenőrizze postafiókját az elkövetkező %s a regisztrációs folyamat befejezéséhez.
 must_change_password=Jelszó módosítása
 allow_password_change=A felhasználóknak meg kell változtatniuk a jelszavukat(ajánlott)
 reset_password_mail_sent_prompt=Megerősítő email lett küldve ide: <b>%s</b>. Ellenőrizze postafiókját az elkövetkező %s a jelszó visszaállítási folyamat befejezéséhez.
@@ -384,6 +385,7 @@ unfollow=Követés törlése
 user_bio=Életrajz
 
 
+
 [settings]
 profile=Profil
 account=Fiók
@@ -722,8 +724,6 @@ editor.no_changes_to_show=Nincsen megjeleníthető változás.
 editor.add_subdir=Mappa hozzáadása…
 
 commits.commits=Commit-ok
-commits.search=Commit-ok keresése…
-commits.find=Keresés
 commits.search_all=Minden ág
 commits.author=Szerző
 commits.message=Üzenet
@@ -929,7 +929,6 @@ pulls.compare_changes=Új egyesítési kérés
 pulls.compare_base=egyesítés ide
 pulls.compare_compare=egyesítés innen
 pulls.filter_branch=Ágra szűrés
-pulls.no_results=Nincs találat.
 pulls.nothing_to_compare=Ezek az ágak egyenlőek. Nincs szükség egyesítési kérésre.
 pulls.create=Egyesítési kérés létrehozása
 pulls.title_desc=egyesíteni szeretné %[1]d változás(oka)t a(z) <code>%[2]s</code>-ból <code id="branch_target">%[3]s</code>-ba
@@ -1056,11 +1055,6 @@ activity.git_stats_deletion_n=%d törlés
 
 contributors.contribution_type.commits=Commit-ok
 
-search=Keresés
-search.search_repo=Tároló keresés
-search.results=`"%s" találatok keresése itt: <a href="%s">%s</a>`
-search.code_no_results=Nincs találat a keresési kifejezésedre.
-
 settings=Beállítások
 settings.options=Tároló
 settings.collaboration.read=Olvasott
@@ -1102,8 +1096,6 @@ settings.branches=Ágak
 settings.protected_branch=Ág védeleme
 settings.protected_branch_can_push=Push engedélyezése?
 settings.protected_branch_can_push_yes=Most már push-olhatja
-settings.protect_whitelist_search_users=Felhasználó keresése…
-settings.protect_whitelist_search_teams=Csoportok keresése…
 settings.protect_check_status_contexts=Állapotellenőrzés engedélyezése
 settings.add_protected_branch=Védelem engedélyezése
 settings.delete_protected_branch=Védelem letiltása
@@ -1248,7 +1240,6 @@ teams.delete_team_desc=Egy csapat törlése visszavonja a tagjai hozzáférésé
 teams.delete_team_success=A csoport törölve lett.
 teams.read_permission_desc=Ez a csoport <strong>Olvasási</strong> jogosultságot biztosít: a tagok megtekinthetik és klónozhatják a csoport tárolóit.
 teams.repositories=Csoport tárolói
-teams.search_repo_placeholder=Tároló keresése…
 teams.remove_all_repos_title=Összes csapattároló eltávolítása
 teams.remove_all_repos_desc=Ez el fogja távolítani az összes tárolót a csoportból.
 teams.add_all_repos_title=Minden tároló hozzáadása
@@ -1266,6 +1257,8 @@ organizations=Szervezetek
 repositories=Tárolók
 authentication=Hitelesítési források
 config=Konfiguráció
+config_summary=Összefoglaló
+config_settings=Beállítások
 notices=Rendszer-értesítések
 monitor=Figyelés
 first_page=Első
@@ -1352,8 +1345,6 @@ repos.repo_manage_panel=Tárolók Kezelése
 repos.owner=Tulajdonos
 repos.name=Név
 repos.private=Privát
-repos.watches=Figyelők
-repos.stars=Csillagok
 repos.issues=Hibajegyek
 repos.size=Méret
 
@@ -1415,7 +1406,6 @@ auths.tip.bitbucket=Igényeljen egy új OAuth jogosultságot itt: https://bitbuc
 auths.tip.dropbox=Vegyen fel új alkalmazást itt: https://www.dropbox.com/developers/apps
 auths.tip.facebook=Vegyen fel új alkalmazást itt: https://developers.facebook.com/apps majd adja hozzá a "Facebook Login"-t
 auths.tip.github=Vegyen fel új OAuth alkalmazást itt: https://github.com/settings/applications/new
-auths.tip.gitlab=Vegyen fel új alkalmazást itt: https://gitlab.com/profile/applications
 auths.tip.google_plus=Szerezzen OAuth2 kliens hitelesítési adatokat a Google API konzolban (https://console.developers.google.com/)
 auths.tip.openid_connect=Használja az OpenID kapcsolódás felfedező URL-t (<kiszolgáló>/.well-known/openid-configuration) a végpontok beállításához
 auths.tip.twitter=Menyjen ide: https://dev.twitter.com/apps, hozzon létre egy alkalmazást és győződjön meg róla, hogy az “Allow this application to be used to Sign in with Twitter” opció be van kapcsolva
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 1aee871b67..ad7e0f4062 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -83,6 +83,12 @@ concept_code_repository=Repositori
 
 name=Nama
 
+filter.is_template=Contoh
+filter.private=Pribadi
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -134,7 +140,6 @@ collaborative_repos=Repositori Kolaboratif
 my_orgs=Organisasi Saya
 my_mirrors=Duplikat Saya
 view_home=Lihat %s
-search_repos=Cari repositori…
 
 
 show_private=Pribadi
@@ -145,12 +150,7 @@ issues.in_your_repos=Dalam repositori anda
 repos=Repositori
 users=Pengguna
 organizations=Organisasi
-search=Cari
 code=Kode
-repo_no_results=Tidak ditemukan repositori yang cocok.
-user_no_results=Tidak ditemukan pengguna yang cocok.
-org_no_results=Tidak ada organisasi yang cocok ditemukan.
-code_no_results=Tidak ada kode sumber yang cocok dengan istilah yang anda cari.
 
 [auth]
 create_new_account=Daftar Akun
@@ -161,7 +161,6 @@ disable_register_mail=Konfirmasi lewat email untuk pengguna baru dimatikan.
 forgot_password_title=Lupa Kata Sandi
 forgot_password=Lupa kata sandi?
 sign_up_now=Butuh akun? Daftar sekarang.
-confirmation_mail_sent_prompt=Surel konfirmasi baru telah dikirim ke <b>%s</b>. Silakan periksa kotak masuk anda dalam %s ke depan untuk menyelesaikan proses pendaftaran.
 must_change_password=Perbarui kata sandi Anda
 allow_password_change=Wajibkan pengguna untuk mengganti kata sandi (disarankan)
 reset_password_mail_sent_prompt=Surel konfirmasi berhasil dikirim ke <b>%s</b>. Silahkan cek akun email Anda dalam %s jam untuk menyelesaikan proses pemulihan akun.
@@ -307,6 +306,7 @@ unfollow=Berhenti Mengikuti
 user_bio=Biografi
 
 
+
 [settings]
 profile=Profil
 account=Akun
@@ -631,7 +631,6 @@ editor.cancel=Membatalkan
 editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan.
 
 commits.commits=Melakukan
-commits.find=Telusuri
 commits.author=Penulis
 commits.message=Pesan
 commits.date=Tanggal
@@ -749,7 +748,6 @@ issues.dependency.remove=Menghapus
 pulls.new=Permintaan Tarik Baru
 pulls.compare_changes=Permintaan Tarik Baru
 pulls.filter_branch=Penyaringan cabang
-pulls.no_results=Hasil tidak ditemukan.
 pulls.create=Buat Permintaan Tarik
 pulls.title_desc=ingin menggabungkan komit %[1]d dari <code>%[2]s</code> menuju <code id="branch_target">%[3]s</code>
 pulls.merged_title_desc=commit %[1]d telah digabungkan dari <code>%[2]s</code> menjadi <code>%[3]s</code> %[4]s
@@ -841,11 +839,6 @@ activity.published_release_label=Dikeluarkan
 
 contributors.contribution_type.commits=Melakukan
 
-search=Cari
-search.search_repo=Cari repositori
-search.results=Cari hasil untuk "%s" dalam <a href="%s">%s</a>
-search.code_no_results=Tidak ada kode sumber yang cocok dengan istilah yang anda cari.
-
 settings=Pengaturan
 settings.desc=Pengaturan dimana anda dapat mengelola pengaturan untuk repositori
 settings.options=Repositori
@@ -873,7 +866,6 @@ settings.transfer_owner=Pemilik Baru
 settings.delete=Menghapus Repositori Ini
 settings.delete_notices_1=- Operasi ini <strong>TIDAK BISA</strong> dibatalkan.
 settings.delete_collaborator=Menghapus
-settings.search_user_placeholder=Cari pengguna…
 settings.teams=Tim
 settings.add_webhook=Tambahkan Webhook
 settings.webhook.test_delivery=Percobaan Pengiriman
@@ -1008,13 +1000,13 @@ teams.update_settings=Memperbarui pengaturan
 teams.add_team_member=Tambahkan Anggota Tim
 teams.delete_team_success=Tim sudah di hapus.
 teams.repositories=Tim repositori
-teams.search_repo_placeholder=Cari repositori…
 
 [admin]
 dashboard=Dasbor
 organizations=Organisasi
 repositories=Repositori
 config=Konfigurasi
+config_settings=Pengaturan
 notices=Pemberitahuan Sistem
 monitor=Memantau
 first_page=Pertama
@@ -1077,8 +1069,6 @@ repos.repo_manage_panel=Manajemen Repositori
 repos.owner=Pemilik
 repos.name=Nama
 repos.private=Pribadi
-repos.watches=Jam tangan
-repos.stars=Bintang
 repos.issues=Masalah
 repos.size=Ukuran
 
@@ -1128,7 +1118,6 @@ auths.tip.oauth2_provider=Penyediaan OAuth2
 auths.tip.dropbox=Membuat aplikasi baru di https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Daftarkan sebuah aplikasi baru di https://developers.facebook.com/apps dan tambakan produk "Facebook Masuk"`
 auths.tip.github=Mendaftar aplikasi OAuth baru di https://github.com/settings/applications/new
-auths.tip.gitlab=Mendaftar aplikasi baru di https://gitlab.com/profile/applications
 auths.tip.openid_connect=Gunakan membuka ID yang terhubung ke jelajah URL (<server>/.well-known/openid-configuration) untuk menentukan titik akhir
 auths.delete=Menghapus Otentikasi Sumber
 auths.delete_auth_title=Menghapus Otentikasi Sumber
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index f67541fe73..3165c4185b 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -111,6 +111,14 @@ concept_code_repository=Hugbúnaðarsafn
 name=Heiti
 value=Gildi
 
+filter=Sía
+filter.is_archived=Safnvistað
+filter.is_template=Sniðmát
+filter.public=Opinbert
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -224,7 +232,6 @@ show_more_repos=Sýna fleiri hugbúnaðarsöfn…
 my_orgs=Stofnanir Mínar
 my_mirrors=Speglanir Mínar
 view_home=Skoða %s
-search_repos=Finna hugbúnaðarsafn…
 filter=Aðrar Síur
 
 show_archived=Safnvistað
@@ -239,14 +246,7 @@ issues.in_your_repos=Í hugbúnaðarsöfnum þínum
 repos=Hugbúnaðarsöfn
 users=Notendur
 organizations=Stofnanir
-search=Leita
 code=Kóði
-search.fuzzy=Óljóst
-code_search_unavailable=Sem stendur er kóðaleit ekki í boði. Vinsamlegast hafðu samband við síðustjórann þinn.
-repo_no_results=Engin samsvarandi hugbúnaðarsöfn fundust.
-user_no_results=Engir samsvarandi notendur fundust.
-org_no_results=Engar samsvarandi stofnanir fundust.
-code_no_results=Enginn samsvarandi frumkóði fannst eftur þínum leitarorðum.
 
 [auth]
 create_new_account=Skrá Notanda
@@ -418,6 +418,7 @@ user_bio=Lífssaga
 disabled_public_activity=Þessi notandi hefur slökkt á opinberum sýnileika virkninnar.
 
 
+
 [settings]
 profile=Notandasíða
 account=Reikningur
@@ -704,7 +705,6 @@ editor.cancel=Hætta við
 editor.fail_to_update_file_summary=Villuskilaboð:
 
 commits.commits=Framlög
-commits.find=Leita
 commits.author=Höfundur
 commits.message=Skilaboð
 commits.date=Dagsetning
@@ -728,7 +728,6 @@ projects.edit=Breyta Verkefnum
 projects.modify=Uppfæra Verkefni
 projects.type.none=Ekkert
 projects.template.desc=Sniðmát
-projects.type.uncategorized=Óflokkuð
 projects.column.edit_title=Heiti
 projects.column.new_title=Heiti
 projects.column.color=Litað
@@ -992,11 +991,6 @@ activity.git_stats_deletion_n=%d eyðingar
 
 contributors.contribution_type.commits=Framlög
 
-search=Leita
-search.fuzzy=Óljóst
-search.code_no_results=Enginn samsvarandi frumkóði fannst eftur þínum leitarorðum.
-search.code_search_unavailable=Sem stendur er kóðaleit ekki í boði. Vinsamlegast hafðu samband við síðustjórann þinn.
-
 settings=Stillingar
 settings.options=Hugbúnaðarsafn
 settings.collaboration.write=Skrifa
@@ -1163,6 +1157,8 @@ teams.all_repositories=Öll hugbúnaðarsöfn
 [admin]
 repositories=Hugbúnaðarsöfn
 config=Stilling
+config_summary=Yfirlit
+config_settings=Stillingar
 first_page=Byrjun
 last_page=Síðasta
 total=Samtals: %d
@@ -1201,9 +1197,6 @@ orgs.members=Meðlimar
 
 repos.owner=Eigandi
 repos.name=Heiti
-repos.watches=Fylgist með
-repos.stars=Eftirlæti
-repos.forks=Skiptingar
 repos.issues=Vandamál
 repos.size=Stærð
 
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index 0e38c1ffb9..cc379e8109 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -116,6 +116,15 @@ concept_user_organization=Organizzazione
 name=Nome
 value=Valore
 
+filter=Filtro
+filter.is_archived=Archiviato
+filter.is_template=Template
+filter.public=Pubblico
+filter.private=Privati
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -254,7 +263,6 @@ collaborative_repos=Repository Condivisi
 my_orgs=Le mie Organizzazioni
 my_mirrors=I miei Mirror
 view_home=Vedi %s
-search_repos=Trova un repository…
 filter=Altro filtri
 filter_by_team_repositories=Filtra per repository del team
 feed_of=`Feed di "%s"`
@@ -275,15 +283,7 @@ issues.in_your_repos=Nei tuoi repository
 repos=Repository
 users=Utenti
 organizations=Organizzazioni
-search=Cerca
 code=Codice
-search.fuzzy=Fuzzy
-search.match=Corrispondenze
-code_search_unavailable=Attualmente la ricerca di codice non è disponibile. Contatta l'amministratore del sito.
-repo_no_results=Nessuna repository corrispondente.
-user_no_results=Nessun utente corrispondente.
-org_no_results=Nessun'organizzazione corrispondente trovata.
-code_no_results=Nessun codice sorgente corrispondente ai termini di ricerca.
 code_last_indexed_at=Ultimo indicizzato %s
 
 [auth]
@@ -297,7 +297,6 @@ remember_me=Ricorda questo dispositivo
 forgot_password_title=Password Dimenticata
 forgot_password=Password dimenticata?
 sign_up_now=Hai bisogno di un account? Registrati adesso.
-confirmation_mail_sent_prompt=Una nuova email di conferma è stata inviata a <b>%s</b>. Per favore controlla la tua posta in arrivo nelle prossime %s per completare il processo di registrazione.
 must_change_password=Aggiorna la tua password
 allow_password_change=Richiede all'utente di cambiare la password (scelta consigliata)
 reset_password_mail_sent_prompt=Una email di conferma è stata inviata a <b>%s</b>. Per favore controlla la tua posta in arrivo nelle prossime %s per completare il processo di reset della password.
@@ -507,6 +506,7 @@ user_bio=Biografia
 disabled_public_activity=L'utente ha disabilitato la vista pubblica dell'attività.
 
 
+
 [settings]
 profile=Profilo
 account=Account
@@ -634,7 +634,6 @@ gpg_invalid_token_signature=La chiave GPG fornita, la firma e il token non corri
 gpg_token_required=Devi fornire una firma per il token sottostante
 gpg_token=Token
 gpg_token_help=È possibile generare una firma utilizzando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Firma GPG corazzata
 key_signature_gpg_placeholder=Comincia con '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Chiave Verificata
@@ -787,7 +786,6 @@ already_forked=Hai già fatto il fork di %s
 fork_to_different_account=Fai Fork a un account diverso
 fork_visibility_helper=La visibilità di un repository forkato non può essere modificata.
 use_template=Usa questo modello
-clone_in_vsc=Clona nel codice VS
 download_zip=Scarica ZIP
 download_tar=Scarica TAR.GZ
 download_bundle=Scarica BUNDLE
@@ -1051,8 +1049,6 @@ editor.revert=Ripristina %s su:
 commits.desc=Sfoglia la cronologia di modifiche del codice rogente.
 commits.commits=Commit
 commits.nothing_to_compare=Questi rami sono uguali.
-commits.search=Ricerca commits…
-commits.find=Cerca
 commits.search_all=Tutti i branch
 commits.author=Autore
 commits.message=Messaggio
@@ -1097,7 +1093,6 @@ projects.type.basic_kanban=Basic Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Template di progetto
 projects.template.desc_helper=Seleziona un modello di progetto per iniziare
-projects.type.uncategorized=Senza categoria
 projects.column.edit_title=Nome
 projects.column.new_title=Nome
 projects.column.color=Colore
@@ -1409,7 +1404,6 @@ pulls.compare_compare=esegui un pull da
 pulls.switch_comparison_type=Cambia tipo di confronto
 pulls.switch_head_and_base=Testa e base di commutazione
 pulls.filter_branch=Filtra branch
-pulls.no_results=Nessun risultato trovato.
 pulls.nothing_to_compare=Questi rami sono uguali. Non c'è alcuna necessità di creare una pull request.
 pulls.nothing_to_compare_and_allow_empty_pr=Questi rami sono uguali. Questa PR sarà vuota.
 pulls.has_pull_request=`Una pull request tra questi rami esiste già: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1626,14 +1620,6 @@ activity.git_stats_deletion_n=%d cancellazioni
 
 contributors.contribution_type.commits=Commit
 
-search=Ricerca
-search.search_repo=Ricerca repository
-search.fuzzy=Fuzzy
-search.match=Corrispondenze
-search.results=Risultati della ricerca per "%s" in <a href="%s">%s</a>
-search.code_no_results=Nessun codice sorgente corrispondente al termine di ricerca trovato.
-search.code_search_unavailable=Attualmente la ricerca di codice non è disponibile. Contatta l'amministratore del sito.
-
 settings=Impostazioni
 settings.desc=Impostazioni ti permette di gestire le impostazioni del repository
 settings.options=Repository
@@ -1760,7 +1746,6 @@ settings.delete_collaborator=Rimuovi
 settings.collaborator_deletion=Rimuovi collaboratore
 settings.collaborator_deletion_desc=Rimuovere un collaboratore revocherà l'accesso a questo repository. Continuare?
 settings.remove_collaborator_success=Il collaboratore è stato rimosso.
-settings.search_user_placeholder=Ricerca utente…
 settings.org_not_allowed_to_be_collaborator=Le organizzazioni non possono essere aggiunte come un collaboratore.
 settings.change_team_access_not_allowed=La modifica dell'accesso al team per il repository è stato limitato al solo proprietario dell'organizzazione
 settings.team_not_in_organization=Il team non è nella stessa organizzazione del repository
@@ -1768,7 +1753,6 @@ settings.teams=Gruppi
 settings.add_team=Aggiungi Squadra
 settings.add_team_duplicate=Il team ha già il repository
 settings.add_team_success=Il team ha ora accesso al repository.
-settings.search_team=Cerca Squadra…
 settings.change_team_permission_tip=Il permesso del team è impostato sulla pagina delle impostazioni del team e non può essere modificato per repository
 settings.delete_team_tip=Questo team ha accesso a tutte le repository e non può essere rimosso
 settings.remove_team_success=L'accesso del team al repository è stato rimosso.
@@ -1907,9 +1891,7 @@ settings.protect_whitelist_committers=Lista bianch push ristretti
 settings.protect_whitelist_committers_desc=Solo gli utenti o i team nella whitelist potranno pushare su questo ramo (ma non forzare il push).
 settings.protect_whitelist_deploy_keys=Chiavi di deploy in whitelist con permessi di scrittura per il push.
 settings.protect_whitelist_users=Utenti nella whitelist per pushare:
-settings.protect_whitelist_search_users=Cerca utenti…
 settings.protect_whitelist_teams=Team nella whitelist per pushare:
-settings.protect_whitelist_search_teams=Ricerca team…
 settings.protect_merge_whitelist_committers=Attiva la whitelist per i merge
 settings.protect_merge_whitelist_committers_desc=Consentire soltanto agli utenti o ai team in whitelist il permesso di unire le pull request di questo branch.
 settings.protect_merge_whitelist_users=Utenti nella whitelist per il merging:
@@ -2218,7 +2200,6 @@ teams.write_permission_desc=Questo team concede l'accesso di <strong>Scrittura</
 teams.admin_permission_desc=Questo team concede l'accesso di <strong>Amministratore</strong>: i membri possono leggere da, pushare su e aggiungere collaboratori ai repository del team.
 teams.create_repo_permission_desc=Inoltre, questo team concede il permesso di <strong>Creare repository</strong>: i membri possono creare nuove repository nell'organizzazione.
 teams.repositories=Repository di Squadra
-teams.search_repo_placeholder=Ricerca repository…
 teams.remove_all_repos_title=Rimuovi tutti i repository del team
 teams.remove_all_repos_desc=Questo rimuoverà tutte le repository dal team.
 teams.add_all_repos_title=Aggiungi tutti i repository
@@ -2243,6 +2224,8 @@ hooks=Webhooks
 authentication=Fonti di autenticazione
 emails=Email Utente
 config=Configurazione
+config_summary=Riepilogo
+config_settings=Impostazioni
 notices=Avvisi di sistema
 monitor=Monitoraggio
 first_page=Prima
@@ -2397,9 +2380,6 @@ repos.unadopted.no_more=Nessun repository non adottato trovato
 repos.owner=Proprietario
 repos.name=Nome
 repos.private=Privati
-repos.watches=Segue
-repos.stars=Voti
-repos.forks=Fork
 repos.issues=Problemi
 repos.size=Dimensione
 
@@ -2515,7 +2495,6 @@ auths.tip.nextcloud=`Registra un nuovo OAuth sulla tua istanza utilizzando il se
 auths.tip.dropbox=Crea una nuova applicazione su https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Registra una nuova applicazione su https://developers.facebook.com/apps e aggiungi il prodotto "Facebook Login"`
 auths.tip.github=Registra una nuova applicazione OAuth su https://github.com/settings/applications/new
-auths.tip.gitlab=Registra una nuova applicazione su https://gitlab.com/profile/applications
 auths.tip.google_plus=Ottieni le credenziali del client OAuth2 dalla console API di Google su https://console.developers.google.com/
 auths.tip.openid_connect=Utilizza l'OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) per specificare gli endpoint
 auths.tip.twitter=Vai su https://dev.twitter.com/apps, crea una applicazione e assicurati che l'opzione "Allow this application to be used to Sign In with Twitter" sia abilitata
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index af06a78642..d5c2885f00 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -142,6 +142,15 @@ confirm_delete_selected=選択したすべてのアイテムを削除してよ
 name=名称
 value=値
 
+filter=フィルター
+filter.is_archived=アーカイブ
+filter.is_template=テンプレート
+filter.public=公開
+filter.private=プライベート
+
+
+[search]
+
 [aria]
 navbar=ナビゲーションバー
 footer=フッター
@@ -315,7 +324,6 @@ collaborative_repos=共同リポジトリ
 my_orgs=自分の組織
 my_mirrors=自分のミラー
 view_home=%s を表示
-search_repos=リポジトリを探す…
 filter=その他のフィルター
 filter_by_team_repositories=チームリポジトリで絞り込み
 feed_of=`"%s" のフィード`
@@ -336,20 +344,8 @@ issues.in_your_repos=あなたのリポジトリ
 repos=リポジトリ
 users=ユーザー
 organizations=組織
-search=検索
 go_to=開く
 code=コード
-search.type.tooltip=検索タイプ
-search.fuzzy=あいまい
-search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます
-search.match=一致
-search.match.tooltip=検索ワードに一致する結果だけを含めます
-code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
-repo_no_results=一致するリポジトリが見つかりません。
-user_no_results=一致するユーザーが見つかりません。
-org_no_results=一致する組織が見つかりません。
-code_no_results=検索ワードに一致するソースコードが見つかりません。
-code_search_results=`"%s" の検索結果`
 code_last_indexed_at=最終取得 %s
 relevant_repositories_tooltip=フォークリポジトリや、トピック、アイコン、説明のいずれも無いリポジトリは表示されません。
 relevant_repositories=妥当と思われるリポジトリのみを表示しています。 <a href="%s">フィルタリングしない結果を表示</a>。
@@ -367,7 +363,6 @@ forgot_password_title=パスワードを忘れた
 forgot_password=パスワードをお忘れですか?
 sign_up_now=アカウントが必要ですか? 今すぐ登録しましょう。
 sign_up_successful=アカウントは無事に作成されました。ようこそ!
-confirmation_mail_sent_prompt=<b>%s</b> に確認メールを送信しました。 %s以内に受信トレイを確認し、登録手続きを完了してください。
 must_change_password=パスワードの更新
 allow_password_change=ユーザーはパスワードの変更が必要 (推奨)
 reset_password_mail_sent_prompt=<b>%s</b> に確認メールを送信しました。 %s以内に受信トレイを確認し、アカウント回復手続きを完了してください。
@@ -617,6 +612,13 @@ form.name_reserved=ユーザー名 "%s" は予約されています。
 form.name_pattern_not_allowed=`"%s" の形式はユーザー名に使用できません。`
 form.name_chars_not_allowed=ユーザー名 "%s" には無効な文字が含まれています。
 
+block.block=ブロック
+block.block.user=ユーザーをブロック
+block.block.org=組織向けにユーザーをブロック
+block.block.failure=ユーザーのブロックに失敗しました: %s
+block.unblock=ブロックを解除
+block.unblock.failure=ユーザーのブロック解除に失敗しました: %s
+
 [settings]
 profile=プロフィール
 account=アカウント
@@ -761,7 +763,6 @@ gpg_invalid_token_signature=入力されたGPG鍵、署名、トークンが合
 gpg_token_required=以下のトークンの署名を入力する必要があります
 gpg_token=トークン
 gpg_token_help=署名はこの方法で生成できます:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Armor形式のGPG署名
 key_signature_gpg_placeholder=先頭は '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=GPG鍵 "%s" を確認しました。
@@ -955,7 +956,6 @@ fork_branch=フォークにクローンされるブランチ
 all_branches=すべてのブランチ
 fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
 use_template=このテンプレートを使用
-clone_in_vsc=VSCodeでクローン
 download_zip=ZIPファイルをダウンロード
 download_tar=TAR.GZファイルをダウンロード
 download_bundle=バンドルをダウンロード
@@ -990,6 +990,7 @@ mirror_prune=Prune
 mirror_prune_desc=不要になった古いリモートトラッキング参照を削除
 mirror_interval=ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 定期的な同期を無効にする場合は0。(最小間隔: %s)
 mirror_interval_invalid=ミラー間隔が不正です。
+mirror_sync=前回の同期
 mirror_sync_on_commit=コミットがプッシュされたときに同期
 mirror_address=クローンするURL
 mirror_address_desc=必要な資格情報は「認証」セクションに設定してください。
@@ -1191,6 +1192,8 @@ audio_not_supported_in_browser=このブラウザーはHTML5のaudioタグをサ
 stored_lfs=Git LFSで保管されています
 symbolic_link=シンボリック リンク
 executable_file=実行ファイル
+vendored=ベンダーファイル
+generated=生成ファイル
 commit_graph=コミットグラフ
 commit_graph.select=ブランチを選択
 commit_graph.hide_pr_refs=プルリクエストを非表示
@@ -1277,9 +1280,7 @@ commits.desc=ソースコードの変更履歴を参照します。
 commits.commits=コミット
 commits.no_commits=共通のコミットはありません。 "%s" と "%s" の履歴はすべて異なっています。
 commits.nothing_to_compare=二つのブランチは同じ内容です。
-commits.search=コミットの検索…
 commits.search.tooltip=`キーワード "author:"、"committer:"、"after:"、"before:" を付けて指定できます。 例 "revert author:Alice before:2019-01-13"`
-commits.find=検索
 commits.search_all=すべてのブランチ
 commits.author=作成者
 commits.message=メッセージ
@@ -1330,7 +1331,6 @@ projects.type.basic_kanban=基本的なカンバン
 projects.type.bug_triage=バグ トリアージ
 projects.template.desc=テンプレート
 projects.template.desc_helper=開始するプロジェクトテンプレートを選択
-projects.type.uncategorized=未分類
 projects.column.edit=列を編集
 projects.column.edit_title=名称
 projects.column.new_title=名称
@@ -1338,10 +1338,7 @@ projects.column.new_submit=列を作成
 projects.column.new=新しい列
 projects.column.set_default=デフォルトに設定
 projects.column.set_default_desc=この列を未分類のイシューやプルリクエストが入るデフォルトの列にします
-projects.column.unset_default=デフォルトを解除
-projects.column.unset_default_desc=この列からデフォルト列の設定を解除します
 projects.column.delete=列を削除
-projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューが '未分類' に移動します。 続行しますか?
 projects.column.color=カラー
 projects.open=オープン
 projects.close=クローズ
@@ -1453,7 +1450,6 @@ issues.filter_sort.moststars=スターが多い順
 issues.filter_sort.feweststars=スターが少ない順
 issues.filter_sort.mostforks=フォークが多い順
 issues.filter_sort.fewestforks=フォークが少ない順
-issues.keyword_search_unavailable=現在キーワード検索は利用できません。 サイト管理者にお問い合わせください。
 issues.action_open=オープン
 issues.action_close=クローズ
 issues.action_label=ラベル
@@ -1705,7 +1701,6 @@ pulls.compare_compare=プル元
 pulls.switch_comparison_type=比較の種類を切り替える
 pulls.switch_head_and_base=ヘッドとベースを切り替える
 pulls.filter_branch=ブランチの絞り込み
-pulls.no_results=結果が見つかりませんでした。
 pulls.show_all_commits=すべてのコミットを表示
 pulls.show_changes_since_your_last_review=前回の自分のレビューからの変更を表示
 pulls.showing_only_single_commit=コミット %[1]s の変更だけを表示しています
@@ -1714,6 +1709,7 @@ pulls.select_commit_hold_shift_for_range=コミットを選択。シフトを押
 pulls.review_only_possible_for_full_diff=すべての差分を表示しているときだけレビューが可能です
 pulls.filter_changes_by_commit=コミットで絞り込み
 pulls.nothing_to_compare=同じブランチ同士のため、 プルリクエストを作成する必要がありません。
+pulls.nothing_to_compare_have_tag=選択したブランチ/タグは同一のものです。
 pulls.nothing_to_compare_and_allow_empty_pr=これらのブランチは内容が同じです。 空のプルリクエストになります。
 pulls.has_pull_request=`同じブランチのプルリクエストはすでに存在します: <a href="%[1]s">%[2]s#%[3]d</a>`
 pulls.create=プルリクエストを作成
@@ -1772,6 +1768,7 @@ pulls.merge_pull_request=マージコミットを作成
 pulls.rebase_merge_pull_request=リベース後にファストフォワード
 pulls.rebase_merge_commit_pull_request=リベース後にマージコミット作成
 pulls.squash_merge_pull_request=スカッシュコミットを作成
+pulls.fast_forward_only_merge_pull_request=ファストフォワードのみ
 pulls.merge_manually=手動マージ済みにする
 pulls.merge_commit_id=マージコミットID
 pulls.require_signed_wont_sign=ブランチでは署名されたコミットが必須ですが、このマージでは署名がされません
@@ -1908,6 +1905,8 @@ wiki.page_name_desc=この Wiki ページの名前を入力してください。
 wiki.original_git_entry_tooltip=フレンドリーリンクを使用する代わりにオリジナルのGitファイルを表示します。
 
 activity=アクティビティ
+activity.navbar.pulse=Pulse
+activity.navbar.contributors=貢献者
 activity.period.filter_label=期間:
 activity.period.daily=1日
 activity.period.halfweekly=3日
@@ -1973,18 +1972,10 @@ activity.git_stats_and_deletions=、
 activity.git_stats_deletion_1=%d行削除
 activity.git_stats_deletion_n=%d行削除
 
+contributors.contribution_type.filter_label=実績タイプ:
 contributors.contribution_type.commits=コミット
-
-search=検索
-search.search_repo=リポジトリを検索
-search.type.tooltip=検索タイプ
-search.fuzzy=あいまい
-search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます
-search.match=一致
-search.match.tooltip=検索ワードに一致する結果だけを含めます
-search.results=<a href="%[2]s">%[3]s</a> 内での "%[1]s" の検索結果
-search.code_no_results=検索ワードに一致するソースコードが見つかりません。
-search.code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
+contributors.contribution_type.additions=追加
+contributors.contribution_type.deletions=削除
 
 settings=設定
 settings.desc=設定では、リポジトリの設定を管理することができます。
@@ -2064,6 +2055,7 @@ settings.pulls.default_allow_edits_from_maintainers=デフォルトでメンテ
 settings.releases_desc=リリースを有効にする
 settings.packages_desc=リポジトリパッケージレジストリを有効にする
 settings.projects_desc=リポジトリプロジェクトを有効にする
+settings.projects_mode_all=すべてのプロジェクト
 settings.actions_desc=Actionsを有効にする
 settings.admin_settings=管理者用設定
 settings.admin_enable_health_check=リポジトリのヘルスチェックを有効にする (git fsck)
@@ -2138,7 +2130,6 @@ settings.delete_collaborator=削除
 settings.collaborator_deletion=共同作業者の削除
 settings.collaborator_deletion_desc=共同作業者を削除し、このリポジトリへのアクセス権を取り消します。 続行しますか?
 settings.remove_collaborator_success=共同作業者を削除しました。
-settings.search_user_placeholder=ユーザーを検索…
 settings.org_not_allowed_to_be_collaborator=組織を共同作業者として追加することはできません。
 settings.change_team_access_not_allowed=リポジトリに対するチームアクセス権の変更は、組織のオーナーのみに制限されています。
 settings.team_not_in_organization=チームがリポジトリと同じ組織に属していません。
@@ -2146,7 +2137,6 @@ settings.teams=チーム
 settings.add_team=チームを追加
 settings.add_team_duplicate=チームにはすでにこのリポジトリが登録されています。
 settings.add_team_success=チームがこのリポジトリにアクセスできるようになりました。
-settings.search_team=チームを検索…
 settings.change_team_permission_tip=チームの権限はチーム設定ページで設定されており、リポジトリごとに変更することはできません
 settings.delete_team_tip=このチームはすべてのリポジトリにアクセスでき、削除できません
 settings.remove_team_success=チームのこのリポジトリへのアクセス権を削除しました。
@@ -2299,9 +2289,7 @@ settings.protect_whitelist_committers=ホワイトリストでプッシュを制
 settings.protect_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにのみ、このブランチへのプッシュが許可されます。(強制プッシュ以外)
 settings.protect_whitelist_deploy_keys=プッシュ可能な書き込み権限を持つデプロイキーをホワイトリストに含める。
 settings.protect_whitelist_users=プッシュ・ホワイトリストに含むユーザー:
-settings.protect_whitelist_search_users=ユーザーを検索…
 settings.protect_whitelist_teams=プッシュ・ホワイトリストに含むチーム:
-settings.protect_whitelist_search_teams=チームを検索…
 settings.protect_merge_whitelist_committers=マージ・ホワイトリストを有効にする
 settings.protect_merge_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにだけ、このブランチに対するプルリクエストのマージを許可します。
 settings.protect_merge_whitelist_users=マージ・ホワイトリストに含むユーザー:
@@ -2322,6 +2310,8 @@ settings.protect_approvals_whitelist_users=ホワイトリストに含めるレ
 settings.protect_approvals_whitelist_teams=ホワイトリストに含めるレビューチーム:
 settings.dismiss_stale_approvals=古くなった承認を取り消す
 settings.dismiss_stale_approvals_desc=プルリクエストの内容を変える新たなコミットがブランチにプッシュされた場合、以前の承認を取り消します。
+settings.ignore_stale_approvals=古くなった承認を無視する
+settings.ignore_stale_approvals_desc=古いコミットに対して行われた承認 (古いレビュー) を、PRの承認数にカウントしません。 古いレビューが取り消される場合は関係ありません。
 settings.require_signed_commits=コミット署名必須
 settings.require_signed_commits_desc=署名されていない場合、または署名が検証できなかった場合は、このブランチへのプッシュを拒否します。
 settings.protect_branch_name_pattern=保護ブランチ名のパターン
@@ -2377,6 +2367,7 @@ settings.archive.error=リポジトリのアーカイブ設定でエラーが発
 settings.archive.error_ismirror=ミラーのリポジトリはアーカイブできません。
 settings.archive.branchsettings_unavailable=ブランチ設定は、アーカイブリポジトリでは使用できません。
 settings.archive.tagsettings_unavailable=タグ設定は、アーカイブリポジトリでは使用できません。
+settings.archive.mirrors_unavailable=リポジトリがアーカイブされている場合、ミラーは利用できません。
 settings.unarchive.button=アーカイブ解除
 settings.unarchive.header=このリポジトリをアーカイブ解除
 settings.unarchive.text=リポジトリのアーカイブを解除すると、コミット、プッシュ、新規のイシューやプルリクエストを受け付ける機能が復活します。
@@ -2543,7 +2534,6 @@ branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチ
 branch.restore=ブランチ "%s" の復元
 branch.download=ブランチ "%s" をダウンロード
 branch.rename=ブランチ名 "%s" を変更
-branch.search=ブランチを検索
 branch.included_desc=このブランチはデフォルトブランチに含まれています
 branch.included=埋没
 branch.create_new_branch=このブランチをもとに作成します:
@@ -2576,6 +2566,11 @@ error.csv.unexpected=このファイルは %d 行目の %d 文字目に予期し
 error.csv.invalid_field_count=このファイルは %d 行目のフィールドの数が正しくないため表示できません。
 
 [graphs]
+component_loading=%sを読み込み中...
+component_loading_failed=%sを読み込めませんでした
+component_loading_info=少し時間がかかるかもしれません…
+component_failed_to_load=予期しないエラーが発生しました。
+contributors.what=実績
 
 [org]
 org_name_holder=組織名
@@ -2681,7 +2676,6 @@ teams.write_permission_desc=このチームは<strong>書き込み</strong>ア
 teams.admin_permission_desc=このチームは<strong>管理者</strong>アクセス権を持ちます: メンバーはチームリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。
 teams.create_repo_permission_desc=さらに、このチームには<strong>リポジトリの作成</strong>権限が与えられています: メンバーは組織のリポジトリを新たに作成できます。
 teams.repositories=チームのリポジトリ
-teams.search_repo_placeholder=リポジトリを検索…
 teams.remove_all_repos_title=チームリポジトリをすべて除去
 teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。
 teams.add_all_repos_title=すべてのリポジトリを追加
@@ -2703,6 +2697,7 @@ teams.invite.description=下のボタンをクリックしてチームに参加
 
 [admin]
 dashboard=ダッシュボード
+self_check=セルフチェック
 identity_access=アイデンティティとアクセス
 users=ユーザーアカウント
 organizations=組織
@@ -2713,6 +2708,8 @@ integrations=連携
 authentication=認証ソース
 emails=ユーザーメールアドレス
 config=設定
+config_summary=サマリー
+config_settings=設定
 notices=システム通知
 monitor=モニタリング
 first_page=最初
@@ -2748,6 +2745,7 @@ dashboard.delete_missing_repos=Gitファイルが存在しないリポジトリ
 dashboard.delete_missing_repos.started=Gitファイルが存在しないリポジトリをすべて削除するタスクを開始しました。
 dashboard.delete_generated_repository_avatars=自動生成したリポジトリアバターを削除
 dashboard.sync_repo_branches=Gitデータからデータベースへ不足しているブランチを同期
+dashboard.sync_repo_tags=Gitデータからデータベースへタグを同期
 dashboard.update_mirrors=ミラーの更新
 dashboard.repo_health_check=全リポジトリのヘルスチェック
 dashboard.check_repo_stats=全リポジトリの統計情報を更新
@@ -2802,6 +2800,7 @@ dashboard.stop_endless_tasks=終わらないタスクを停止
 dashboard.cancel_abandoned_jobs=放置されたままのジョブをキャンセル
 dashboard.start_schedule_tasks=スケジュールタスクを開始
 dashboard.sync_branch.started=ブランチの同期を開始しました
+dashboard.sync_tag.started=タグの同期を開始しました
 dashboard.rebuild_issue_indexer=イシューインデクサーの再構築
 
 users.user_manage_panel=ユーザーアカウント管理
@@ -2887,9 +2886,6 @@ repos.unadopted.no_more=未登録のリポジトリはありません
 repos.owner=オーナー
 repos.name=名称
 repos.private=プライベート
-repos.watches=ウォッチ
-repos.stars=スター
-repos.forks=フォーク
 repos.issues=イシュー
 repos.size=サイズ
 repos.lfs_size=LFSサイズ
@@ -3014,7 +3010,6 @@ auths.tip.nextcloud=新しいOAuthコンシューマーを、インスタンス
 auths.tip.dropbox=新しいアプリケーションを https://www.dropbox.com/developers/apps から登録してください。
 auths.tip.facebook=新しいアプリケーションを https://developers.facebook.com/apps で登録し、"Facebook Login"を追加してください。
 auths.tip.github=新しいOAuthアプリケーションを https://github.com/settings/applications/new から登録してください。
-auths.tip.gitlab=新しいアプリケーションを https://gitlab.com/profile/applications から登録してください。
 auths.tip.google_plus=OAuth2クライアント資格情報を、Google APIコンソール https://console.developers.google.com/ から取得してください。
 auths.tip.openid_connect=OpenID Connect DiscoveryのURL (<server>/.well-known/openid-configuration) をエンドポイントとして指定してください
 auths.tip.twitter=https://dev.twitter.com/apps へアクセスしてアプリケーションを作成し、“Allow this application to be used to Sign in with Twitter”オプションを有効にしてください。
@@ -3228,6 +3223,12 @@ notices.desc=説明
 notices.op=操作
 notices.delete_success=システム通知を削除しました。
 
+self_check.no_problem_found=今のところ問題は見つかっていません。
+self_check.database_collation_mismatch=データベースに想定される照合順序: %s
+self_check.database_collation_case_insensitive=データベースは照合順序 %s を使用しており、大文字小文字を区別しません。 Giteaはその照合順序でも動作するかもしれませんが、まれに期待どおり動作しないケースがあるかもしれません。
+self_check.database_inconsistent_collation_columns=データベースは照合順序 %s を使用していますが、以下のカラムはそれと一致しない照合順序を使用しており、予期せぬ問題を引き起こす可能性があります。
+self_check.database_fix_mysql=MySQL/MariaDBユーザーの方は、"gitea doctor convert" コマンドを使用することで、照合順序の問題を修正できます。 また、"ALTER ... COLLATE ..." のSQLを手で実行しても修正することができます。
+self_check.database_fix_mssql=MSSQLユーザーの方は、問題を修正するには今のところ "ALTER ... COLLATE ..." のSQLを手で実行するしかありません。
 
 [action]
 create_repo=がリポジトリ <a href="%s">%s</a> を作成しました
@@ -3415,6 +3416,7 @@ rpm.distros.suse=SUSE系ディストリビューションの場合
 rpm.install=パッケージをインストールするには、次のコマンドを実行します:
 rpm.repository=リポジトリ情報
 rpm.repository.architectures=Architectures
+rpm.repository.multiple_groups=このパッケージは複数のグループで利用可能です。
 rubygems.install=gem を使用してパッケージをインストールするには、次のコマンドを実行します:
 rubygems.install2=または Gemfile に追加します:
 rubygems.dependencies.runtime=実行用依存関係
@@ -3567,6 +3569,7 @@ variables.none=変数はまだありません。
 variables.deletion=変数を削除
 variables.deletion.description=変数の削除は恒久的で元に戻すことはできません。 続行しますか?
 variables.description=変数は特定のActionsに渡されます。 それ以外で読み出されることはありません。
+variables.id_not_exist=IDが%dの変数は存在しません。
 variables.edit=変数の編集
 variables.deletion.failed=変数を削除できませんでした。
 variables.deletion.success=変数を削除しました。
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index ed0bb897c4..3e9679575c 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -84,6 +84,12 @@ concept_user_organization=조직
 
 name=이름
 
+filter.is_template=템플릿
+filter.private=비공개
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -199,7 +205,6 @@ collaborative_repos=협업 저장소
 my_orgs=내 조직
 my_mirrors=내 미러 저장소들
 view_home=%s 보기
-search_repos=저장소 찾기..
 
 
 show_private=비공개
@@ -210,12 +215,7 @@ issues.in_your_repos=당신의 저장소에
 repos=저장소
 users=유저
 organizations=조직
-search=검색
 code=코드
-repo_no_results=일치하는 레포지토리가 없습니다.
-user_no_results=일치하는 사용자가 없습니다.
-org_no_results=일치하는 조직이 없습니다.
-code_no_results=검색어와 일치하는 소스코드가 없습니다.
 
 [auth]
 create_new_account=계정 등록
@@ -226,7 +226,6 @@ disable_register_mail=계정 등록을 위한 이메일 검증이 비활성화 
 forgot_password_title=비밀번호 찾기
 forgot_password=비밀번호를 잊으셨나요?
 sign_up_now=계정이 필요하신가요? 지금 가입하세요.
-confirmation_mail_sent_prompt=새로운 확인 메일이 <b>%s</b>로 전송되었습니다. 받은 편지함으로 도착한 메일을 %s 안에 확인해서 등록 절차를 완료하십시오.
 must_change_password=비밀번호를 변경하세요.
 allow_password_change=사용자에게 비밀번호 변경을 요청 (권장됨)
 reset_password_mail_sent_prompt=확인 메일이 <b>%s</b>로 전송되었습니다. 받은 편지함으로 도착한 메일을 %s 안에 확인해서 비밀번호 찾기 절차를 완료하십시오.
@@ -363,6 +362,7 @@ unfollow=추적해제
 user_bio=소개
 
 
+
 [settings]
 profile=프로필
 account=계정
@@ -661,8 +661,6 @@ editor.add_subdir=경로 추가...
 
 commits.desc=소스 코드 변경 내역 탐색
 commits.commits=커밋
-commits.search=커밋 찾기...
-commits.find=검색
 commits.search_all=모든 브랜치
 commits.author=작성자
 commits.message=메시지
@@ -844,7 +842,6 @@ pulls.compare_changes=새 풀 리퀘스트
 pulls.compare_base=병합하기
 pulls.compare_compare=다음으로부터 풀
 pulls.filter_branch=Filter Branch
-pulls.no_results=결과 없음
 pulls.create=풀 리퀘스트 생성
 pulls.title_desc="<code>%[2]s</code> 에서 <code id=\"branch_target\">%[3]s</code> 로 %[1]d commits 를 머지하려 합니다"
 pulls.merged_title_desc=<code>%[2]s</code> 에서 <code>%[3]s</code> 로 %[1]d commits 를 머지했습니다 %[4]s
@@ -952,11 +949,6 @@ activity.published_release_label=배포됨
 
 contributors.contribution_type.commits=커밋
 
-search=검색
-search.search_repo=저장소 검색
-search.results="<a href=\"%s\">%s</a> 에서 \"%s\" 에 대한 검색 결과"
-search.code_no_results=검색어와 일치하는 소스코드가 없습니다.
-
 settings=설정
 settings.desc=설정은 저장소 설정을 관리할 수 있습니다.
 settings.options=저장소
@@ -1016,7 +1008,6 @@ settings.add_collaborator=새 공동작업자 추가
 settings.add_collaborator_success=공동작업자가 추가 되었습니다.
 settings.delete_collaborator=제거
 settings.collaborator_deletion=공동작업자 삭제
-settings.search_user_placeholder=사용자 검색...
 settings.teams=팀
 settings.add_webhook=Webhook 추가
 settings.webhook_deletion=Webhook 삭제
@@ -1090,8 +1081,6 @@ settings.branch_protection='<b>%s</b>' 브랜치 보호
 settings.protect_this_branch=브랜치 보호 활성화
 settings.protect_disable_push=푸시 끄기
 settings.protect_enable_push=푸시 켜기
-settings.protect_whitelist_search_users=사용자 찾기...
-settings.protect_whitelist_search_teams=팀 찾기...
 settings.protect_merge_whitelist_committers=머지 화이트리스트 활성화
 settings.protect_required_approvals=필요한 승인:
 settings.protect_approvals_whitelist_users=화이트리스트된 리뷰어:
@@ -1226,7 +1215,6 @@ teams.add_team_member=팀 구성원 추가
 teams.delete_team_title=팀 삭제
 teams.delete_team_success=팀이 삭제되었습니다.
 teams.repositories=팀 저장소
-teams.search_repo_placeholder=저장소 찾기...
 teams.add_duplicate_users=사용자가 이미 팀 멤버입니다.
 teams.members.none=이 팀에 멤버가 없습니다.
 
@@ -1237,6 +1225,8 @@ organizations=조직
 repositories=저장소
 authentication=인증 소스
 config=설정
+config_summary=요약
+config_settings=설정
 notices=시스템 공지
 monitor=모니터링
 first_page=처음
@@ -1323,9 +1313,6 @@ repos.repo_manage_panel=저장소 관리
 repos.owner=소유자
 repos.name=이름
 repos.private=비공개
-repos.watches=지켜보기
-repos.stars=별
-repos.forks=포크
 repos.issues=이슈
 repos.size=크기
 
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 3c3513ad48..0a2729980b 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -141,6 +141,15 @@ confirm_delete_selected=Apstiprināt, lai izdzēstu visus atlasītos vienumus?
 name=Nosaukums
 value=Vērtība
 
+filter=Filtrs
+filter.is_archived=Arhivētie
+filter.is_template=Sagatave
+filter.public=Publiska
+filter.private=Privāts
+
+
+[search]
+
 [aria]
 navbar=Navigācijas josla
 footer=Kājene
@@ -314,7 +323,6 @@ collaborative_repos=Sadarbības repozitoriji
 my_orgs=Manas organizācijas
 my_mirrors=Mani spoguļi
 view_home=Skatīties %s
-search_repos=Meklēt repozitoriju…
 filter=Citi filtri
 filter_by_team_repositories=Filtrēt pēc komandas repozitorijiem
 feed_of=`"%s" plūsma`
@@ -335,20 +343,8 @@ issues.in_your_repos=Jūsu repozitorijos
 repos=Repozitoriji
 users=Lietotāji
 organizations=Organizācijas
-search=Meklēt
 go_to=Iet uz
 code=Kods
-search.type.tooltip=Meklēšanas veids
-search.fuzzy=Aptuveni
-search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības
-search.match=Precīzi
-search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības
-code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru.
-repo_no_results=Netika atrasts neviens repozitorijs, kas atbilstu kritērijiem.
-user_no_results=Netika atrasts neviens lietotājs, kas atbilstu kritērijiem.
-org_no_results=Netika atrasta neviena organizācija, kas atbilstu kritērijiem.
-code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem.
-code_search_results=`Meklēšanas rezultāti "%s"`
 code_last_indexed_at=Pēdējo reizi indeksēts %s
 relevant_repositories_tooltip=Repozitoriju, kas ir atdalīti vai kuriem nav tēmas, ikonas un apraksta ir paslēpti.
 relevant_repositories=Tikai būtiskie repozitoriji tiek rādīti, <a href="%s">pārādīt nefiltrētus rezultātus</a>.
@@ -366,7 +362,6 @@ forgot_password_title=Aizmirsu paroli
 forgot_password=Aizmirsi paroli?
 sign_up_now=Nepieciešams konts? Reģistrējies tagad.
 sign_up_successful=Konts tika veiksmīgi izveidots. Laipni lūdzam!
-confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz <b>%s</b>, pārbaudies savu e-pasta kontu tuvāko %s laikā, lai pabeigtu reģistrācijas procesu.
 must_change_password=Mainīt paroli
 allow_password_change=Pieprasīt lietotājam mainīt paroli (ieteicams)
 reset_password_mail_sent_prompt=Apstiprināšanas e-pasts tika nosūtīts uz <b>%s</b>. Pārbaudiet savu e-pasta kontu tuvāko %s laikā, lai pabeigtu paroles atjaunošanas procesu.
@@ -614,6 +609,7 @@ form.name_reserved=Lietotājvārdu "%s" nedrīkst izmantot.
 form.name_pattern_not_allowed=Lietotājvārds "%s" nav atļauts.
 form.name_chars_not_allowed=Lietotāja vārds "%s" satur neatļautus simbolus.
 
+
 [settings]
 profile=Profils
 account=Konts
@@ -758,7 +754,6 @@ gpg_invalid_token_signature=Norādītā GPG atslēga, paraksts un pilnvara neatb
 gpg_token_required=Jānorāda paraksts zemāk esošajai pilnvarai
 gpg_token=Pilnvara
 gpg_token_help=Parakstu ir iespējams uzģenerēt izmantojot komandu:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Tekstuāls GPG paraksts
 key_signature_gpg_placeholder=Sākas ar '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=GPG atslēga "%s" veiksmīgi pārbaudīta.
@@ -952,7 +947,6 @@ fork_branch=Atzars, ko klonēt atdalītajā repozitorijā
 all_branches=Visi atzari
 fork_no_valid_owners=Šim repozitorijam nevar izveidot atdalītu repozitoriju, jo tam nav spēkā esošu īpašnieku.
 use_template=Izmantot šo sagatavi
-clone_in_vsc=Atvērt VS Code
 download_zip=Lejupielādēt ZIP
 download_tar=Lejupielādēt TAR.GZ
 download_bundle=Lejupielādēt BUNDLE
@@ -1272,9 +1266,7 @@ commits.desc=Pārlūkot pirmkoda izmaiņu vēsturi.
 commits.commits=Revīzijas
 commits.no_commits=Nav kopīgu revīziju. Atzariem "%s" un "%s" ir pilnībā atšķirīga izmaiņu vēsture.
 commits.nothing_to_compare=Atzari ir vienādi.
-commits.search=Meklēt revīzijas…
 commits.search.tooltip=Jūs varat izmantot atslēgas vārdus "author:", "committer:", "after:" vai "before:", piemēram, "revert author:Alice before:2019-01-13".
-commits.find=Meklēt
 commits.search_all=Visi atzari
 commits.author=Autors
 commits.message=Ziņojums
@@ -1325,7 +1317,6 @@ projects.type.basic_kanban=`Vienkāršots "Kanban"`
 projects.type.bug_triage=Kļūdu šķirošana
 projects.template.desc=Projekta sagatave
 projects.template.desc_helper=Izvēlieties projekta sagatavi, lai sāktu darbu
-projects.type.uncategorized=Bez kategorijas
 projects.column.edit=Rediģēt kolonnas
 projects.column.edit_title=Nosaukums
 projects.column.new_title=Nosaukums
@@ -1333,10 +1324,7 @@ projects.column.new_submit=Izveidot kolonnu
 projects.column.new=Jauna kolonna
 projects.column.set_default=Izvēlēties kā noklusēto
 projects.column.set_default_desc=Izvēlēties šo kolonnu kā noklusēto nekategorizētām problēmām un izmaiņu pieteikumiem
-projects.column.unset_default=Atiestatīt noklusēto
-projects.column.unset_default_desc=Noņemt šo kolonnu kā noklusēto
 projects.column.delete=Dzēst kolonnu
-projects.column.deletion_desc=Dzēšot projekta kolonnu visas tam piesaistītās problēmas tiks pārliktas kā nekategorizētas. Vai turpināt?
 projects.column.color=Krāsa
 projects.open=Aktīvie
 projects.close=Pabeigtie
@@ -1448,7 +1436,6 @@ issues.filter_sort.moststars=Visvairāk atzīmētie
 issues.filter_sort.feweststars=Vismazāk atzīmētie
 issues.filter_sort.mostforks=Visvairāk atdalītie
 issues.filter_sort.fewestforks=Vismazāk atdalītie
-issues.keyword_search_unavailable=Meklēšana pēc atslēgvārda pašreiz nav pieejama. Lūgums sazināties ar vietnes administratoru.
 issues.action_open=Atvērt
 issues.action_close=Aizvērt
 issues.action_label=Etiķete
@@ -1700,7 +1687,6 @@ pulls.compare_compare=salīdzināmais
 pulls.switch_comparison_type=Mainīt salīdzināšanas tipu
 pulls.switch_head_and_base=Mainīt galvas un pamata atzarus
 pulls.filter_branch=Filtrēt atzarus
-pulls.no_results=Nekas netika atrasts.
 pulls.show_all_commits=Rādīt visas revīzijas
 pulls.show_changes_since_your_last_review=Rādīt izmaiņas kopš Tavas pēdējās recenzijas
 pulls.showing_only_single_commit=Rāda tikai revīzijas %[1]s izmaiņas
@@ -1970,17 +1956,6 @@ activity.git_stats_deletion_n=%d dzēšanas
 
 contributors.contribution_type.commits=Revīzijas
 
-search=Meklēt
-search.search_repo=Meklēšana repozitorijā
-search.type.tooltip=Meklēšanas veids
-search.fuzzy=Aptuveni
-search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības
-search.match=Precīzi
-search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības
-search.results=Meklēšanas rezultāti nosacījumam "%s" repozitorijā <a href="%s">%s</a>
-search.code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem.
-search.code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru.
-
 settings=Iestatījumi
 settings.desc=Iestatījumi ir vieta, kur varat pārvaldīt repozitorija iestatījumus
 settings.options=Repozitorijs
@@ -2058,6 +2033,7 @@ settings.pulls.default_allow_edits_from_maintainers=Atļaut uzturētājiem labot
 settings.releases_desc=Iespējot repozitorija laidienus
 settings.packages_desc=Iespējot repozitorija pakotņu reģistru
 settings.projects_desc=Iespējot repozitorija projektus
+settings.projects_mode_all=Visi projekti
 settings.actions_desc=Iespējot repozitorija darbības
 settings.admin_settings=Administratora iestatījumi
 settings.admin_enable_health_check=Iespējot veselības pārbaudi (git fsck) šim repozitorijam
@@ -2132,7 +2108,6 @@ settings.delete_collaborator=Noņemt
 settings.collaborator_deletion=Noņemt līdzstrādnieku
 settings.collaborator_deletion_desc=Noņemot līdzstrādnieku, tam tiks liegta piekļuve šim repozitorijam. Vai turpināt?
 settings.remove_collaborator_success=Līdzstrādnieks tika noņemts.
-settings.search_user_placeholder=Meklēt lietotāju…
 settings.org_not_allowed_to_be_collaborator=Organizācijas nevar tikt pievienotas kā līdzstrādnieki.
 settings.change_team_access_not_allowed=Iespēja mainīt komandu piekļuvi repozitorijam ir organizācijas īpašniekam
 settings.team_not_in_organization=Komanda nav tajā pašā organizācijā kā repozitorijs
@@ -2140,7 +2115,6 @@ settings.teams=Komandas
 settings.add_team=Pievienot komandu
 settings.add_team_duplicate=Komandai jau ir piekļuve šim repozitorijam
 settings.add_team_success=Komandai tagad ir piekļuve šim repozitorijam.
-settings.search_team=Meklēt komandu…
 settings.change_team_permission_tip=Komandas tiesības tiek uzstādītas komandas iestatījumu lapā un nevar tikt individuāli mainītas katram repozitorijam atsevišķi
 settings.delete_team_tip=Komandai ir piekļuve visiem repozitorijiem un tā nevar tikt noņemta individuāli
 settings.remove_team_success=Komandas piekļuve šim repozitorijam ir noņemta.
@@ -2293,9 +2267,7 @@ settings.protect_whitelist_committers=Atļaut iesūtīt izmaiņas norādītajiem
 settings.protect_whitelist_committers_desc=Tikai norādītiem lietotāji vai komandas varēs iesūtīt izmaiņas šajā atzarā (piespiedu izmaiņu iesūtīšanas netiks atļauta).
 settings.protect_whitelist_deploy_keys=Atļaut izvietošanas atslēgām ar rakstīšanas tiesībām nosūtīt izmaiņas.
 settings.protect_whitelist_users=Lietotāji, kas var veikt izmaiņu nosūtīšanu:
-settings.protect_whitelist_search_users=Meklēt lietotājus…
 settings.protect_whitelist_teams=Komandas, kas var veikt izmaiņu nosūtīšanu:
-settings.protect_whitelist_search_teams=Meklēt komandas…
 settings.protect_merge_whitelist_committers=Iespējot sapludināšanas ierobežošanu
 settings.protect_merge_whitelist_committers_desc=Atļaut tikai noteiktiem lietotājiem vai komandām sapludināt izmaiņu pieprasījumus šajā atzarā.
 settings.protect_merge_whitelist_users=Lietotāji, kas var veikt izmaiņu sapludināšanu:
@@ -2537,7 +2509,6 @@ branch.default_deletion_failed=Atzars "%s" ir noklusētais atzars un to nevar dz
 branch.restore=`Atjaunot atzaru "%s"`
 branch.download=`Lejupielādēt atzaru "%s"`
 branch.rename=`Pārsaukt atzaru "%s"`
-branch.search=Meklēt atzarā
 branch.included_desc=Šis atzars ir daļa no noklusēta atzara
 branch.included=Iekļauts
 branch.create_new_branch=Izveidot jaunu atzaru no atzara:
@@ -2679,7 +2650,6 @@ teams.write_permission_desc=Šai komandai ir <strong>rakstīšanas</strong> ties
 teams.admin_permission_desc=Šai komandai ir <strong>administratora</strong> tiesības: dalībnieki var lasīt, rakstīt un pievienot citus dalībniekus komandas repozitorijiem.
 teams.create_repo_permission_desc=Papildus šī komanda piešķirt <strong>Veidot repozitorijus</strong> tiesības: komandas biedri var veidot jaunus repozitorijus šajā organizācijā.
 teams.repositories=Komandas repozitoriji
-teams.search_repo_placeholder=Meklēt repozitorijā…
 teams.remove_all_repos_title=Noņemt visus komandas repozitorijus
 teams.remove_all_repos_desc=Šī darbība noņems visus repozitorijus no komandas.
 teams.add_all_repos_title=Pievienot visus repozitorijus
@@ -2712,6 +2682,8 @@ integrations=Integrācijas
 authentication=Autentificēšanas avoti
 emails=Lietotāja e-pasts
 config=Konfigurācija
+config_summary=Kopsavilkums
+config_settings=Iestatījumi
 notices=Sistēmas paziņojumi
 monitor=Uzraudzība
 first_page=Pirmā
@@ -2886,9 +2858,6 @@ repos.unadopted.no_more=Netika atrasts neviens nepārņemtais repozitorijs
 repos.owner=Īpašnieks
 repos.name=Nosaukums
 repos.private=Privāts
-repos.watches=Vērošana
-repos.stars=Zvaigznes
-repos.forks=Atdalītie
 repos.issues=Problēmas
 repos.size=Izmērs
 repos.lfs_size=LFS izmērs
@@ -3013,7 +2982,6 @@ auths.tip.nextcloud=`Reģistrējiet jaunu OAuth klientu jūsu instances sadāļ
 auths.tip.dropbox=Izveidojiet jaunu aplikāciju adresē https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Reģistrējiet jaunu aplikāciju adresē https://developers.facebook.com/apps un pievienojiet produktu "Facebook Login"`
 auths.tip.github=Reģistrējiet jaunu aplikāciju adresē https://github.com/settings/applications/new
-auths.tip.gitlab=Reģistrējiet jaunu aplikāciju adresē https://gitlab.com/profile/applications
 auths.tip.google_plus=Iegūstiet OAuth2 klienta pilnvaru no Google API konsoles adresē https://console.developers.google.com/
 auths.tip.openid_connect=Izmantojiet OpenID pieslēgšanās atklāšanas URL (<serveris>/.well-known/openid-configuration), lai norādītu galapunktus
 auths.tip.twitter=Dodieties uz adresi https://dev.twitter.com/apps, izveidojiet lietotni un pārliecinieties, ka ir atzīmēts “Allow this application to be used to Sign in with Twitter”
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index fc1da2b992..255a3db9fa 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -115,6 +115,15 @@ concept_user_organization=Organisatie
 
 name=Naam
 
+filter=Filter
+filter.is_archived=Gearchiveerd
+filter.is_template=Sjabloon
+filter.public=Publiek
+filter.private=Prive
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -253,7 +262,6 @@ collaborative_repos=Gedeelde repositories
 my_orgs=Mijn organisaties
 my_mirrors=Mijn spiegels
 view_home=Bekijk %s
-search_repos=Zoek een repository…
 filter=Andere filters
 filter_by_team_repositories=Filter op team repositories
 feed_of=`Feed van "%s"`
@@ -274,15 +282,7 @@ issues.in_your_repos=In uw repositories
 repos=Repositories
 users=Gebruikers
 organizations=Organisaties
-search=Zoeken
 code=Code
-search.fuzzy=Vergelijkbaar
-search.match=Overeenkomst
-code_search_unavailable=Er is momenteel geen code zoekfunctie beschikbaar. Neem contact op met uw sitebeheerder.
-repo_no_results=Er zijn geen overeenkomende repositories gevonden.
-user_no_results=Er zijn geen overeenkomende gebruikers gevonden.
-org_no_results=Er zijn geen overeenkomende organisaties gevonden.
-code_no_results=Geen broncode gevonden in overeenstemming met uw zoekterm.
 code_last_indexed_at=Laatst geïndexeerd %s
 
 [auth]
@@ -296,7 +296,6 @@ remember_me=Onthoud dit apparaat
 forgot_password_title=Wachtwoord vergeten
 forgot_password=Wachtwoord vergeten?
 sign_up_now=Een account nodig? Meld u nu aan.
-confirmation_mail_sent_prompt=Een nieuwe bevestigingsmail is gestuurd naar <b>%s</b>. De mail moet binnen %s worden bevestigd om je registratie te voltooien.
 must_change_password=Uw wachtwoord wijzigen
 allow_password_change=Verplicht de gebruiker om zijn/haar wachtwoord te wijzigen (aanbevolen)
 reset_password_mail_sent_prompt=Een bevestigingsmail is verstuurd naar <b>%s</b>. Controleer uw inbox in de volgende %s om het herstel van uw account te voltooien.
@@ -506,6 +505,7 @@ user_bio=Biografie
 disabled_public_activity=Deze gebruiker heeft de publieke zichtbaarheid van de activiteit uitgeschakeld.
 
 
+
 [settings]
 profile=Profiel
 account=Account
@@ -633,7 +633,6 @@ gpg_invalid_token_signature=De opgegeven GPG-sleutel, handtekening en token kome
 gpg_token_required=U moet een handtekening opgeven voor de onderstaande token
 gpg_token=Token
 gpg_token_help=U kunt een handtekening genereren met:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Gepantserde GPG-handtekening
 key_signature_gpg_placeholder=Begint met '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Geverifieerde sleutel
@@ -785,7 +784,6 @@ already_forked=Je hebt %s al geforked
 fork_to_different_account=Fork naar een ander account
 fork_visibility_helper=De zichtbaarheid van een geforkte repository kan niet worden veranderd.
 use_template=Gebruik dit sjabloon
-clone_in_vsc=Kloon in VS Code
 download_zip=ZIP downloaden
 download_tar=TAR.GZ downloaden
 download_bundle=BUNDLE downloaden
@@ -1049,8 +1047,6 @@ editor.revert=%s ongedaan maken op:
 commits.desc=Bekijk de broncode-wijzigingsgeschiedenis.
 commits.commits=Commits
 commits.nothing_to_compare=Deze branches zijn gelijk.
-commits.search=Zoek commits…
-commits.find=Zoek
 commits.search_all=Alle branches
 commits.author=Auteur
 commits.message=Bericht
@@ -1095,7 +1091,6 @@ projects.type.basic_kanban=Basis Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Project sjabloon
 projects.template.desc_helper=Selecteer een projecttemplate om aan de slag te gaan
-projects.type.uncategorized=Ongecategoriseerd
 projects.column.edit_title=Naam
 projects.column.new_title=Naam
 projects.column.color=Kleur
@@ -1406,7 +1401,6 @@ pulls.compare_compare=trekken van
 pulls.switch_comparison_type=Wissel vergelijking type
 pulls.switch_head_and_base=Verwissel hoofd en basis
 pulls.filter_branch=Filter branch
-pulls.no_results=Geen resultaten gevonden.
 pulls.nothing_to_compare=Deze branches zijn gelijk. Er is geen pull-aanvraag nodig.
 pulls.nothing_to_compare_and_allow_empty_pr=Deze branches zijn gelijk. Deze pull verzoek zal leeg zijn.
 pulls.has_pull_request=`Een pull-verzoek tussen deze branches bestaat al: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1621,14 +1615,6 @@ activity.git_stats_deletion_n=%d verwijderingen
 
 contributors.contribution_type.commits=Commits
 
-search=Zoek
-search.search_repo=Zoek repository
-search.fuzzy=Vergelijkbaar
-search.match=Overeenkomst
-search.results=Zoek resultaat voor "%s" in <a href="%s">%s</a>
-search.code_no_results=Geen broncode gevonden die aan uw zoekterm voldoet.
-search.code_search_unavailable=Er is momenteel geen code zoekfunctie beschikbaar. Neem contact op met uw sitebeheerder.
-
 settings=Instellingen
 settings.desc=In de instellingen kan je de instellingen van de repository aanpassen
 settings.options=Repository
@@ -1705,7 +1691,6 @@ settings.delete_collaborator=Verwijder
 settings.collaborator_deletion=Verwijder medewerker
 settings.collaborator_deletion_desc=Het verwijderen van een collaborator zal hun toegang tot deze repository intrekken. Doorgaan?
 settings.remove_collaborator_success=De medewerker is verwijderd.
-settings.search_user_placeholder=Zoek gebruiker…
 settings.org_not_allowed_to_be_collaborator=Organisaties kunnen niet worden toegevoegd als een medewerker.
 settings.change_team_access_not_allowed=Het veranderen van team toegang voor de repository is beperkt tot de organisatie eigenaar
 settings.team_not_in_organization=Het team zit niet in dezelfde organisatie als de repository
@@ -1713,7 +1698,6 @@ settings.teams=Teams
 settings.add_team=Team toevoegen
 settings.add_team_duplicate=Team heeft al de repository
 settings.add_team_success=Het team heeft nu toegang tot de repository.
-settings.search_team=Zoek team…
 settings.change_team_permission_tip=Teammachtiging is ingesteld op de team-instellingspagina en kan niet per repository worden gewijzigd
 settings.delete_team_tip=Dit team heeft toegang tot alle repositories en kan niet verwijderd worden
 settings.remove_team_success=De toegang van het team tot de repository is verwijderd.
@@ -1845,9 +1829,7 @@ settings.protect_whitelist_committers=Whitelist Beperkte Push
 settings.protect_whitelist_committers_desc=Alleen gewhiteliste gebruikers of teams mogen pushen naar deze branch (maar geen force push).
 settings.protect_whitelist_deploy_keys=Whitelist deploy sleutels met schrijftoegang om te pushen.
 settings.protect_whitelist_users=Toegestane gebruikers voor push:
-settings.protect_whitelist_search_users=Zoek gebruiker…
 settings.protect_whitelist_teams=Toegestane teams voor push:
-settings.protect_whitelist_search_teams=Zoek teams…
 settings.protect_merge_whitelist_committers=Samenvoegen whitelist inschakelen
 settings.protect_merge_whitelist_committers_desc=Sta alleen gebruikers of teams van de whitelist toe om pull requests samen te voegen met deze branch.
 settings.protect_merge_whitelist_users=Toegestane gebruikers voor samenvoegen:
@@ -2123,7 +2105,6 @@ teams.write_permission_desc=Dit team heeft <strong>Schrijf</strong> rechten: led
 teams.admin_permission_desc=Dit team heeft <strong>beheersrechten</strong>: leden kunnen van en naar teamrepositories pullen, pushen, en er medewerkers aan toevoegen.
 teams.create_repo_permission_desc=Daarnaast verleent dit team <strong>Maak repository</strong> permissie: leden kunnen nieuwe repositories maken in de organisatie.
 teams.repositories=Teamrepositories
-teams.search_repo_placeholder=Repository zoeken…
 teams.remove_all_repos_title=Verwijder alle team repositories
 teams.remove_all_repos_desc=Dit zal alle repositories uit het team verwijderen.
 teams.add_all_repos_title=Voeg alle repositories toe
@@ -2145,6 +2126,8 @@ repositories=Repositories
 authentication=Authenticatie bronnen
 emails=Gebruikeremails
 config=Configuratie
+config_summary=Overzicht
+config_settings=Instellingen
 notices=Systeem aankondigingen
 monitor=Bijhouden
 first_page=Eerste
@@ -2282,9 +2265,6 @@ repos.unadopted.no_more=Geen niet-geadopteerde repositories meer gevonden
 repos.owner=Eigenaar
 repos.name=Naam
 repos.private=Prive
-repos.watches=Volgers
-repos.stars=Sterren
-repos.forks=Forks
 repos.issues=Kwesties
 repos.size=Grootte
 
@@ -2365,7 +2345,6 @@ auths.tip.nextcloud=`Registreer een nieuwe OAuth consument op je installatie met
 auths.tip.dropbox=Maak een nieuwe applicatie aan op https://www.dropbox.com/developers/apps
 auths.tip.facebook=Registreer een nieuwe applicatie op https://developers.facebook.com/apps en voeg het product "Facebook Login" toe
 auths.tip.github=Registreer een nieuwe OAuth toepassing op https://github.com/settings/applications/new
-auths.tip.gitlab=Registreer een nieuwe applicatie op https://gitlab.com/profile/applicaties
 auths.tip.google_plus=Verkrijg OAuth2 client referenties van de Google API console op https://console.developers.google.com/
 auths.tip.openid_connect=Gebruik de OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) om de eindpunten op te geven
 auths.tip.yandex=`Maak een nieuwe applicatie aan op https://oauth.yandex.com/client/new. Selecteer de volgende machtigingen van de "Yandex". assport API sectie: "Toegang tot e-mailadres", "Toegang tot avatar" en "Toegang tot gebruikersnaam, voornaam en achternaam, geslacht"`
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 2af3ce1a11..1496877fd5 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -113,6 +113,14 @@ concept_user_organization=Organizacja
 
 name=Nazwa
 
+filter.is_archived=Zarchiwizowane
+filter.is_template=Szablon
+filter.public=Publiczne
+filter.private=Prywatne
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -251,7 +259,6 @@ collaborative_repos=Wspólne repozytoria
 my_orgs=Moje organizacje
 my_mirrors=Moje kopie lustrzane
 view_home=Zobacz %s
-search_repos=Znajdź repozytorium…
 filter=Inne filtry
 filter_by_team_repositories=Filtruj według repozytoriów zespołu
 feed_of=`Kanał "%s"`
@@ -272,14 +279,7 @@ issues.in_your_repos=W Twoich repozytoriach
 repos=Repozytoria
 users=Użytkownicy
 organizations=Organizacje
-search=Szukaj
 code=Kod
-search.fuzzy=Fuzzy
-search.match=Dopasuj
-repo_no_results=Nie znaleziono pasujących repozytoriów.
-user_no_results=Nie znaleziono pasującego użytkowników.
-org_no_results=Nie znaleziono pasujących organizacji.
-code_no_results=Nie znaleziono kodu źródłowego odpowiadającego Twojej frazie wyszukiwania.
 code_last_indexed_at=Ostatnio indeksowane %s
 
 [auth]
@@ -292,7 +292,6 @@ remember_me=Zapamiętaj to urządzenie
 forgot_password_title=Zapomniałem hasła
 forgot_password=Zapomniałeś hasła?
 sign_up_now=Potrzebujesz konta? Zarejestruj się teraz.
-confirmation_mail_sent_prompt=Nowy email aktywacyjny został wysłany na adres <b>%s</b>. Sprawdź swoją skrzynkę odbiorczą w ciągu %s aby dokończyć proces rejestracji.
 must_change_password=Zaktualizuj swoje hasło
 allow_password_change=Użytkownik musi zmienić hasło (zalecane)
 reset_password_mail_sent_prompt=E-mail potwierdzający został wysłany na adres <b>%s</b>. Sprawdź swoją skrzynkę odbiorczą w przeciągu %s, aby ukończyć proces odzyskiwania konta.
@@ -491,6 +490,7 @@ user_bio=Biografia
 disabled_public_activity=Ten użytkownik wyłączył publiczne wyświetlanie jego aktywności.
 
 
+
 [settings]
 profile=Profil
 account=Konto
@@ -597,7 +597,6 @@ gpg_invalid_token_signature=Podany klucz GPG, podpis i token nie pasują lub tok
 gpg_token_required=Musisz podać podpis poniższego tokenu
 gpg_token=Token
 gpg_token_help=Możesz wygenerować podpis za pomocą:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Wzmocniony podpis GPG
 key_signature_gpg_placeholder=Zaczyna się od '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Zweryfikowany klucz
@@ -741,7 +740,6 @@ fork_repo=Forkuj repozytorium
 fork_from=Forkuj z
 fork_visibility_helper=Widoczność sforkowanego repozytorium nie może być zmieniona.
 use_template=Użyj tego szablonu
-clone_in_vsc=Klonuj w VS Code
 download_zip=Pobierz ZIP
 download_tar=Pobierz TAR.GZ
 download_bundle=Pobierz BUNDLE
@@ -974,8 +972,6 @@ editor.require_signed_commit=Gałąź wymaga podpisanych commitów
 
 commits.desc=Przeglądaj historię zmian kodu źródłowego.
 commits.commits=Commity
-commits.search=Przeszukaj commity…
-commits.find=Szukaj
 commits.search_all=Wszystkie gałęzie
 commits.author=Autor
 commits.message=Wiadomość
@@ -1011,7 +1007,6 @@ projects.type.basic_kanban=Basic Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Szablon projektu
 projects.template.desc_helper=Wybierz szablon projektu do rozpoczęcia
-projects.type.uncategorized=Bez kategorii
 projects.column.edit_title=Nazwa
 projects.column.new_title=Nazwa
 projects.column.color=Kolor
@@ -1280,7 +1275,6 @@ pulls.compare_changes_desc=Wybierz gałąź, do której chcesz scalić oraz gał
 pulls.compare_base=scal do
 pulls.compare_compare=ściągnij z
 pulls.filter_branch=Filtruj branch
-pulls.no_results=Nie znaleziono wyników.
 pulls.nothing_to_compare=Te gałęzie są sobie równe. Nie ma potrzeby tworzyć Pull Requesta.
 pulls.nothing_to_compare_and_allow_empty_pr=Te gałęzie są równe. Ten PR będzie pusty.
 pulls.create=Utwórz Pull Request
@@ -1470,13 +1464,6 @@ activity.git_stats_deletion_n=%d usunięć
 
 contributors.contribution_type.commits=Commity
 
-search=Szukaj
-search.search_repo=Przeszukaj repozytorium
-search.fuzzy=Fuzzy
-search.match=Dopasuj
-search.results=Wyniki wyszukiwania dla "%s" w <a href="%s">%s</a>
-search.code_no_results=Nie znaleziono kodu źródłowego odpowiadającego Twojej frazie wyszukiwania.
-
 settings=Ustawienia
 settings.desc=Ustawienia to miejsce, w którym możesz zmieniać parametry repozytorium
 settings.options=Repozytorium
@@ -1586,7 +1573,6 @@ settings.delete_collaborator=Usuń
 settings.collaborator_deletion=Usuń współpracownika
 settings.collaborator_deletion_desc=Usunięcie współpracownika odbierze mu dostęp do tego repozytorium. Kontynuować?
 settings.remove_collaborator_success=Usunięto użytkownika.
-settings.search_user_placeholder=Szukaj użytkownika…
 settings.org_not_allowed_to_be_collaborator=Organizacji nie można dodać jako współpracownika.
 settings.change_team_access_not_allowed=Zmiana dostępu zespołu do repozytorium zostało zastrzeżone do właściciela organizacji
 settings.team_not_in_organization=Zespół nie jest w tej samej organizacji co repozytorium
@@ -1594,7 +1580,6 @@ settings.teams=Zespoły
 settings.add_team=Dodaj zespół
 settings.add_team_duplicate=Zespół już posiada repozytorium
 settings.add_team_success=Zespół ma teraz dostęp do repozytorium.
-settings.search_team=Szukaj zespołu…
 settings.change_team_permission_tip=Uprawnienia zespołu ustawione są konfigurowane na stronie ustawień zespołu i nie mogą być zmieniane dla pojedynczych repozytoriów
 settings.delete_team_tip=Ten zespół ma dostęp do wszystkich repozytoriów i nie może zostać usunięty
 settings.remove_team_success=Dostęp zespołu do repozytorium został usunięty.
@@ -1710,9 +1695,7 @@ settings.protect_whitelist_committers=Wypychanie ograniczone białą listą
 settings.protect_whitelist_committers_desc=Tylko dopuszczeni użytkownicy oraz zespoły będą miały możliwość wypychania zmian do tej gałęzi (oprócz wymuszenia wypchnięcia).
 settings.protect_whitelist_deploy_keys=Dozwolona lista kluczy wdrożeniowych z uprawnieniem zapisu do push'a.
 settings.protect_whitelist_users=Użytkownicy dopuszczeni do wypychania:
-settings.protect_whitelist_search_users=Szukaj użytkowników…
 settings.protect_whitelist_teams=Zespoły dopuszczone do wypychania:
-settings.protect_whitelist_search_teams=Szukaj zespołów…
 settings.protect_merge_whitelist_committers=Włącz dopuszczenie scalania
 settings.protect_merge_whitelist_committers_desc=Zezwól jedynie dopuszczonym użytkownikom lub zespołom na scalanie Pull Requestów w tej gałęzi.
 settings.protect_merge_whitelist_users=Użytkownicy dopuszczeni do scalania:
@@ -1994,7 +1977,6 @@ teams.write_permission_desc=Ten zespół udziela dostępu <strong>z zapisem</str
 teams.admin_permission_desc=Ten zespół udziela dostępu <strong>administratora</strong>: członkowie mogą wyświetlać i wypychać zmiany oraz dodawać współpracowników do repozytoriów zespołu.
 teams.create_repo_permission_desc=Dodatkowo, ten zespół otrzyma uprawnienie <strong>Tworzenie repozytoriów</strong>: jego członkowie mogą tworzyć nowe repozytoria w organizacji.
 teams.repositories=Repozytoria zespołu
-teams.search_repo_placeholder=Szukaj repozytorium…
 teams.remove_all_repos_title=Usuń wszystkie repozytoria zespołu
 teams.remove_all_repos_desc=Usunie to wszystkie repozytoria przypisane do zespołu.
 teams.add_all_repos_title=Dodaj wszystkie repozytoria
@@ -2019,6 +2001,8 @@ hooks=Weebhook'i
 authentication=Źródła uwierzytelniania
 emails=Emaile użytkowników
 config=Konfiguracja
+config_summary=Podsumowanie
+config_settings=Ustawienia
 notices=Powiadomienia systemu
 monitor=Monitorowanie
 first_page=Pierwsza
@@ -2157,9 +2141,6 @@ repos.unadopted.no_more=Nie znaleziono więcej nieprzyjętych repozytoriów
 repos.owner=Właściciel
 repos.name=Nazwa
 repos.private=Prywatne
-repos.watches=Obserwujących
-repos.stars=Polubienia
-repos.forks=Forki
 repos.issues=Zgłoszenia
 repos.size=Rozmiar
 
@@ -2248,7 +2229,6 @@ auths.tip.nextcloud=`Zarejestruj nowego klienta OAuth w swojej instancji za pomo
 auths.tip.dropbox=Stwórz nową aplikację na https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Zarejestruj nową aplikację na https://developers.facebook.com/apps i dodaj produkt "Facebook Login"`
 auths.tip.github=Zarejestruj nową aplikację OAuth na https://github.com/settings/applications/new
-auths.tip.gitlab=Zarejestruj nową aplikację na https://gitlab.com/profile/applications
 auths.tip.google_plus=Uzyskaj dane uwierzytelniające klienta OAuth2 z konsoli Google API na https://console.developers.google.com/
 auths.tip.openid_connect=Użyj adresu URL OpenID Connect Discovery (<server>/.well-known/openid-configuration), aby określić punkty końcowe
 auths.tip.twitter=Przejdź na https://dev.twitter.com/apps, stwórz aplikację i upewnij się, że opcja “Allow this application to be used to Sign in with Twitter” jest włączona
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 11743f29a5..0d1614df3f 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -90,6 +90,7 @@ remove=Remover
 remove_all=Excluir todos
 remove_label_str=`Remover item "%s"`
 edit=Editar
+view=Visualizar
 
 enabled=Habilitado
 disabled=Desabilitado
@@ -97,6 +98,7 @@ locked=Bloqueado
 
 copy=Copiar
 copy_url=Copiar URL
+copy_hash=Copiar hash
 copy_content=Copiar conteúdo
 copy_branch=Copiar nome do branch
 copy_success=Copiado!
@@ -109,6 +111,7 @@ loading=Carregando…
 
 error=Erro
 error404=A página que você está tentando acessar <strong>não existe</strong> ou <strong>você não está autorizado</strong> a visualizá-la.
+go_back=Voltar
 
 never=Nunca
 unknown=Desconhecido
@@ -119,6 +122,7 @@ pin=Fixar
 unpin=Desfixar
 
 artifacts=Artefatos
+confirm_delete_artifact=Tem certeza que deseja excluir o artefato '%s' ?
 
 archived=Arquivado
 
@@ -137,6 +141,15 @@ confirm_delete_selected=Confirma a exclusão de todos os itens selecionados?
 name=Nome
 value=Valor
 
+filter=Filtro
+filter.is_archived=Arquivado
+filter.is_template=Template
+filter.public=Pública
+filter.private=Privado
+
+
+[search]
+
 [aria]
 navbar=Barra de navegação
 footer=Rodapé
@@ -309,7 +322,6 @@ collaborative_repos=Repositórios colaborativos
 my_orgs=Minhas organizações
 my_mirrors=Meus espelhos
 view_home=Ver %s
-search_repos=Encontre um repositório…
 filter=Outros filtros
 filter_by_team_repositories=Filtrar por repositórios da equipe
 feed_of=`Feed de "%s"`
@@ -330,20 +342,8 @@ issues.in_your_repos=Em seus repositórios
 repos=Repositórios
 users=Usuários
 organizations=Organizações
-search=Pesquisar
 go_to=Ir para
 code=Código
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Similar
-search.fuzzy.tooltip=Incluir resultados que sejam próximos ao termo de busca
-search.match=Correspondência
-search.match.tooltip=Incluir somente resultados que correspondam exatamente ao termo de busca
-code_search_unavailable=A pesquisa por código não está disponível no momento. Entre em contato com o administrador do site.
-repo_no_results=Nenhum repositório correspondente foi encontrado.
-user_no_results=Nenhum usuário correspondente foi encontrado.
-org_no_results=Nenhuma organização correspondente foi encontrada.
-code_no_results=Nenhum código-fonte correspondente ao seu termo de pesquisa foi encontrado.
-code_search_results=`Resultados da pesquisa por: "%s"`
 code_last_indexed_at=Última indexação %s
 relevant_repositories_tooltip=Repositórios que são forks ou que não possuem tópico, nem ícone e nem descrição estão ocultos.
 relevant_repositories=Apenas repositórios relevantes estão sendo mostrados, <a href="%s">mostrar resultados não filtrados</a>.
@@ -356,11 +356,11 @@ disable_register_prompt=Cadastro está desabilitado. Entre em contato com o admi
 disable_register_mail=E-mail de confirmação de cadastro está desabilitado.
 manual_activation_only=Entre em contato com o administrador do site para concluir a ativação.
 remember_me=Lembrar deste Dispositivo
+remember_me.compromised=O token de login não é mais válido, o que pode indicar uma conta comprometida. Por favor, verifique a sua conta por atividades incomuns.
 forgot_password_title=Esqueci minha senha
 forgot_password=Esqueceu sua senha?
 sign_up_now=Precisa de uma conta? Cadastre-se agora.
 sign_up_successful=A conta foi criada com sucesso. Bem-vindo!
-confirmation_mail_sent_prompt=Um novo e-mail de confirmação foi enviado para <b>%s</b>. Por favor, verifique sua caixa de e-mail nas próximas %s horas para finalizar o processo de cadastro.
 must_change_password=Redefina sua senha
 allow_password_change=Exigir que o usuário redefina a senha (recomendado)
 reset_password_mail_sent_prompt=Um e-mail de confirmação foi enviado para <b>%s</b>. Por favor, verifique sua caixa de entrada dentro do(s) próximo(s) %s para concluir o processo de recuperação de conta.
@@ -417,6 +417,7 @@ authorization_failed_desc=A autorização falhou porque detectamos uma solicita
 sspi_auth_failed=Falha de autenticação SSPI
 password_pwned=A senha que você escolheu faz parte de uma <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">lista de senhas roubadas</a> expostas anteriormente em violações de dados. Tente novamente com uma senha diferente e considere alterar essa senha em outro lugar também.
 password_pwned_err=Não foi possível concluir a requisição ao HaveIBeenPwned
+last_admin=Você não pode remover o último administrador. Deve haver pelo menos um administrador.
 
 [mail]
 view_it_on=Veja em %s
@@ -582,6 +583,7 @@ org_still_own_packages=Esta organização ainda possui pacotes, exclua-os primei
 
 target_branch_not_exist=O branch de destino não existe.
 
+admin_cannot_delete_self=Você não pode excluir você mesmo quando você é um administrador. Por favor, remova seus privilégios de administrador primeiro.
 
 [user]
 change_avatar=Altere seu avatar...
@@ -608,6 +610,7 @@ form.name_reserved=O nome de usuário "%s" está reservado.
 form.name_pattern_not_allowed=O padrão de "%s" não é permitido em um nome de usuário.
 form.name_chars_not_allowed=Nome de usuário "%s" contém caracteres inválidos.
 
+
 [settings]
 profile=Perfil
 account=Conta
@@ -752,7 +755,6 @@ gpg_invalid_token_signature=A chave GPG fornecida, a assinatura ou o token não
 gpg_token_required=Você tem que fornecer uma assinatura para o token abaixo
 gpg_token=Token
 gpg_token_help=Você pode gerar uma assinatura usando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Assinatura GPG blindada
 key_signature_gpg_placeholder=Começa com '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=A chave GPG "%s" foi validada.
@@ -859,6 +861,7 @@ revoke_oauth2_grant_description=Revogando o acesso para este aplicativo de terce
 revoke_oauth2_grant_success=Acesso revogado com sucesso.
 
 twofa_desc=Autenticação de dois fatores melhora a segurança de sua conta.
+twofa_recovery_tip=Se você perder o seu dispositivo, você será capaz de usar uma chave de recuperação de uso único para recuperar o acesso à sua conta.
 twofa_is_enrolled=Sua conta está atualmente <strong>habilitada</strong> com autenticação de dois fatores.
 twofa_not_enrolled=Sua conta não está atualmente inscrita para a autenticação em duas etapas.
 twofa_disable=Desabilitar a autenticação de dois fatores
@@ -881,6 +884,8 @@ webauthn_register_key=Adicionar chave de segurança
 webauthn_nickname=Apelido
 webauthn_delete_key=Remover chave de segurança
 webauthn_delete_key_desc=Se você remover uma chave de segurança, não poderá mais entrar com ela. Continuar?
+webauthn_key_loss_warning=Se você perder suas chaves de segurança, perderá o acesso à sua conta.
+webauthn_alternative_tip=Você pode querer configurar um método de autenticação adicional.
 
 manage_account_links=Gerenciar contas vinculadas
 manage_account_links_desc=Estas contas externas estão vinculadas a sua conta de Gitea.
@@ -917,6 +922,7 @@ visibility.private=Privada
 visibility.private_tooltip=Visível apenas para membros das organizações às quais você se associou
 
 [repo]
+new_repo_helper=Um repositório contém todos os arquivos do projeto, inclusive o histórico de revisões. Já está hospedando um em outro lugar? <a href="%s">Migre o repositório.</a>
 owner=Proprietário
 owner_helper=Algumas organizações podem não aparecer no menu devido a um limite de contagem dos repositórios.
 repo_name=Nome do repositório
@@ -937,9 +943,10 @@ fork_from=Fork de
 already_forked=Você já fez o fork de %s
 fork_to_different_account=Faça um fork para uma conta diferente
 fork_visibility_helper=A visibilidade do fork de um repositório não pode ser alterada.
+fork_branch=Branch a ser clonado para o fork
+all_branches=Todos os branches
 fork_no_valid_owners=Não é possível fazer um fork desse repositório porque não há proprietários validos.
 use_template=Usar este modelo
-clone_in_vsc=Clonar no VS Code
 download_zip=Baixar ZIP
 download_tar=Baixar TAR.GZ
 download_bundle=Baixar PACOTE
@@ -972,6 +979,7 @@ mirror_prune=Varrer
 mirror_prune_desc=Remover referências obsoletas de controle remoto
 mirror_interval=Intervalo de espelhamento (unidades válidas são 'h', 'm', ou 's'). O desabilita a sincronização automática. (Intervalo mínimo: %s)
 mirror_interval_invalid=O intervalo do espelhamento não é válido.
+mirror_sync=sincronizado
 mirror_sync_on_commit=Sincronizar quando commits forem enviados
 mirror_address=Clonar de URL
 mirror_address_desc=Coloque todas as credenciais necessárias na seção de autorização.
@@ -1017,6 +1025,7 @@ desc.public=Público
 desc.template=Template
 desc.internal=Interno
 desc.archived=Arquivado
+desc.sha256=SHA256
 
 template.items=Itens do modelo
 template.git_content=Conteúdo Git (Branch padrão)
@@ -1167,6 +1176,7 @@ audio_not_supported_in_browser=Seu navegador não suporta a tag 'audio' do HTML5
 stored_lfs=Armazenado com Git LFS
 symbolic_link=Link simbólico
 executable_file=Arquivo executável
+generated=Gerado
 commit_graph=Gráfico de commits
 commit_graph.select=Selecionar branches
 commit_graph.hide_pr_refs=Esconder Pull Requests
@@ -1253,9 +1263,7 @@ commits.desc=Veja o histórico de alterações do código de fonte.
 commits.commits=Commits
 commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes.
 commits.nothing_to_compare=Estes branches são iguais.
-commits.search=Pesquisar commits...
 commits.search.tooltip=Você pode prefixar as palavras-chave com "author:" (autor da mudança), "committer:" (autor do commit), "after:" (depois) ou "before:" (antes). Por exemplo: "revert author:Ana before:2019-01-13".\
-commits.find=Pesquisar
 commits.search_all=Todos os branches
 commits.author=Autor
 commits.message=Mensagem
@@ -1267,6 +1275,7 @@ commits.signed_by_untrusted_user=Assinado por usuário não confiável
 commits.signed_by_untrusted_user_unmatched=Assinado por usuário não confiável que não corresponde ao autor da submissão
 commits.gpg_key_id=ID da chave GPG
 commits.ssh_key_fingerprint=Impressão Digital da Chave SSH
+commits.view_path=Visualizar neste ponto do histórico
 
 commit.operations=Operações
 commit.revert=Reverter
@@ -1305,7 +1314,6 @@ projects.type.basic_kanban=Kanban básico
 projects.type.bug_triage=Triagem de Bugs
 projects.template.desc=Modelo de projeto
 projects.template.desc_helper=Selecione um modelo de projeto para começar
-projects.type.uncategorized=Sem categoria
 projects.column.edit=Editar coluna
 projects.column.edit_title=Nome
 projects.column.new_title=Nome
@@ -1313,10 +1321,7 @@ projects.column.new_submit=Criar coluna
 projects.column.new=Nova Coluna
 projects.column.set_default=Atribuir como padrão
 projects.column.set_default_desc=Definir esta coluna como padrão para pull e issues sem categoria
-projects.column.unset_default=Desatribuir padrão
-projects.column.unset_default_desc=Desatribuir esta coluna como padrão
 projects.column.delete=Excluir coluna
-projects.column.deletion_desc=Excluir uma coluna do projeto move todas as issues relacionadas para 'Sem categoria'. Continuar?
 projects.column.color=Cor
 projects.open=Abrir
 projects.close=Fechar
@@ -1357,6 +1362,7 @@ issues.choose.blank=Padrão
 issues.choose.blank_about=Criar uma issue a partir do modelo padrão.
 issues.choose.ignore_invalid_templates=Modelos inválidos foram ignorados
 issues.choose.invalid_templates=%v modelo(s) inválido(s) encontrado(s)
+issues.choose.invalid_config=A configuração da issue contém erros:
 issues.no_ref=Nenhum branch/tag especificado
 issues.create=Criar issue
 issues.new_label=Nova etiqueta
@@ -1427,7 +1433,6 @@ issues.filter_sort.moststars=Mais estrelas
 issues.filter_sort.feweststars=Menos estrelas
 issues.filter_sort.mostforks=Mais forks
 issues.filter_sort.fewestforks=Menos forks
-issues.keyword_search_unavailable=A pesquisa por palavra-chave não está disponível no momento. Entre em contato com o administrador do site.
 issues.action_open=Abrir
 issues.action_close=Fechar
 issues.action_label=Etiqueta
@@ -1480,6 +1485,11 @@ issues.author_helper=Este usuário é o autor.
 issues.role.owner=Proprietário
 issues.role.owner_helper=Este usuário é o dono deste repositório.
 issues.role.member=Membro
+issues.role.collaborator=Colaborador
+issues.role.collaborator_helper=Este usuário foi convidado para colaborar no repositório.
+issues.role.first_time_contributor=Primeira vez contribuindo
+issues.role.first_time_contributor_helper=Esta é a primeira contribuição deste usuário para o repositório.
+issues.role.contributor=Contribuidor
 issues.re_request_review=Re-solicitar revisão
 issues.is_stale=Houve alterações nessa PR desde essa revisão
 issues.remove_request_review=Remover solicitação de revisão
@@ -1495,6 +1505,8 @@ issues.label_description=Descrição da etiqueta
 issues.label_color=Cor da etiqueta
 issues.label_exclusive=Exclusivo
 issues.label_archive=Arquivar etiqueta
+issues.label_archived_filter=Mostrar etiquetas arquivadas
+issues.label_archive_tooltip=Etiquetas arquivadas são excluídas, por padrão, das sugestões ao pesquisar por etiqueta.
 issues.label_exclusive_desc=Nomeie o rótulo <code>escopo/item</code> para torná-lo mutuamente exclusivo com outros rótulos do <code>escopo/</code>.
 issues.label_exclusive_warning=Quaisquer rótulos com escopo conflitantes serão removidos ao editar os rótulos de uma issue ou pull request.
 issues.label_count=%d etiquetas
@@ -1573,6 +1585,7 @@ issues.due_date_form=dd/mm/aaaa
 issues.due_date_form_add=Adicionar data limite
 issues.due_date_form_edit=Editar
 issues.due_date_form_remove=Remover
+issues.due_date_not_writer=Você precisa de acesso de gravação a esse repositório para atualizar a data limite de uma issue.
 issues.due_date_not_set=Data limite não informada.
 issues.due_date_added=adicionou a data limite %s %s
 issues.due_date_modified=modificou a data limite de %[2]s para %[1]s %[3]s
@@ -1669,7 +1682,6 @@ pulls.compare_compare=pull de
 pulls.switch_comparison_type=Mudar tipo de comparação
 pulls.switch_head_and_base=Trocar cabeça e base
 pulls.filter_branch=Filtrar branch
-pulls.no_results=Nada encontrado.
 pulls.show_all_commits=Mostrar todos os commits
 pulls.show_changes_since_your_last_review=Mostrar alterações desde sua última revisão
 pulls.showing_only_single_commit=Mostrando apenas as alterações do commit %[1]s
@@ -1736,6 +1748,7 @@ pulls.merge_pull_request=Criar commit de merge
 pulls.rebase_merge_pull_request=Rebase e fast-forward
 pulls.rebase_merge_commit_pull_request=Rebase e criar commit de merge
 pulls.squash_merge_pull_request=Criar commit de squash
+pulls.fast_forward_only_merge_pull_request=Apenas Fast-forward
 pulls.merge_manually=Merge feito manualmente
 pulls.merge_commit_id=A ID de merge commit
 pulls.require_signed_wont_sign=O branch requer commits assinados, mas este merge não será assinado
@@ -1759,6 +1772,8 @@ pulls.status_checks_failure=Algumas verificações falharam
 pulls.status_checks_error=Algumas verificações reportaram erros
 pulls.status_checks_requested=Obrigatário
 pulls.status_checks_details=Detalhes
+pulls.status_checks_hide_all=Ocultar todas as verificações
+pulls.status_checks_show_all=Mostrar todas as verificações
 pulls.update_branch=Atualizar branch por merge
 pulls.update_branch_rebase=Atualizar branch por rebase
 pulls.update_branch_success=Atualização do branch foi bem-sucedida
@@ -1767,6 +1782,9 @@ pulls.outdated_with_base_branch=Este branch está desatualizado com o branch bas
 pulls.close=Fechar pull request
 pulls.closed_at=`fechou este pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`reabriu este pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_checkout_title=Checkout
+pulls.cmd_instruction_merge_title=Merge
+pulls.cmd_instruction_merge_desc=Faça merge das alterações e atualize no Gitea.
 pulls.clear_merge_message=Limpar mensagem do merge
 pulls.clear_merge_message_hint=Limpar a mensagem de merge só irá remover o conteúdo da mensagem de commit e manter trailers git gerados, como "Co-Authored-By …".
 
@@ -1863,6 +1881,8 @@ wiki.page_name_desc=Digite um nome para esta página Wiki. Alguns nomes especiai
 wiki.original_git_entry_tooltip=Ver o arquivo Git original em vez de usar o link amigável.
 
 activity=Atividade
+activity.navbar.pulse=Pulso
+activity.navbar.contributors=Contribuidores
 activity.period.filter_label=Período:
 activity.period.daily=1 dia
 activity.period.halfweekly=3 dias
@@ -1928,18 +1948,10 @@ activity.git_stats_and_deletions=e
 activity.git_stats_deletion_1=%d exclusão
 activity.git_stats_deletion_n=%d exclusões
 
+contributors.contribution_type.filter_label=Tipo de contribuição:
 contributors.contribution_type.commits=Commits
-
-search=Pesquisar
-search.search_repo=Pesquisar no repositório...
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir resultados que sejam próximos ao termo de busca
-search.match=Corresponde
-search.match.tooltip=Incluir somente resultados que correspondam exatamente ao termo de busca
-search.results=Resultados da pesquisa para "%s" em <a href="%s">%s</a>
-search.code_no_results=Nenhum código-fonte correspondente ao seu termo de pesquisa foi encontrado.
-search.code_search_unavailable=A pesquisa por código não está disponível no momento. Entre em contato com o administrador do site.
+contributors.contribution_type.additions=Adições
+contributors.contribution_type.deletions=Exclusões
 
 settings=Configurações
 settings.desc=Opções é onde você pode gerenciar as configurações para o repositório
@@ -2008,6 +2020,7 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir edições de manten
 settings.releases_desc=Habilitar versões do Repositório
 settings.packages_desc=Habilitar Registro de Pacotes de Repositório
 settings.projects_desc=Habilitar Projetos do Repositório
+settings.projects_mode_all=Todos os projetos
 settings.actions_desc=Habilitar ações do repositório
 settings.admin_settings=Configurações do administrador
 settings.admin_enable_health_check=Habilitar verificações de integridade (git fsck) no repositório
@@ -2079,7 +2092,6 @@ settings.delete_collaborator=Remover
 settings.collaborator_deletion=Remover colaborador
 settings.collaborator_deletion_desc=A exclusão de um colaborador irá revogar o acesso a este repositório. Continuar?
 settings.remove_collaborator_success=O colaborador foi removido.
-settings.search_user_placeholder=Pesquisar usuário...
 settings.org_not_allowed_to_be_collaborator=Organizações não podem ser adicionadas como um colaborador.
 settings.change_team_access_not_allowed=Alteração do acesso da equipe para o repositório está restrito ao proprietário da organização
 settings.team_not_in_organization=A equipe não está na mesma organização que o repositório
@@ -2087,7 +2099,6 @@ settings.teams=Equipes
 settings.add_team=Adicionar Equipe
 settings.add_team_duplicate=A equipe já tem o repositório
 settings.add_team_success=A equipe agora tem acesso ao repositório.
-settings.search_team=Pesquisar Equipe…
 settings.change_team_permission_tip=A permissão da equipe está definida na página de configurações da equipe e não pode ser alterada por repositório
 settings.delete_team_tip=Esta equipe tem acesso a todos os repositórios e não pode ser removida
 settings.remove_team_success=O acesso da equipe ao repositório foi removido.
@@ -2232,9 +2243,7 @@ settings.protect_whitelist_committers=Lista permitida para push
 settings.protect_whitelist_committers_desc=Somente usuários ou equipes da lista permitida serão autorizados realizar push neste branch (mas não forçar o push).
 settings.protect_whitelist_deploy_keys=Dar permissão às chaves de deploy com acesso de gravação para push.
 settings.protect_whitelist_users=Usuários com permissão para realizar push:
-settings.protect_whitelist_search_users=Pesquisar usuários...
 settings.protect_whitelist_teams=Equipes com permissão para realizar push:
-settings.protect_whitelist_search_teams=Pesquisar equipes...
 settings.protect_merge_whitelist_committers=Habilitar controle de permissão de merge
 settings.protect_merge_whitelist_committers_desc=Permitir que determinados usuários ou equipes possam aplicar merge de pull requests neste branch.
 settings.protect_merge_whitelist_users=Usuários com permissão para aplicar merge:
@@ -2301,6 +2310,9 @@ settings.archive.error=Um erro ocorreu enquanto estava sendo arquivado o reposit
 settings.archive.error_ismirror=Você não pode arquivar um repositório espelhado.
 settings.archive.branchsettings_unavailable=Configurações do branch não estão disponíveis quando o repositório está arquivado.
 settings.archive.tagsettings_unavailable=As configurações de tag não estão disponíveis se o repositório estiver arquivado.
+settings.unarchive.button=Desarquivar o repositório
+settings.unarchive.header=Desarquivar este repositório
+settings.unarchive.success=O repositório foi desarquivado com sucesso.
 settings.update_avatar_success=O avatar do repositório foi atualizado.
 settings.lfs=LFS
 settings.lfs_filelist=Arquivos LFS armazenados neste repositório
@@ -2423,6 +2435,7 @@ release.edit_release=Atualizar versão
 release.delete_release=Excluir versão
 release.delete_tag=Apagar Tag
 release.deletion=Excluir versão
+release.deletion_desc=A exclusão de uma versão apenas a remove do Gitea. Isso não afetará a tag do Git, o conteúdo do seu repositório ou seu histórico. Continuar?
 release.deletion_success=A versão foi excluída.
 release.deletion_tag_desc=A tag será excluída do repositório. Conteúdo do repositório e histórico permanecerão inalterados. Continuar?
 release.deletion_tag_success=A tag foi excluída.
@@ -2488,6 +2501,9 @@ error.csv.unexpected=Não é possível renderizar este arquivo porque ele conté
 error.csv.invalid_field_count=Não é possível renderizar este arquivo porque ele tem um número errado de campos na linha %d.
 
 [graphs]
+component_loading=Carregando %s...
+component_loading_failed=Não foi possível carregar %s
+component_loading_info=Isto pode demorar um pouco…
 
 [org]
 org_name_holder=Nome da organização
@@ -2518,6 +2534,7 @@ form.create_org_not_allowed=Você não tem permissão para criar uma organizaç
 settings=Configurações
 settings.options=Organização
 settings.full_name=Nome completo
+settings.email=E-mail de contato
 settings.website=Site
 settings.location=Localização
 settings.permission=Permissões
@@ -2590,7 +2607,6 @@ teams.write_permission_desc=Esta equipe concede acesso para <strong>escrita</str
 teams.admin_permission_desc=Esta equipe concede acesso de <strong>Administrador</strong>: Membros podem ler, fazer push e adicionar outros colaboradores para os repositórios da equipe.
 teams.create_repo_permission_desc=Além disso, esta equipe concede permissão de <strong>Criar repositório</strong>: membros podem criar novos repositórios na organização.
 teams.repositories=Repositórios da equipe
-teams.search_repo_placeholder=Pesquisar repositório...
 teams.remove_all_repos_title=Remover todos os repositórios da equipe
 teams.remove_all_repos_desc=Isto irá remover todos os repositórios da equipe.
 teams.add_all_repos_title=Adicionar todos os repositórios
@@ -2606,11 +2622,13 @@ teams.all_repositories_helper=A equipe tem acesso a todos os repositórios. Sele
 teams.all_repositories_read_permission_desc=Esta equipe concede acesso <strong>Leitura</strong> a <strong>todos os repositórios</strong>: membros podem ver e clonar repositórios.
 teams.all_repositories_write_permission_desc=Esta equipe concede acesso <strong>Escrita</strong> a <strong>todos os repositórios</strong>: os membros podem ler de e fazer push para os repositórios.
 teams.all_repositories_admin_permission_desc=Esta equipe concede acesso <strong>Administrativo</strong> a <strong>todos os repositórios</strong>: os membros podem ler, fazer push e adicionar colaboradores aos repositórios.
+teams.invite.title=Você foi convidado para fazer parte da equipe <strong>%s</strong> na organização <strong>%s</strong>.
 teams.invite.by=Convidado por %s
 teams.invite.description=Por favor, clique no botão abaixo para se juntar à equipe.
 
 [admin]
 dashboard=Painel
+identity_access=Identidade e acesso
 users=Contas de usuário
 organizations=Organizações
 repositories=Repositórios
@@ -2619,11 +2637,14 @@ integrations=Integrações
 authentication=Fontes de autenticação
 emails=E-mails do Usuário
 config=Configuração
+config_summary=Resumo
+config_settings=Configurações
 notices=Avisos do sistema
 monitor=Monitoramento
 first_page=Primeira
 last_page=Última
 total=Total: %d
+settings=Configurações de Administrador
 
 dashboard.new_version_hint=Uma nova versão está disponível: %s. Versão atual: %s. Visite <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">o blog</a> para mais informações.
 dashboard.statistic=Resumo
@@ -2782,9 +2803,6 @@ repos.unadopted.no_more=Não foram encontrados mais repositórios não adotados
 repos.owner=Proprietário
 repos.name=Nome
 repos.private=Privado
-repos.watches=Observadores
-repos.stars=Favoritos
-repos.forks=Forks
 repos.issues=Issues
 repos.size=Tamanho
 repos.lfs_size=Tamanho do LFS
@@ -2906,7 +2924,6 @@ auths.tip.nextcloud=`Registre um novo consumidor OAuth em sua instância usando
 auths.tip.dropbox=Criar um novo aplicativo em https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Cadastrar um novo aplicativo em https://developers.facebook.com/apps e adicionar o produto "Facebook Login"`
 auths.tip.github=Cadastrar um novo aplicativo de OAuth na https://github.com/settings/applications/new
-auths.tip.gitlab=Cadastrar um novo aplicativo em https://gitlab.com/profile/applications
 auths.tip.google_plus=Obter credenciais de cliente OAuth2 do console de API do Google em https://console.developers.google.com/
 auths.tip.openid_connect=Use o OpenID Connect Discovery URL (<servidor>/.well-known/openid-configuration) para especificar os endpoints
 auths.tip.twitter=Vá em https://dev.twitter.com/apps, crie um aplicativo e certifique-se de que está habilitada a opção “Allow this application to be used to Sign in with Twitter“
@@ -3414,13 +3431,20 @@ runners.status.idle=Inativo
 runners.status.active=Ativo
 runners.status.offline=Offiline
 runners.version=Versão
+runners.reset_registration_token=Redefinir token de registro
 runners.reset_registration_token_success=Token de registro de runner redefinido com sucesso
 
 runs.all_workflows=Todos os Workflows
 runs.commit=Commit
+runs.scheduled=Agendado
 runs.pushed_by=push feito por
 runs.invalid_workflow_helper=O arquivo de configuração do workflow é inválido. Por favor, verifique seu arquivo de configuração: %s
+runs.actor=Ator
 runs.status=Status
+runs.actors_no_select=Todos os atores
+runs.status_no_select=Todos os Status
+runs.no_results=Não houve correspondência de resultados.
+runs.empty_commit_message=(mensagem de commit vazia)
 
 
 need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork.
@@ -3433,5 +3457,9 @@ type-3.display_name=Projeto da organização
 
 [git.filemode]
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Diretório
+normal_file=Arquivo normal
+executable_file=Arquivo executável
 symbolic_link=Link simbólico
+submodule=Submódulo
 
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 99165ed332..ea80cd7abb 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -25,6 +25,7 @@ enable_javascript=Este sítio Web requer JavaScript.
 toc=Índice
 licenses=Licenças
 return_to_gitea=Retornar ao Gitea
+more_items=Mais itens
 
 username=Nome de utilizador
 email=Endereço de email
@@ -113,6 +114,7 @@ loading=Carregando…
 error=Erro
 error404=A página que pretende aceder <strong>não existe</strong> ou <strong>não tem autorização</strong> para a ver.
 go_back=Voltar
+invalid_data=Dados inválidos: %v
 
 never=Nunca
 unknown=Desconhecido
@@ -142,6 +144,43 @@ confirm_delete_selected=Confirma a exclusão de todos os itens marcados?
 name=Nome
 value=Valor
 
+filter=Filtro
+filter.clear=Retirar filtro
+filter.is_archived=Arquivado
+filter.not_archived=Não arquivado
+filter.is_fork=Derivado
+filter.not_fork=Não derivado
+filter.is_mirror=Replicado
+filter.not_mirror=Não replicado
+filter.is_template=Modelo
+filter.not_template=Não é modelo
+filter.public=Público
+filter.private=Privado
+
+no_results_found=Não foram encontrados quaisquer resultados.
+
+[search]
+search=Pesquisar...
+type_tooltip=Tipo de pesquisa
+fuzzy=Aproximada
+fuzzy_tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
+match=Fiel
+match_tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
+repo_kind=Pesquisar repositórios...
+user_kind=Pesquisar utilizadores...
+org_kind=Pesquisar organizações...
+team_kind=Pesquisar equipas...
+code_kind=Pesquisar código...
+code_search_unavailable=A pesquisa de código não está disponível, neste momento. Entre em contacto com o administrador.
+code_search_by_git_grep=Os resultados da pesquisa no código-fonte neste momento são fornecidos pelo "git grep". Esses resultados podem ser melhores se o administrador habilitar o indexador do repositório.
+package_kind=Pesquisar pacotes...
+project_kind=Pesquisar planeamentos...
+branch_kind=Pesquisar ramos...
+commit_kind=Pesquisar cometimentos...
+runner_kind=Pesquisar executores...
+no_results=Não foram encontrados resultados correspondentes.
+keyword_search_unavailable=Pesquisar por palavra-chave não está disponível, neste momento. Entre em contacto com o administrador.
+
 [aria]
 navbar=Barra de navegação
 footer=Rodapé
@@ -247,6 +286,7 @@ email_title=Configurações de email
 smtp_addr=Servidor SMTP
 smtp_port=Porto do SMTP
 smtp_from=Email do remetente
+smtp_from_invalid=O endereço para "Enviar email como" é inválido
 smtp_from_helper=Endereço de email que o Gitea vai usar. Insira um endereço de email simples ou use o formato "Nome" <email@exemplo.com>.
 mailer_user=Nome de utilizador do SMTP
 mailer_password=Senha do SMTP
@@ -306,6 +346,7 @@ env_config_keys=Configuração do ambiente
 env_config_keys_prompt=As seguintes variáveis de ambiente também serão aplicadas ao seu ficheiro de configuração:
 
 [home]
+nav_menu=Menu de navegação
 uname_holder=Nome de utilizador ou endereço de email
 password_holder=Senha
 switch_dashboard_context=Trocar contexto do painel
@@ -315,7 +356,6 @@ collaborative_repos=Repositórios colaborativos
 my_orgs=As minhas organizações
 my_mirrors=As minhas réplicas
 view_home=Ver %s
-search_repos=Procurar um repositório…
 filter=Outros filtros
 filter_by_team_repositories=Filtrar por repositórios da equipa
 feed_of=`Fonte de "%s"`
@@ -336,20 +376,8 @@ issues.in_your_repos=Nos seus repositórios
 repos=Repositórios
 users=Utilizadores
 organizations=Organizações
-search=Procurar
 go_to=Ir para
 code=Código
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
-search.match=Fiel
-search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
-code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador.
-repo_no_results=Não foram encontrados quaisquer repositórios correspondentes.
-user_no_results=Não foram encontrados quaisquer utilizadores correspondentes.
-org_no_results=Não foram encontradas quaisquer organizações correspondentes.
-code_no_results=Não foi encontrado qualquer código-fonte correspondente à sua pesquisa.
-code_search_results=`Resultados da pesquisa para "%s"`
 code_last_indexed_at=Última indexação %s
 relevant_repositories_tooltip=Repositórios que são derivações ou que não têm tópico, nem ícone, nem descrição, estão escondidos.
 relevant_repositories=Apenas estão a ser mostrados os repositórios relevantes. <a href="%s">Mostrar resultados não filtrados</a>.
@@ -367,7 +395,7 @@ forgot_password_title=Esqueci-me da senha
 forgot_password=Esqueceu a sua senha?
 sign_up_now=Precisa de uma conta? Inscreva-se agora.
 sign_up_successful=A conta foi criada com sucesso. Bem-vindo/a!
-confirmation_mail_sent_prompt=Foi enviado um novo email de confirmação para <b>%s</b>. Verifique a sua caixa de entrada dentro de %s para completar o processo de inscrição.
+confirmation_mail_sent_prompt_ex=Foi enviado um email de confirmação para <b>%s</b>. Verifique a sua caixa de entrada dentro de %s para completar o processo de registo. Se o seu endereço de email de registo estiver errado, pode iniciar a sessão novamente e mudá-lo.
 must_change_password=Mude a sua senha
 allow_password_change=Exigir que o utilizador mude a senha (recomendado)
 reset_password_mail_sent_prompt=Foi enviado um email de confirmação para <b>%s</b>. Verifique a sua caixa de entrada dentro de %s para completar o processo de recuperação.
@@ -377,6 +405,7 @@ prohibit_login=Início de sessão proibido
 prohibit_login_desc=A sua conta está proibida de iniciar sessão. Contacte o administrador.
 resent_limit_prompt=Já fez um pedido recentemente para enviar um email para pôr a conta em funcionamento. Espere 3 minutos e tente novamente.
 has_unconfirmed_mail=Olá %s, tem um endereço de email não confirmado (<b>%s</b>). Se não recebeu um email de confirmação ou precisa de o voltar a enviar, clique no botão abaixo.
+change_unconfirmed_mail_address=Se o seu endereço de email estiver errado, pode mudá-lo aqui e enviar um novo email de confirmação.
 resend_mail=Clique aqui para voltar a enviar um email para pôr a conta em funcionamento
 email_not_associate=O endereço de email não está associado a qualquer conta.
 send_reset_mail=Enviar email de recuperação da conta
@@ -557,6 +586,7 @@ team_name_been_taken=O nome da equipa já foi tomado.
 team_no_units_error=Permitir acesso a pelo menos uma secção do repositório.
 email_been_used=O endereço de email já está em uso.
 email_invalid=O endereço de email é inválido.
+email_domain_is_not_allowed=O domínio do email de utilizador <b>%s</b> entra en conflito com o EMAIL_DOMAIN_ALLOWLIST ou com o EMAIL_DOMAIN_BLOCKLIST. Verifique se a operação estava prevista.
 openid_been_used=O endereço OpenID "%s" já está em uso.
 username_password_incorrect=O nome de utilizador ou a senha estão errados.
 password_complexity=A senha não passa nos requisitos de complexidade:
@@ -568,6 +598,8 @@ enterred_invalid_repo_name=O nome do repositório que inseriu está errado.
 enterred_invalid_org_name=O nome da organização que inseriu está errado.
 enterred_invalid_owner_name=O novo nome de proprietário não é válido.
 enterred_invalid_password=A senha que inseriu está errada.
+unset_password=O utilizador não definiu a senha.
+unsupported_login_type=O tipo de início de sessão não é suportado para eliminar a conta.
 user_not_exist=O utilizador não existe.
 team_not_exist=A equipa não existe.
 last_org_owner=Não pode remover o último utilizador da equipa 'proprietários'. Tem que haver pelo menos um proprietário numa organização.
@@ -617,6 +649,30 @@ form.name_reserved=O nome de utilizador "%s" está reservado.
 form.name_pattern_not_allowed=O padrão "%s" não é permitido no nome de utilizador.
 form.name_chars_not_allowed=O nome de utilizador "%s" contém caracteres inválidos.
 
+block.block=Bloquear
+block.block.user=Bloquear utilizador
+block.block.org=Bloquear utilizador para a organização
+block.block.failure=Falhou o bloqueio do utilizador: %s
+block.unblock=Desbloquear
+block.unblock.failure=Falhou o desbloqueio do utilizador: %s
+block.blocked=Bloqueou este utilizador.
+block.title=Bloquear um utilizador
+block.info=Bloquear um utilizador evita que este interaja com repositórios, tal como abrir ou comentar em pedidos de integração ou questões. Saiba mais sobre como bloquear um utilizador.
+block.info_1=Bloquear um utilizador impede as seguintes operações na sua conta e nos seus repositórios:
+block.info_2=seguir a sua conta
+block.info_3=enviar-lhe notificações ao @mencionar o seu nome de utilizador
+block.info_4=convidá-lo/a para ser colaborador/a nos repositórios dele/dela
+block.info_5=juntar aos favoritos, derivar ou vigiar repositórios
+block.info_6=abrir e comentar questões ou pedidos de integração
+block.info_7=reagir aos seus comentários em questões ou pedidos de integração
+block.user_to_block=Utilizador a bloquear
+block.note=Nota
+block.note.title=Nota opcional:
+block.note.info=A nota não é visível para o utilizador bloqueado.
+block.note.edit=Editar nota
+block.list=Utilizadores bloqueados
+block.list.none=Você ainda não bloqueou quaisquer utilizadores.
+
 [settings]
 profile=Perfil
 account=Conta
@@ -761,7 +817,6 @@ gpg_invalid_token_signature=A chave GPG, assinatura ou código fornecidos não c
 gpg_token_required=Tem que fornecer uma assinatura para o código abaixo
 gpg_token=Código
 gpg_token_help=Pode gerar uma assinatura usando o seguinte comando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Assinatura GPG blindada (com armadura ASCII)
 key_signature_gpg_placeholder=Começa com '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=A chave GPG "%s" foi validada.
@@ -954,8 +1009,9 @@ fork_visibility_helper=A visibilidade de um repositório derivado não poderá s
 fork_branch=Ramo a ser clonado para a derivação
 all_branches=Todos os ramos
 fork_no_valid_owners=Não pode fazer uma derivação deste repositório porque não existem proprietários válidos.
+fork.blocked_user=Não pode derivar o repositório porque foi bloqueado/a pelo/a proprietário/a do repositório.
 use_template=Usar este modelo
-clone_in_vsc=Clonar no VS Code
+open_with_editor=Abrir com %s
 download_zip=Descarregar ZIP
 download_tar=Descarregar TAR.GZ
 download_bundle=Descarregar PACOTE
@@ -1008,6 +1064,7 @@ watchers=Vigilantes
 stargazers=Fãs
 stars_remove_warning=Isto irá remover todas as marcas de favoritos deste repositório.
 forks=Derivações
+stars=Favoritos
 reactions_more=e mais %d
 unit_disabled=O administrador desabilitou esta secção do repositório.
 language_other=Outros
@@ -1129,6 +1186,7 @@ watch=Vigiar
 unstar=Tirar dos favoritos
 star=Juntar aos favoritos
 fork=Derivar
+action.blocked_user=Não pode realizar a operação porque foi bloqueado/a pelo/a proprietário/a do repositório.
 download_archive=Descarregar repositório
 more_operations=Mais operações
 
@@ -1257,6 +1315,8 @@ editor.file_editing_no_longer_exists=O ficheiro que está a ser editado, "%s", j
 editor.file_deleting_no_longer_exists=O ficheiro que está a ser eliminado, "%s", já não existe neste repositório.
 editor.file_changed_while_editing=O conteúdo do ficheiro mudou desde que começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver as modificações ou clique em <strong>Cometer novamente</strong> para escrever por cima.
 editor.file_already_exists=Já existe um ficheiro com o nome "%s" neste repositório.
+editor.commit_id_not_matching=O ID do cometimento não corresponde ao ID de quando começou a editar. Faça o cometimento para um ramo de remendo (patch) e depois faça a integração.
+editor.push_out_of_date=O envio parece estar obsoleto.
 editor.commit_empty_file_header=Cometer um ficheiro vazio
 editor.commit_empty_file_text=O ficheiro que está prestes a cometer está vazio. Quer continuar?
 editor.no_changes_to_show=Não existem modificações para mostrar.
@@ -1280,9 +1340,8 @@ commits.desc=Navegar pelo histórico de modificações no código fonte.
 commits.commits=Cometimentos
 commits.no_commits=Não há cometimentos em comum. "%s" e "%s" têm históricos completamente diferentes.
 commits.nothing_to_compare=Estes ramos são iguais.
-commits.search=Procurar cometimentos…
 commits.search.tooltip=Pode prefixar palavras-chave com "author:", "committer:", "after:", ou "before:". Por exemplo: "revert author:Alice before:2019-01-13".
-commits.find=Procurar
+commits.search_branch=Este ramo
 commits.search_all=Todos os ramos
 commits.author=Autor(a)
 commits.message=Mensagem
@@ -1333,7 +1392,6 @@ projects.type.basic_kanban=Kanban básico
 projects.type.bug_triage=Triagem de erros
 projects.template.desc=Modelo de planeamento
 projects.template.desc_helper=Escolha um modelo de planeamento para começar
-projects.type.uncategorized=Sem categoria
 projects.column.edit=Editar coluna
 projects.column.edit_title=Nome
 projects.column.new_title=Nome
@@ -1341,8 +1399,6 @@ projects.column.new_submit=Criar coluna
 projects.column.new=Nova coluna
 projects.column.set_default=Tornar predefinida
 projects.column.set_default_desc=Definir esta coluna como a predefinida para questões e pedidos de integração não categorizados
-projects.column.unset_default=Deixar de ser a predefinida
-projects.column.unset_default_desc=Faz com que esta coluna deixe de ser a predefinida
 projects.column.delete=Eliminar coluna
 projects.column.deletion_desc=Eliminar uma coluna de um planeamento faz com que todas as questões que nela constam sejam movidas para a coluna 'Sem categoria'. Continuar?
 projects.column.color=Colorido
@@ -1379,6 +1435,8 @@ issues.new.assignees=Encarregados
 issues.new.clear_assignees=Retirar todos os encarregados
 issues.new.no_assignees=Sem encarregados
 issues.new.no_reviewers=Sem revisores
+issues.new.blocked_user=Não pode criar a questão porque foi bloqueado/a pelo/a proprietário/a do repositório.
+issues.edit.blocked_user=Não pode editar o conteúdo porque foi bloqueado/a pelo/a remetente ou pelo/a proprietário/a do repositório.
 issues.choose.get_started=Começar
 issues.choose.open_external_link=Abrir
 issues.choose.blank=Padrão
@@ -1456,7 +1514,6 @@ issues.filter_sort.moststars=Favorito (decrescente)
 issues.filter_sort.feweststars=Favorito (crescente)
 issues.filter_sort.mostforks=Mais derivações
 issues.filter_sort.fewestforks=Menos derivações
-issues.keyword_search_unavailable=A pesquisa por palavra-chave não está disponível, neste momento. Entre em contacto com o administrador.
 issues.action_open=Abrir
 issues.action_close=Fechar
 issues.action_label=Rótulo
@@ -1494,6 +1551,7 @@ issues.close_comment_issue=Comentar e fechar
 issues.reopen_issue=Reabrir
 issues.reopen_comment_issue=Comentar e reabrir
 issues.create_comment=Comentar
+issues.comment.blocked_user=Não pode criar ou editar o comentário porque foi bloqueado/a pelo remetente ou pelo/a proprietário/a do repositório.
 issues.closed_at=`encerrou esta questão <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`reabriu esta questão <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.commit_ref_at=`referenciou esta questão num cometimento <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@@ -1692,6 +1750,7 @@ compare.compare_head=comparar
 
 pulls.desc=Habilitar pedidos de integração e revisão de código.
 pulls.new=Novo pedido de integração
+pulls.new.blocked_user=Não pode criar o pedido de integração porque foi bloqueado/a pelo/a proprietário/a do repositório.
 pulls.view=Ver pedido de integração
 pulls.compare_changes=Novo pedido de integração
 pulls.allow_edits_from_maintainers=Permitir edições por parte dos responsáveis
@@ -1708,7 +1767,6 @@ pulls.compare_compare=puxar de
 pulls.switch_comparison_type=Trocar o tipo de comparação
 pulls.switch_head_and_base=Trocar o topo com a base
 pulls.filter_branch=Filtrar ramo
-pulls.no_results=Não foram encontrados quaisquer resultados.
 pulls.show_all_commits=Mostrar todos os cometimentos
 pulls.show_changes_since_your_last_review=Mostrar modificações desde a sua última revisão
 pulls.showing_only_single_commit=Mostrando apenas as modificações do comentimento %[1]s
@@ -1914,7 +1972,9 @@ wiki.original_git_entry_tooltip=Ver o ficheiro Git original, ao invés de usar u
 
 activity=Trabalho
 activity.navbar.pulse=Pulso
+activity.navbar.code_frequency=Frequência de programação
 activity.navbar.contributors=Contribuidores
+activity.navbar.recent_commits=Cometimentos recentes
 activity.period.filter_label=Período:
 activity.period.daily=1 dia
 activity.period.halfweekly=3 dias
@@ -1985,17 +2045,6 @@ contributors.contribution_type.commits=Cometimentos
 contributors.contribution_type.additions=Adições
 contributors.contribution_type.deletions=Eliminações
 
-search=Procurar
-search.search_repo=Procurar repositório
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
-search.match=Fiel
-search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
-search.results=Resultados da procura de "%s" em <a href="%s">%s</a>
-search.code_no_results=Não foi encontrado qualquer código-fonte correspondente à sua pesquisa.
-search.code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador.
-
 settings=Configurações
 settings.desc=Configurações é onde pode gerir as configurações do repositório
 settings.options=Repositório
@@ -2044,6 +2093,8 @@ settings.branches.add_new_rule=Adicionar nova regra
 settings.advanced_settings=Configurações avançadas
 settings.wiki_desc=Habilitar wiki do repositório
 settings.use_internal_wiki=Usar o wiki nativo
+settings.default_wiki_branch_name=Nome do ramo predefinido do wiki
+settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki.
 settings.use_external_wiki=Usar um wiki externo
 settings.external_wiki_url=URL do wiki externo
 settings.external_wiki_url_error=O URL do wiki externo não é um URL válido.
@@ -2074,6 +2125,10 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir, por norma, que os
 settings.releases_desc=Habilitar lançamentos no repositório
 settings.packages_desc=Habilitar o registo de pacotes do repositório
 settings.projects_desc=Habilitar planeamentos no repositório
+settings.projects_mode_desc=Modo de planeamentos (tipos de planeamentos a mostrar)
+settings.projects_mode_repo=Apenas planeamentos de repositórios
+settings.projects_mode_owner=Apenas planeamentos de utilizadores ou de organizações
+settings.projects_mode_all=Todos os planeamentos
 settings.actions_desc=Habilitar operações no repositório (Gitea Actions)
 settings.admin_settings=Configurações do administrador
 settings.admin_enable_health_check=Habilitar verificações de integridade (git fsck) no repositório
@@ -2099,6 +2154,7 @@ settings.convert_fork_succeed=A derivação foi convertida num repositório norm
 settings.transfer=Transferir a propriedade
 settings.transfer.rejected=A transferência do repositório foi rejeitada.
 settings.transfer.success=A transferência do repositório foi bem sucedida.
+settings.transfer.blocked_user=Não foi possível transferir o repositório porque foi bloqueado/a pelo/a novo/a proprietário/a.
 settings.transfer_abort=Cancelar a transferência
 settings.transfer_abort_invalid=Não pode cancelar a transferência de um repositório inexistente.
 settings.transfer_abort_success=A transferência de repositório para %s foi cancelada com sucesso.
@@ -2144,11 +2200,11 @@ settings.add_collaborator_success=O colaborador foi adicionado.
 settings.add_collaborator_inactive_user=Não é possível adicionar um utilizador desabilitado como colaborador.
 settings.add_collaborator_owner=Não é possível adicionar um proprietário como um colaborador.
 settings.add_collaborator_duplicate=O colaborador já tinha sido adicionado a este repositório.
+settings.add_collaborator.blocked_user=O/A colaborador/a foi bloqueado/a pelo/a proprietário/a do repositório ou vice-versa.
 settings.delete_collaborator=Remover
 settings.collaborator_deletion=Remover colaborador
 settings.collaborator_deletion_desc=Remover um colaborador irá revogar o seu acesso a este repositório. Quer continuar?
 settings.remove_collaborator_success=O colaborador foi removido.
-settings.search_user_placeholder=Procurar utilizador…
 settings.org_not_allowed_to_be_collaborator=As organizações não podem ser adicionadas como colaborador.
 settings.change_team_access_not_allowed=Alterar o acesso da equipa ao repositório foi restrito ao proprietário da organização
 settings.team_not_in_organization=A equipa não está na mesma organização que o repositório
@@ -2156,7 +2212,6 @@ settings.teams=Equipas
 settings.add_team=Adicionar equipa
 settings.add_team_duplicate=A equipa já tem o repositório
 settings.add_team_success=A equipa agora tem acesso ao repositório.
-settings.search_team=Procurar equipa…
 settings.change_team_permission_tip=A permissão da equipa é definida na página de configurações da equipa e não pode ter modificações específicas de cada repositório
 settings.delete_team_tip=Esta equipa tem acesso a todos os repositórios e não pode ser removida
 settings.remove_team_success=O acesso da equipa ao repositório foi removido.
@@ -2309,9 +2364,7 @@ settings.protect_whitelist_committers=Lista de permissões para restringir os en
 settings.protect_whitelist_committers_desc=Apenas os utilizadores ou equipas constantes na lista terão permissão para enviar para este ramo (mas não poderão fazer envios forçados).
 settings.protect_whitelist_deploy_keys=Dar permissão às chaves de instalação para terem acesso de escrita para enviar.
 settings.protect_whitelist_users=Utilizadores com permissão para enviar:
-settings.protect_whitelist_search_users=Procurar utilizadores…
 settings.protect_whitelist_teams=Equipas com permissão para enviar:
-settings.protect_whitelist_search_teams=Procurar equipas…
 settings.protect_merge_whitelist_committers=Habilitar lista de permissão para integrar
 settings.protect_merge_whitelist_committers_desc=Permitir que somente utilizadores ou equipas constantes na lista de permissão possam executar, neste ramo, integrações constantes em pedidos de integração.
 settings.protect_merge_whitelist_users=Utilizadores com permissão para executar integrações:
@@ -2556,7 +2609,6 @@ branch.default_deletion_failed=O ramo "%s" é o ramo principal, não pode ser el
 branch.restore=`Restaurar o ramo "%s"`
 branch.download=`Descarregar o ramo "%s"`
 branch.rename=`Renomear ramo "%s"`
-branch.search=Pesquisar ramo
 branch.included_desc=Este ramo faz parte do ramo principal
 branch.included=Incluído
 branch.create_new_branch=Criar ramo a partir do ramo:
@@ -2587,13 +2639,16 @@ find_file.no_matching=Não foi encontrado qualquer ficheiro correspondente
 error.csv.too_large=Não é possível apresentar este ficheiro por ser demasiado grande.
 error.csv.unexpected=Não é possível apresentar este ficheiro porque contém um caractere inesperado na linha %d e coluna %d.
 error.csv.invalid_field_count=Não é possível apresentar este ficheiro porque tem um número errado de campos na linha %d.
+error.broken_git_hook=Os automatismos git deste repositório parecem estar danificados. Consulte a <a target="_blank" rel="noreferrer" href="%s">documentação</a> sobre como os consertar e depois envie alguns cometimentos para refrescar o estado.
 
 [graphs]
 component_loading=A carregar %s...
 component_loading_failed=Não foi possível carregar %s
 component_loading_info=Isto pode demorar um pouco…
 component_failed_to_load=Ocorreu um erro inesperado.
+code_frequency.what=frequência de programação
 contributors.what=contribuições
+recent_commits.what=cometimentos recentes
 
 [org]
 org_name_holder=Nome da organização
@@ -2699,7 +2754,6 @@ teams.write_permission_desc=Esta equipa atribui acesso de <strong>escrita</stron
 teams.admin_permission_desc=Esta equipa atribui o acesso de <strong>administração</strong>: os seus membros podem ler de, enviar para, e adicionar colaboradores aos repositórios da equipa.
 teams.create_repo_permission_desc=Adicionalmente, esta equipa atribui a permissão de <strong>criar repositórios</strong>: os seus membros podem criar novos repositórios na organização.
 teams.repositories=Repositórios da equipa
-teams.search_repo_placeholder=Procurar repositório…
 teams.remove_all_repos_title=Remover todos os repositórios da equipa
 teams.remove_all_repos_desc=Isto irá remover todos os repositórios da equipa.
 teams.add_all_repos_title=Adicionar todos os repositórios
@@ -2708,6 +2762,7 @@ teams.add_nonexistent_repo=O repositório que está a tentar adicionar não exis
 teams.add_duplicate_users=O utilizador já é um membro da equipa.
 teams.repos.none=Não há repositórios que possam ser acedidos por esta equipa.
 teams.members.none=Não há membros nesta equipa.
+teams.members.blocked_user=Não foi possível adicionar o/a utilizador/a porque essa operação foi bloqueada pela organização.
 teams.specific_repositories=Repositórios específicos
 teams.specific_repositories_helper=Os membros só terão acesso a repositórios explicitamente adicionados à equipa. Escolher isto <strong>não irá</strong> remover automaticamente os repositórios já adicionados com <i>Todos os repositórios</i>.
 teams.all_repositories=Todos os repositórios
@@ -2732,6 +2787,8 @@ integrations=Integrações
 authentication=Fontes de autenticação
 emails=Emails do utilizador
 config=Configuração
+config_summary=Resumo
+config_settings=Configurações
 notices=Notificações do sistema
 monitor=Monitorização
 first_page=Primeira
@@ -2908,9 +2965,6 @@ repos.unadopted.no_more=Não foram encontrados mais repositórios não adoptados
 repos.owner=Proprietário(a)
 repos.name=Nome
 repos.private=Privado
-repos.watches=Vigilâncias
-repos.stars=Favoritos
-repos.forks=Derivações
 repos.issues=Questões
 repos.size=Tamanho
 repos.lfs_size=Tamanho do LFS
@@ -3035,7 +3089,7 @@ auths.tip.nextcloud=`Registe um novo consumidor OAuth na sua instância usando o
 auths.tip.dropbox=Crie uma nova aplicação em https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Registe uma nova aplicação em https://developers.facebook.com/apps e adicione o produto "Facebook Login"`
 auths.tip.github=Registe uma nova aplicação OAuth em https://github.com/settings/applications/new
-auths.tip.gitlab=Registe uma nova aplicação em https://gitlab.com/profile/applications
+auths.tip.gitlab_new=Registe uma nova aplicação em https://gitlab.com/-/profile/applications
 auths.tip.google_plus=Obtenha credenciais de cliente OAuth2 a partir da consola do Google API em https://console.developers.google.com/
 auths.tip.openid_connect=Use o URL da descoberta de conexão OpenID (<server>/.well-known/openid-configuration) para especificar os extremos
 auths.tip.twitter=`Vá a https://dev.twitter.com/apps, crie uma aplicação e certifique-se de que está habilitada a opção "Allow this application to be used to Sign in with Twitter"`
@@ -3171,6 +3225,7 @@ config.picture_config=Configuração da imagem e do avatar
 config.picture_service=Serviço de imagem
 config.disable_gravatar=Desabilitar o Gravatar
 config.enable_federated_avatar=Habilitar avatares federados
+config.open_with_editor_app_help=Os editores de "Abrir com" do menu de clonagem. Se for deixado em branco, será usado o predefinido. Expanda para ver o predefinido.
 
 config.git_config=Configuração Git
 config.git_disable_diff_highlight=Desabilitar o realce de sintaxe no diff
@@ -3569,6 +3624,7 @@ runs.scheduled=Agendadas
 runs.pushed_by=enviado por
 runs.invalid_workflow_helper=O ficheiro de configuração da sequência de trabalho é inválido. Verifique o seu ficheiro de configuração: %s
 runs.no_matching_online_runner_helper=Não existem executores ligados que tenham o rótulo %s
+runs.no_job_without_needs=A sequência de trabalho tem que conter pelo menos um trabalho sem dependências.
 runs.actor=Interveniente
 runs.status=Estado
 runs.actors_no_select=Todos os intervenientes
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 36b9d0e39e..74c4c9c935 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -139,6 +139,15 @@ confirm_delete_selected=Вы уверены, что хотите удалить
 name=Название
 value=Значение
 
+filter=Фильтр
+filter.is_archived=Архивировано
+filter.is_template=Шаблон
+filter.public=Публичный
+filter.private=Личный
+
+
+[search]
+
 [aria]
 navbar=Панель навигации
 footer=Подвал
@@ -312,7 +321,6 @@ collaborative_repos=Совместные репозитории
 my_orgs=Мои организации
 my_mirrors=Мои зеркала
 view_home=Показать %s
-search_repos=Поиск репозитория…
 filter=Другие фильтры
 filter_by_team_repositories=Фильтровать по репозиториям команды
 feed_of=Лента «%s»
@@ -333,20 +341,8 @@ issues.in_your_repos=В ваших репозиториях
 repos=Репозитории
 users=Пользователи
 organizations=Организации
-search=Поиск
 go_to=Перейти к
 code=Код
-search.type.tooltip=Тип поиска
-search.fuzzy=Неточный
-search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу
-search.match=Соответствие
-search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу
-code_search_unavailable=В настоящее время поиск по коду недоступен. Обратитесь к администратору сайта.
-repo_no_results=Подходящие репозитории не найдены.
-user_no_results=Подходящие пользователи не найдены.
-org_no_results=Подходящие организации не найдены.
-code_no_results=Соответствующий поисковому запросу исходный код не найден.
-code_search_results=Результаты поиска «%s»
 code_last_indexed_at=Последний проиндексированный %s
 relevant_repositories_tooltip=Репозитории, являющиеся ответвлениями или не имеющие ни темы, ни значка, ни описания, скрыты.
 relevant_repositories=Показаны только релевантные репозитории, <a href="%s">показать результаты без фильтрации</a>.
@@ -364,7 +360,6 @@ forgot_password_title=Восстановить пароль
 forgot_password=Забыли пароль?
 sign_up_now=Нужен аккаунт? Зарегистрируйтесь.
 sign_up_successful=Учётная запись успешно создана. Добро пожаловать!
-confirmation_mail_sent_prompt=Новое письмо для подтверждения направлено на <b>%s</b>. Пожалуйста, проверьте ваш почтовый ящик в течение %s для завершения регистрации.
 must_change_password=Обновить пароль
 allow_password_change=Требовать смену пароля пользователем (рекомендуется)
 reset_password_mail_sent_prompt=Письмо с подтверждением отправлено на <b>%s</b>. Пожалуйста, проверьте входящую почту в течение %s, чтобы завершить процесс восстановления аккаунта.
@@ -612,6 +607,7 @@ form.name_reserved=Имя пользователя «%s» зарезервиро
 form.name_pattern_not_allowed=Шаблон «%s» не допускается в имени пользователя.
 form.name_chars_not_allowed=Имя пользователя «%s» содержит недопустимые символы.
 
+
 [settings]
 profile=Профиль
 account=Аккаунт
@@ -755,7 +751,6 @@ gpg_invalid_token_signature=Предоставленный ключ GPG, под
 gpg_token_required=Вы должны предоставить подпись для токена ниже
 gpg_token=Токен
 gpg_token_help=Вы можете сгенерировать подпись с помощью:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Текстовая подпись GPG
 key_signature_gpg_placeholder=Начинается с '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=Ключ GPG «%s» верифицирован.
@@ -942,7 +937,6 @@ fork_visibility_helper=Видимость форкнутого репозито
 fork_branch=Ветка для клонирования в форк
 all_branches=Все ветки
 use_template=Использовать этот шаблон
-clone_in_vsc=Клонировать в VS Code
 download_zip=Скачать ZIP
 download_tar=Скачать TAR.GZ
 download_bundle=Скачать BUNDLE
@@ -1249,9 +1243,7 @@ commits.desc=Просмотр истории изменений исходног
 commits.commits=Коммитов
 commits.no_commits=Нет общих коммитов. «%s» и «%s» имеют совершенно разные истории.
 commits.nothing_to_compare=Эти ветки одинаковы.
-commits.search=Поиск коммитов…
 commits.search.tooltip=Можно предварять ключевые слова префиксами "author:", "committer:", "after:", или "before:", например "revert author:Alice before:2019-01-13".
-commits.find=Поиск
 commits.search_all=Все ветки
 commits.author=Автор
 commits.message=Сообщение
@@ -1301,7 +1293,6 @@ projects.type.basic_kanban=Обычный Канбан
 projects.type.bug_triage=Планирование работы с багами
 projects.template.desc=Шаблон проекта
 projects.template.desc_helper=Выберите шаблон проекта для начала
-projects.type.uncategorized=Без категории
 projects.column.edit=Изменить столбец
 projects.column.edit_title=Название
 projects.column.new_title=Название
@@ -1309,10 +1300,7 @@ projects.column.new_submit=Создать столбец
 projects.column.new=Новый столбец
 projects.column.set_default=Установить по умолчанию
 projects.column.set_default_desc=Назначить этот столбец по умолчанию для неклассифицированных задач и запросов на слияние
-projects.column.unset_default=Снять установку по умолчанию
-projects.column.unset_default_desc=Снять установку этого столбца по умолчанию
 projects.column.delete=Удалить столбец
-projects.column.deletion_desc=При удалении столбца проекта все связанные задачи перемещаются в 'Без категории'. Продолжить?
 projects.column.color=Цвет
 projects.open=Открыть
 projects.close=Закрыть
@@ -1424,7 +1412,6 @@ issues.filter_sort.moststars=Больше звезд
 issues.filter_sort.feweststars=Меньше звезд
 issues.filter_sort.mostforks=Больше форков
 issues.filter_sort.fewestforks=Меньше форков
-issues.keyword_search_unavailable=В настоящее время поиск по ключевым словам недоступен. Обратитесь к администратору сайта.
 issues.action_open=Открыть
 issues.action_close=Закрыть
 issues.action_label=Метка
@@ -1673,7 +1660,6 @@ pulls.compare_compare=взять из
 pulls.switch_comparison_type=Переключить тип сравнения
 pulls.switch_head_and_base=Поменять исходную и целевую ветки местами
 pulls.filter_branch=Фильтр по ветке
-pulls.no_results=Результатов не найдено.
 pulls.show_all_commits=Показать все коммиты
 pulls.show_changes_since_your_last_review=Показать изменения с момента вашего последнего отзыва
 pulls.showing_only_single_commit=Показать только изменения коммита %[1]s
@@ -1929,17 +1915,6 @@ activity.git_stats_deletion_n=%d удалений
 
 contributors.contribution_type.commits=коммитов
 
-search=Поиск
-search.search_repo=Поиск по репозиторию
-search.type.tooltip=Тип поиска
-search.fuzzy=Неточный
-search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу
-search.match=Соответствие
-search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу
-search.results=Результаты поиска "%s" в <a href="%s">%s</a>
-search.code_no_results=Не найдено исходного кода, соответствующего поисковому запросу.
-search.code_search_unavailable=В настоящее время поиск по коду недоступен. Обратитесь к администратору сайта.
-
 settings=Настройки
 settings.desc=В настройках вы можете менять различные параметры этого репозитория
 settings.options=Репозиторий
@@ -2015,6 +1990,7 @@ settings.pulls.default_allow_edits_from_maintainers=По умолчанию ра
 settings.releases_desc=Включить релизы
 settings.packages_desc=Включить реестр пакетов
 settings.projects_desc=Включить проекты репозитория
+settings.projects_mode_all=Все проекты
 settings.actions_desc=Включить действия репозитория
 settings.admin_settings=Настройки администратора
 settings.admin_enable_health_check=Выполнять проверки целостности этого репозитория (git fsck)
@@ -2089,7 +2065,6 @@ settings.delete_collaborator=Удалить
 settings.collaborator_deletion=Удалить соавтора
 settings.collaborator_deletion_desc=Этот пользователь больше не будет иметь доступа для совместной работы в этом репозитории после удаления. Вы хотите продолжить?
 settings.remove_collaborator_success=Соавтор удалён.
-settings.search_user_placeholder=Поиск пользователя…
 settings.org_not_allowed_to_be_collaborator=Организации не могут быть добавлены как соавторы.
 settings.change_team_access_not_allowed=Доступ к репозиторию команде ограничен владельцем организации
 settings.team_not_in_organization=Команда не в той же организации, что и репозиторий
@@ -2097,7 +2072,6 @@ settings.teams=Команды
 settings.add_team=Добавить команду
 settings.add_team_duplicate=Команда уже имеет репозиторий
 settings.add_team_success=Команда теперь имеет доступ к репозиторию.
-settings.search_team=Поиск команды…
 settings.change_team_permission_tip=Разрешение команды установлено на странице настройки команды и не может быть изменено для каждого репозитория
 settings.delete_team_tip=Эта команда имеет доступ ко всем репозиториям и не может быть удалена
 settings.remove_team_success=Доступ команды к репозиторию удалён.
@@ -2248,9 +2222,7 @@ settings.protect_whitelist_committers=Ограничение отправки п
 settings.protect_whitelist_committers_desc=Только пользователям или командам из белого списка будет разрешена отправка изменений в эту ветку (но не принудительная отправка).
 settings.protect_whitelist_deploy_keys=Белый список развёртываемых ключей с доступом на запись в push.
 settings.protect_whitelist_users=Пользователи, которые могут отправлять изменения в эту ветку:
-settings.protect_whitelist_search_users=Поиск пользователей…
 settings.protect_whitelist_teams=Команды, члены которых могут отправлять изменения в эту ветку:
-settings.protect_whitelist_search_teams=Поиск команд…
 settings.protect_merge_whitelist_committers=Ограничить право на слияние белым списком
 settings.protect_merge_whitelist_committers_desc=Разрешить принимать запросы на слияние в эту ветку только пользователям и командам из «белого списка».
 settings.protect_merge_whitelist_users=Пользователи с правом на слияние:
@@ -2486,7 +2458,6 @@ branch.default_deletion_failed=Ветка «%s» является веткой 
 branch.restore=Восстановить ветку «%s»
 branch.download=Скачать ветку «%s»
 branch.rename=Переименовать ветку «%s»
-branch.search=Поиск ветки
 branch.included_desc=Эта ветка является частью ветки по умолчанию
 branch.included=Включено
 branch.create_new_branch=Создать ветку из ветви:
@@ -2623,7 +2594,6 @@ teams.write_permission_desc=Эта команда предоставляет д
 teams.admin_permission_desc=Эта команда даёт <strong>административный</strong> доступ: участники могут читать, отправлять изменения и добавлять соавторов к её репозиториям.
 teams.create_repo_permission_desc=Кроме того, эта команда предоставляет право <strong>Создание репозитория</strong>: члены команды могут создавать новые репозитории в организации.
 teams.repositories=Репозитории группы разработки
-teams.search_repo_placeholder=Поиск репозитория…
 teams.remove_all_repos_title=Удалить все репозитории команды
 teams.remove_all_repos_desc=Удаляет все репозитории из команды.
 teams.add_all_repos_title=Добавить все репозитории
@@ -2654,6 +2624,8 @@ integrations=Интеграции
 authentication=Аутентификация
 emails=Адреса эл. почты пользователей
 config=Конфигурация
+config_summary=Статистика
+config_settings=Настройки
 notices=Системные уведомления
 monitor=Мониторинг
 first_page=Первая
@@ -2822,9 +2794,6 @@ repos.unadopted.no_more=Больше непринятых репозиторие
 repos.owner=Владелец
 repos.name=Название
 repos.private=Личный
-repos.watches=Следят
-repos.stars=Звезды
-repos.forks=Форки
 repos.issues=Задачи
 repos.size=Размер
 repos.lfs_size=Размер LFS
@@ -2946,7 +2915,6 @@ auths.tip.nextcloud=`Зарегистрируйте нового потреби
 auths.tip.dropbox=Добавьте новое приложение на https://www.dropbox.com/developers/apps
 auths.tip.facebook=Зарегистрируйте новое приложение на https://developers.facebook.com/apps и добавьте модуль «Facebook Login»
 auths.tip.github=Добавьте OAuth приложение на https://github.com/settings/applications/new
-auths.tip.gitlab=Добавьте новое приложение на https://gitlab.com/profile/applications
 auths.tip.google_plus=Получите учётные данные клиента OAuth2 в консоли Google API на странице https://console.developers.google.com/
 auths.tip.openid_connect=Используйте OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) для автоматической настройки входа OAuth
 auths.tip.twitter=Перейдите на https://dev.twitter.com/apps, создайте приложение и убедитесь, что включена опция «Разрешить это приложение для входа в систему с помощью Twitter»
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index fb97daf5e0..7e82cfe3d6 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -100,6 +100,15 @@ concept_user_organization=සංවිධානය
 
 name=නම
 
+filter=පෙරහන
+filter.is_archived=සංරක්ෂිත
+filter.is_template=සැකිලි
+filter.public=ප්‍රසිද්ධ
+filter.private=පෞද්ගලික
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -223,7 +232,6 @@ collaborative_repos=සහයෝගී ගබඩාවලදී
 my_orgs=මාගේ සංවිධාන
 my_mirrors=මගේ දර්පණ
 view_home=%s දකින්න
-search_repos=ගබඩාවක් සොයා ගන්න…
 filter=වෙනත් පෙරහන්
 filter_by_team_repositories=කණ්ඩායම් කෝෂ්ඨ අනුව පෙරන්න
 
@@ -243,13 +251,7 @@ issues.in_your_repos=ඔබගේ කෝෂ්ඨවල
 repos=කෝෂ්ඨ
 users=පරිශීලකයින්
 organizations=සංවිධාන
-search=සොයන්න
 code=කේතය
-search.match=තරගය
-repo_no_results=ගැලපෙන ගබඩාවක් හමු නොවීය.
-user_no_results=ගැලපෙන පරිශීලකයින් හමු නොවීය.
-org_no_results=ගැලපෙන සංවිධාන හමු නොවීය.
-code_no_results=ඔබගේ සෙවුම් පදය ගැලපෙන ප්රභව කේතයක් නොමැත.
 code_last_indexed_at=අවසන් සුචිගත %s
 
 [auth]
@@ -262,7 +264,6 @@ remember_me=උපාංගය මතක තබාගන්න
 forgot_password_title=මුරපදය අමතක වුණා
 forgot_password=මුරපදය අමතක වුණා ද?
 sign_up_now=ගිණුමක් ඇවැසිද? දැන් ලියාපදිංචි වන්න.
-confirmation_mail_sent_prompt=නව තහවුරු කිරීමේ විද්යුත් තැපෑලක් <b>%s</b>වෙත යවා ඇත. ලියාපදිංචි කිරීමේ ක්රියාවලිය සම්පූර්ණ කිරීම සඳහා කරුණාකර ඊළඟ %s තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න.
 must_change_password=මුරපදය යාවත්කාල කරන්න
 allow_password_change=මුරපදය වෙනස් කිරීමට පරිශීලකයාට අවශ්ය වේ (නිර්දේශිත)
 reset_password_mail_sent_prompt=තහවුරු කිරීමේ විද්යුත් තැපෑලක් <b>%s</b>වෙත යවා ඇත. ඊළඟ තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න %s ගිණුම යථා ක්රියාවලිය සම්පූර්ණ කිරීම සඳහා.
@@ -467,6 +468,7 @@ user_bio=චරිතාපදානය
 disabled_public_activity=මෙම පරිශීලකයා ක්රියාකාරකම්වල මහජන දෘශ්යතාව අක්රීය කර ඇත.
 
 
+
 [settings]
 profile=පැතිකඩ
 account=ගිණුම
@@ -578,7 +580,6 @@ gpg_invalid_token_signature=සපයන ලද GPG යතුර, අත්ස
 gpg_token_required=පහත ටෝකනය සඳහා ඔබ අත්සනක් ලබා දිය යුතුය
 gpg_token=ටෝකනය
 gpg_token_help=ඔබට අත්සනක් ජනනය කළ හැකිය:
-gpg_token_code=දෝංකාරය "%s" | gpg -a -පැහැර හැරීම-යතුර %s —වෙන්ච-සිග්
 gpg_token_signature=සන්නද්ධ GPG අත්සන
 key_signature_gpg_placeholder=ආරම්භ වන්නේ '—ආරම්භ කරන්න PGP සිග්නේටුර්—'
 ssh_key_verified=සත්යාපිත යතුර
@@ -717,7 +718,6 @@ fork_repo=දෙබලක ගබඩාව
 fork_from=සිට දෙබලක
 fork_visibility_helper=ව්යාජ ගබඩාවේ දෘශ්යතාව වෙනස් කළ නොහැක.
 use_template=මෙම අච්චුව භාවිතා කරන්න
-clone_in_vsc=VS කේතය පරිගණක ක්රිඩාවට සමාන
 download_zip=ZIP බාගන්න
 download_tar=TAR.GZ බාගන්න
 download_bundle=බණ්ඩලය බාගත කරන්න
@@ -943,8 +943,6 @@ editor.require_signed_commit=ශාඛාවට අත්සන් කළ කැ
 commits.desc=මූලාශ්ර කේත වෙනස් කිරීමේ ඉතිහාසය පිරික්සන්න.
 commits.commits=විවරයන්
 commits.nothing_to_compare=මෙම ශාඛා සමාන වේ.
-commits.search=සෙවුම් වාර…
-commits.find=සොයන්න
 commits.search_all=සියළුම ශාඛා
 commits.author=කතෘ
 commits.message=පණිවිඩය
@@ -981,7 +979,6 @@ projects.type.basic_kanban=මූලික කන්ෙවනි
 projects.type.bug_triage=දෝෂ ට්රයිජ්
 projects.template.desc=ව්යාපෘති සැකිල්ල
 projects.template.desc_helper=ආරම්භ කිරීම සඳහා ව්යාපෘති සැකිල්ලක් තෝරන්න
-projects.type.uncategorized=ප්‍රවර්ග ගත නැති
 projects.column.edit_title=නම
 projects.column.new_title=නම
 projects.column.color=වර්ණය
@@ -1263,7 +1260,6 @@ pulls.compare_compare=සිට අදින්න
 pulls.switch_comparison_type=ස්විච් සංසන්දනය වර්ගය
 pulls.switch_head_and_base=හිස සහ පාදය මාරු කරන්න
 pulls.filter_branch=ශාඛාව පෙරන්න
-pulls.no_results=ප්රතිඵල සොයාගත නොහැකි විය.
 pulls.nothing_to_compare=මෙම ශාඛා සමාන වේ. අදින්න ඉල්ලීමක් නිර්මාණය කිරීමට අවශ්ය නැත.
 pulls.nothing_to_compare_and_allow_empty_pr=මෙම ශාඛා සමාන වේ. මෙම මහජන සම්බන්ධතා හිස් වනු ඇත.
 pulls.has_pull_request=`මෙම ශාඛා අතර අදින්න ඉල්ලීම දැනටමත් පවතී: <a href="%[1]s">%[2]s #%[3]d</a>`
@@ -1462,13 +1458,6 @@ activity.git_stats_deletion_n=%d මකාදැමීම්
 
 contributors.contribution_type.commits=විවරයන්
 
-search=සොයන්න
-search.search_repo=කෝෂ්ඨය සොයන්න
-search.fuzzy=සිනිඳු
-search.match=තරගය
-search.results=<a href="%s">%s</a> හි "%s" සඳහා සෙවුම් ප්‍රතිඵල
-search.code_no_results=ඔබගේ සෙවුම් පදය ගැලපෙන ප්රභව කේතයක් නොමැත.
-
 settings=සැකසුම්
 settings.desc=සැකසුම් යනු ගබඩාව සඳහා සැකසුම් කළමනාකරණය කළ හැකි ස්ථානයයි
 settings.options=කෝෂ්ඨය
@@ -1584,7 +1573,6 @@ settings.delete_collaborator=ඉවත් කරන්න
 settings.collaborator_deletion=සහයෝගිතාකරු ඉවත් කරන්න
 settings.collaborator_deletion_desc=සහයෝගිතාකරුවෙකු ඉවත් කිරීම මෙම ගබඩාවට ඔවුන්ගේ ප්රවේශය අවලංගු කරනු ඇත. දිගටම?
 settings.remove_collaborator_success=සහයෝගිතාකරු ඉවත් කර ඇත.
-settings.search_user_placeholder=පරිශීලක සොයන්න…
 settings.org_not_allowed_to_be_collaborator=සහයෝගීකයෙකු ලෙස සංවිධාන එකතු කළ නොහැක.
 settings.change_team_access_not_allowed=ගබඩාව සඳහා කණ්ඩායම් ප්රවේශය වෙනස් කිරීම සංවිධාන හිමිකරුට සීමා කර ඇත
 settings.team_not_in_organization=මෙම කණ්ඩායම ගබඩාවේ එකම සංවිධානයේ නොමැත
@@ -1592,7 +1580,6 @@ settings.teams=කණ්ඩායම්
 settings.add_team=කණ්ඩායම එකතු කරන්න
 settings.add_team_duplicate=කණ්ඩායම දැනටමත් ගබඩාවක් ඇත
 settings.add_team_success=කණ්ඩායමට දැන් කෝෂ්ඨයට ප්‍රවේශය ඇත.
-settings.search_team=කණ්ඩායම සොයන්න…
 settings.change_team_permission_tip=කණ්ඩායමේ අවසරය කණ්ඩායම් සැකසුම් පිටුවේ සකසන අතර කෝෂ්ඨය අනුව වෙනස් කළ නොහැකිය
 settings.delete_team_tip=මෙම කණ්ඩායම සියළුම කෝෂ්ඨවලට ප්‍රවේශය ඇති අතර ඉවත් කළ නොහැකිය
 settings.remove_team_success=කෝෂ්ඨය වෙත කණ්ඩායමේ ප්‍රවේශය ඉවත් කර ඇත.
@@ -1709,9 +1696,7 @@ settings.protect_whitelist_committers=වයිට්ලිස්ට් සී
 settings.protect_whitelist_committers_desc=මෙම ශාඛාව වෙත තල්ලු කිරීමට අවසර ඇත්තේ වයිට්ලිස්ට් පරිශීලකයින්ට හෝ කණ්ඩායම්වලට පමණි (නමුත් බල තල්ලුව නොවේ).
 settings.protect_whitelist_deploy_keys=වයිට්ලිස්ට් තල්ලු කිරීමට ලිවීමේ ප්රවේශය සහිත යතුරු යොදවන්න.
 settings.protect_whitelist_users=තල්ලු කිරීම සඳහා වයිට්ලිස්ට් පරිශීලකයින්:
-settings.protect_whitelist_search_users=පරිශීලකයින් සොයන්න…
 settings.protect_whitelist_teams=තල්ලු කිරීම සඳහා වයිට්ලිස්ට් කණ්ඩායම්:
-settings.protect_whitelist_search_teams=කණ්ඩායම් සොයන්න…
 settings.protect_merge_whitelist_committers=ඒකාබද්ධ වයිට්ලිස්ට් සක්රීය කරන්න
 settings.protect_merge_whitelist_committers_desc=මෙම ශාඛාවට ඇද ගැනීමේ ඉල්ලීම් ඒකාබද්ධ කිරීමට සුදු පැහැති පරිශීලකයින්ට හෝ කණ්ඩායම්වලට පමණක් ඉඩ දෙන්න.
 settings.protect_merge_whitelist_users=ඒකාබද්ධ කිරීම සඳහා Whitelisted පරිශීලකයන්:
@@ -2006,7 +1991,6 @@ teams.write_permission_desc=මෙම කණ්ඩායම ප්රදාන
 teams.admin_permission_desc=මෙම කණ්ඩායම <strong>පරිපාලක</strong> ප්රවේශය ලබා දෙයි: සාමාජිකයින්ට කියවීමට, කණ්ඩායම් ගබඩාවන්ට සහයෝගීකයින් වෙත තල්ලු කිරීමට සහ එකතු කිරීමට හැකිය.
 teams.create_repo_permission_desc=මීට අමතරව, මෙම කණ්ඩායම <strong>ලබා දෙයි ගබඩාව සාදන්න</strong> අවසරය: සාමාජිකයින්ට සංවිධානයේ නව ගබඩාවක් නිර්මාණය කළ හැකිය.
 teams.repositories=කණ්ඩායම් කෝෂ්ඨ
-teams.search_repo_placeholder=කෝෂ්ඨය සොයන්න…
 teams.remove_all_repos_title=සියළුම කණ්ඩායම් කෝෂ්ඨ ඉවත් කරන්න
 teams.remove_all_repos_desc=මෙය කණ්ඩායමෙන් සියළුම කෝෂ්ඨ ඉවත් කෙරෙනු ඇත.
 teams.add_all_repos_title=සියළුම කෝෂ්ඨ එක්කරන්න
@@ -2031,6 +2015,8 @@ hooks=වෙබ්කොකු
 authentication=සත්යාපන ප්රභවයන්
 emails=පරිශීලක වි-තැපැල්
 config=වින්‍යාසය
+config_summary=සාරාංශය
+config_settings=සැකසුම්
 notices=පද්ධතියේ දැන්වීම්
 monitor=අධීක්ෂණය
 first_page=පළමු
@@ -2178,9 +2164,6 @@ repos.unadopted.no_more=තවත් සම්මත නොකළ ගබඩා
 repos.owner=හිමිකරු
 repos.name=නම
 repos.private=පෞද්ගලික
-repos.watches=අත් ඔරලෝසු
-repos.stars=තරු
-repos.forks=දෙබලක
 repos.issues=ගැටළු
 repos.size=ප්‍රමාණය
 
@@ -2278,7 +2261,6 @@ auths.tip.nextcloud=පහත සඳහන් මෙනුව භාවිතා
 auths.tip.dropbox=https://www.dropbox.com/developers/apps හි නව යෙදුමක් සාදන්න
 auths.tip.facebook=https://developers.facebook.com/apps හි නව යෙදුමක් ලියාපදිංචි කර නිෂ්පාදනය එකතු කරන්න “ෆේස්බුක් ලොගින් වන්න”
 auths.tip.github=https://github.com/settings/applications/new හි නව OAUTH අයදුම්පතක් ලියාපදිංචි කරන්න
-auths.tip.gitlab=https://gitlab.com/profile/applications හි නව අයදුම්පතක් ලියාපදිංචි කරන්න
 auths.tip.google_plus=ගූගල් API කොන්සෝලය වෙතින් OUT2 සේවාදායක අක්තපත්ර ලබා ගන්න https://console.developers.google.com/
 auths.tip.openid_connect=අන්ත ලක්ෂ්ය නියම කිරීම සඳහා OpenID Connect ඩිස්කවරි URL (<server>/.හොඳින් දැන /openid-වින්යාසය) භාවිතා කරන්න
 auths.tip.twitter=https://dev.twitter.com/apps වෙත යන්න, යෙදුමක් සාදන්න සහ “මෙම යෙදුම ට්විටර් සමඟ පුරනය වීමට භාවිතා කිරීමට ඉඩ දෙන්න” විකල්පය සක්රීය කර ඇති බවට සහතික වන්න
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index 4a223ee90d..b468b55283 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -140,6 +140,13 @@ confirm_delete_selected=Potvrdzujete zmazanie všetkých vybraných položiek?
 name=Meno
 value=Hodnota
 
+filter.is_archived=Archivované
+filter.is_template=Šablóna
+filter.private=Súkromný
+
+
+[search]
+
 [aria]
 navbar=Navigačná lišta
 footer=Päta
@@ -309,7 +316,6 @@ collaborative_repos=Kolaboratívne repozitáre
 my_orgs=Moje organizácie
 my_mirrors=Moje zrkadlá
 view_home=Zobraziť %s
-search_repos=Nájsť repozitár…
 filter=Ostatné filtre
 filter_by_team_repositories=Filtrovať podľa tímových repozitárov
 feed_of=Informačný kanál „%s“
@@ -330,20 +336,8 @@ issues.in_your_repos=Vo vašich repozitároch
 repos=Repozitáre
 users=Používatelia
 organizations=Organizácie
-search=Hľadať
 go_to=Ísť na
 code=Zdrojový kód
-search.type.tooltip=Typ vyhľadávania
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnúť iba výsledky, ktoré sa takmer zhodujú s hľadaným výrazom
-search.match=Zhoda
-search.match.tooltip=Zahrnúť iba výsledky, ktoré sa presne zhodujú s hľadaným výrazom
-code_search_unavailable=Vyhľadávanie kódu momentálne nie je dostupné. Kontaktujte, prosím, správcu.
-repo_no_results=Nenašli sa zodpovedajúce repozitáre.
-user_no_results=Nenašli sa zodpovedajúci používatelia.
-org_no_results=Nenašli sa zodpovedajúce organizácie.
-code_no_results=Nenašiel sa žiaden zdrojový kód zodpovedajúci hľadanému výrazu.
-code_search_results=`Výsledky hľadania pre "%s"`
 code_last_indexed_at=Naposledy indexované %s
 relevant_repositories_tooltip=Repozitáre, ktoré sú forkami alebo ktoré nemajú tému, žiadnu ikonu ani popis, sú skryté.
 relevant_repositories=Zobrazujú sa iba relevantné repozitáre, <a href="%s">zobraziť nefiltrované výsledky</a>.
@@ -359,7 +353,6 @@ remember_me=Zapamätať si toto zariadenie
 forgot_password_title=Zabudnuté heslo
 forgot_password=Zabudli ste heslo?
 sign_up_now=Potrebujete účet? Zaregistrujte sa teraz.
-confirmation_mail_sent_prompt=Na adresu <b>%s</b> bol odoslaný nový potvrdzovací e-mail. Skontrolujte si, prosím, vašu doručenú poštu počas najbližších %s pre dokončenie procesu registrácie.
 allow_password_change=Vyžiadať od používateľa zmenu hesla (doporučuje sa)
 reset_password_mail_sent_prompt=Na adresu <b>%s</b> bol odoslaný potvrdzovací e-mail. Skontrolujte si, prosím, vašu doručenú poštu počas najbližších %s pre dokončenie procesu obnovenia účtu.
 active_your_account=Aktivovať účet
@@ -582,6 +575,7 @@ user_bio=Životopis
 disabled_public_activity=Tento používateľ zákázal verejnú viditeľnosť aktivity.
 
 
+
 [settings]
 profile=Profil
 account=Účet
@@ -707,7 +701,6 @@ gpg_invalid_token_signature=Zadaný GPG kľúč, podpis a token sa nezhodujú al
 gpg_token_required=Musíte zadať podpis pre nižšie uvedený token
 gpg_token=Token
 gpg_token_help=Podpis môžete vygenerovať pomocou:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Zakódovaný (ASCII) podpis GPG
 key_signature_gpg_placeholder=Začína s '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Overený kľúč
@@ -853,7 +846,6 @@ visibility_helper_forced=Váš správca vynucuje že nové repozitáre musia by
 visibility_fork_helper=(Zmena ovplyvní všetky forky.)
 clone_helper=Potrebujete pomoc s klonovaním? Navštívte <a target="_blank" rel="noopener noreferrer" href="%s">Pomocníka</a>.
 use_template=Použiť túto šablónu
-clone_in_vsc=Klonovať vo VS Code
 generate_repo=Generovať repozitár
 generate_from=Generovať z
 repo_desc=Popis
@@ -1037,7 +1029,6 @@ editor.no_commit_to_branch=Nedá sa odoslať priamo do vetvy, pretože:
 editor.require_signed_commit=Vetva vyžaduje podpísaný commit
 
 commits.commits=Commity
-commits.find=Hľadať
 commits.search_all=Všetky vetvy
 commits.author=Autor
 commits.message=Správa
@@ -1147,15 +1138,6 @@ activity.git_stats_commit_n=%d commity
 
 contributors.contribution_type.commits=Commitov
 
-search=Hľadať
-search.type.tooltip=Typ vyhľadávania
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnúť iba výsledky, ktoré sa takmer zhodujú s hľadaným výrazom
-search.match=Zhoda
-search.match.tooltip=Zahrnúť iba výsledky, ktoré sa presne zhodujú s hľadaným výrazom
-search.code_no_results=Nenašiel sa žiaden zdrojový kód zodpovedajúci hľadanému výrazu.
-search.code_search_unavailable=Vyhľadávanie kódu momentálne nie je dostupné. Kontaktujte, prosím, správcu.
-
 settings.collaboration.owner=Vlastník
 settings.hooks=Webhooky
 settings.githooks=Git hooky
@@ -1287,7 +1269,6 @@ dashboard.delete_generated_repository_avatars=Odstrániť vygenerované avatary
 
 repos.owner=Vlastník
 repos.private=Súkromný
-repos.forks=Forky
 
 packages.owner=Vlastník
 packages.repository=Repozitár
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 0a484c9b79..e48d84ff78 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -91,6 +91,14 @@ concept_user_organization=Organisation
 
 name=Namn
 
+filter.is_archived=Arkiverade
+filter.is_template=Mall
+filter.public=Offentlig
+filter.private=Privat
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -212,7 +220,6 @@ collaborative_repos=Kollaborativa Utvecklingskataloger
 my_orgs=Mina organisationer
 my_mirrors=Mina speglar
 view_home=Visa %s
-search_repos=Hitta en utvecklingskatalog…
 filter=Övriga Filter
 
 show_archived=Arkiverade
@@ -231,12 +238,7 @@ issues.in_your_repos=I dina utvecklingskataloger
 repos=Utvecklingskataloger
 users=Användare
 organizations=Organisationer
-search=Sök
 code=Kod
-repo_no_results=Inga matchande utvecklingskataloger hittades.
-user_no_results=Inga matchande användare hittades.
-org_no_results=Inga matchande organisationer hittades.
-code_no_results=Ingen källkod hittades som matchar din sökterm.
 code_last_indexed_at=Indexerades senast %s
 
 [auth]
@@ -249,7 +251,6 @@ remember_me=Kom ihåg denna enhet
 forgot_password_title=Glömt lösenord
 forgot_password=Glömt lösenord?
 sign_up_now=Behöver du ett konto? Registrera nu.
-confirmation_mail_sent_prompt=Ett nytt bekräftelsemail has skickats till <b>%s</b>. Vänligen kolla din inkorg inom dom kommande %s för att slutföra registreringsprocessen.
 must_change_password=Ändra ditt lösenord
 allow_password_change=Kräv att användaren byter lösenord (rekommenderas)
 reset_password_mail_sent_prompt=Ett nytt bekräftelsemail has skickats till <b>%s</b>. Vänligen kontrollera din inkorg inom de kommande %s för att slutföra återställning av ditt konto.
@@ -406,6 +407,7 @@ user_bio=Biografi
 disabled_public_activity=Den här användaren har inaktiverat den publika synligheten av aktiviteten.
 
 
+
 [settings]
 profile=Profil
 account=Konto
@@ -798,8 +800,6 @@ editor.require_signed_commit=Branchen kräver en signerad commit
 
 commits.desc=Bläddra i källkodens förändringshistorik.
 commits.commits=Incheckningar
-commits.search=Sök commits…
-commits.find=Sök
 commits.search_all=Alla brancher
 commits.author=Upphovsman
 commits.message=Meddelande
@@ -827,7 +827,6 @@ projects.edit=Redigera projekt
 projects.modify=Uppdatera projekt
 projects.type.none=Ingen
 projects.template.desc=Projektmall
-projects.type.uncategorized=Okatergoriserad
 projects.column.edit_title=Namn
 projects.column.new_title=Namn
 projects.open=Öppna
@@ -1084,7 +1083,6 @@ pulls.compare_changes_desc=Välj branchen att merga in i, och ifrån.
 pulls.compare_base=merga in i
 pulls.compare_compare=pulla från
 pulls.filter_branch=Filtrera gren
-pulls.no_results=Inga resultat hittades.
 pulls.nothing_to_compare=Dessa brancher är ekvivalenta. Det finns ingen anledning att skapa en pull-request.
 pulls.create=Skapa Pullförfrågan
 pulls.title_desc=vill sammanfoga %[1]d incheckningar från <code>s[2]s</code> in i <code id="branch_target">%[3]s</code>
@@ -1230,11 +1228,6 @@ activity.git_stats_deletion_n=%d borttagningar
 
 contributors.contribution_type.commits=Incheckningar
 
-search=Sök
-search.search_repo=Sök utvecklingskatalog
-search.results=Sökresultat för ”%s” i <a href="%s"> %s</a>
-search.code_no_results=Ingen källkod hittades som matchar din sökterm.
-
 settings=Inställningar
 settings.desc=Inställningarna är där du kan hantera inställningar för utvecklingskatalogen
 settings.options=Utvecklingskatalog
@@ -1315,7 +1308,6 @@ settings.delete_collaborator=Ta bort
 settings.collaborator_deletion=Ta bort medarbetare
 settings.collaborator_deletion_desc=Borttagning av en medarbetare kommer att återkalla deras åtkomst till utvecklingskatalogen. Vill du fortsätta?
 settings.remove_collaborator_success=Medarbetaren har blivit borttagen.
-settings.search_user_placeholder=Sök användare…
 settings.org_not_allowed_to_be_collaborator=Organisationer kan inte läggas till som en medarbetare.
 settings.change_team_access_not_allowed=Att ändra teamåtkomst för utvecklingskatalogen har begränsats till organisationsägaren
 settings.team_not_in_organization=Teamet är inte i samma organisation som utvecklingskatalogen
@@ -1407,9 +1399,7 @@ settings.protect_enable_push=Aktivera Push
 settings.protect_enable_push_desc=Alla med skrivrättigheter kommer att kunna pusha till denna branch (men inte force-pusha).
 settings.protect_whitelist_deploy_keys=Vitlista deploy-nyckar med skrivåtkomst till push.
 settings.protect_whitelist_users=Vitlistade användare för pushning:
-settings.protect_whitelist_search_users=Sök användare…
 settings.protect_whitelist_teams=Vitlistade team för pushning:
-settings.protect_whitelist_search_teams=Sök team…
 settings.protect_merge_whitelist_committers=Aktivera vitlista för sammanfogning
 settings.protect_merge_whitelist_committers_desc=Tillåt endast vitlistade användare eller team att sammanfoga pull requests i denna branch.
 settings.protect_merge_whitelist_users=Vitlistade användare för sammanfogning:
@@ -1626,7 +1616,6 @@ teams.write_permission_desc=Medlemskap i detta team ger <strong>skrivrättighete
 teams.admin_permission_desc=Medlemskap i detta team ger <strong>administratörsrättigheter</strong>: medlemmar kan läsa, pusha och lägga till medarbetare till teamets utvecklingskataloger.
 teams.create_repo_permission_desc=Vidare så ger detta team <strong>Skapa utvecklingskatalog</strong> rättigheten: medlemmar can skapa nya utvecklingskataloger i organisationen.
 teams.repositories=Teamförråd
-teams.search_repo_placeholder=Sök utvecklingskatalog…
 teams.remove_all_repos_title=Ta bort alla utvecklingskataloger för teamet
 teams.remove_all_repos_desc=Detta kommer att ta bort alla utvecklingskataloger från teamet.
 teams.add_all_repos_title=Lägg till alla utvecklingskataloger
@@ -1649,6 +1638,8 @@ organizations=Organisationer
 repositories=Utvecklingskataloger
 authentication=Autentiseringskälla
 config=Konfiguration
+config_summary=Översikt
+config_settings=Inställningar
 notices=Systemaviseringar
 monitor=Övervakning
 first_page=Första
@@ -1750,9 +1741,6 @@ repos.repo_manage_panel=Utvecklingskatalogshantering
 repos.owner=Ägare
 repos.name=Namn
 repos.private=Privat
-repos.watches=Vakter
-repos.stars=Stjärnor
-repos.forks=Forkar
 repos.issues=Ärenden
 repos.size=Storlek
 
@@ -1818,7 +1806,6 @@ auths.tip.bitbucket=Registrera en ny OAuth konsument på https://bitbucket.org/a
 auths.tip.dropbox=Skapa en ny applikation på https://www.dropbox.com/developers/apps
 auths.tip.facebook=Registrera en ny appliaktion på https://developers.facebook.com/apps och lägg till produkten ”Facebook-inloggning”
 auths.tip.github=Registrera en ny OAuth applikation på https://github.com/settings/applications/new
-auths.tip.gitlab=Registrera en ny applikation på https://gitlab.com/profile/applications
 auths.tip.google_plus=Erhåll inloggningsuppgifter för OAuth2 från Google API-konsolen på https://console.developers.google.com/
 auths.tip.openid_connect=Använd OpenID Connect Discovery länken (<server>/.well-known/openid-configuration) för att ange slutpunkterna
 auths.tip.twitter=Gå till https://dev.twitter.com/app, skapa en applikation och försäkra att alternativet "Allow this application to be used to Sign in with Twitter" är aktiverat
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 3c8cb08726..5a5036f87d 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -141,6 +141,15 @@ confirm_delete_selected=Tüm seçili öğeleri gerçekten silmek istiyor musunuz
 name=İsim
 value=Değer
 
+filter=Filtre
+filter.is_archived=Arşivlenmiş
+filter.is_template=Şablon
+filter.public=Genel
+filter.private=Özel
+
+
+[search]
+
 [aria]
 navbar=Gezinti Çubuğu
 footer=Alt Bilgi
@@ -314,7 +323,6 @@ collaborative_repos=Katkıya Açık Depolar
 my_orgs=Organizasyonlarım
 my_mirrors=Yansılarım
 view_home=%s Görüntüle
-search_repos=Depo bul…
 filter=Diğer Süzgeçler
 filter_by_team_repositories=Takım depolarına göre süz
 feed_of=`"%s" beslemesi`
@@ -335,20 +343,8 @@ issues.in_your_repos=Depolarınızda
 repos=Depolar
 users=Kullanıcılar
 organizations=Organizasyonlar
-search=Ara
 go_to=Git
 code=Kod
-search.type.tooltip=Arama türü
-search.fuzzy=Bulanık
-search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer
-search.match=Eşleştir
-search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
-code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticinizle bağlantıya geçin.
-repo_no_results=Eşleşen depo bulunamadı.
-user_no_results=Eşleşen kullanıcı bulunamadı.
-org_no_results=Eşleşen organizasyon bulunamadı.
-code_no_results=Arama teriminizi içeren kaynak kod bulunamadı.
-code_search_results=`"%s" için sonuçları ara`
 code_last_indexed_at=Son dizinlenen %s
 relevant_repositories_tooltip=Çatal olan veya konusu, simgesi veya açıklaması olmayan depolar gizlenmiştir.
 relevant_repositories=Sadece ilişkili depolar gösteriliyor, <a href="%s">süzülmemiş sonuçları göster</a>.
@@ -366,7 +362,6 @@ forgot_password_title=Şifremi unuttum
 forgot_password=Şifrenizi mi unuttunuz?
 sign_up_now=Bir hesaba mı ihtiyacınız var? Hemen kaydolun.
 sign_up_successful=Hesap başarılı bir şekilde oluşturuldu. Hoşgeldiniz!
-confirmation_mail_sent_prompt=Yeni onay e-postası <b>%s</b> adresine gönderildi. Lütfen gelen kutunuzu bir sonraki %s e kadar kontrol edip kayıt işlemini tamamlayın.
 must_change_password=Parolanızı güncelleyin
 allow_password_change=Kullanıcıyı parola değiştirmeye zorla (önerilen)
 reset_password_mail_sent_prompt=<b>%s</b> adresine bir onay e-postası gönderildi. Hesap kurtarma işlemini tamamlamak için lütfen gelen kutunuzu sonraki %s içinde kontrol edin.
@@ -614,6 +609,7 @@ form.name_reserved=`"%s" kullanıcı adı rezerve edilmiş.`
 form.name_pattern_not_allowed=Kullanıcı adında "%s" deseni kullanılamaz.
 form.name_chars_not_allowed=`"%s" kullanıcı adı geçersiz karakterler içeriyor.`
 
+
 [settings]
 profile=Profil
 account=Hesap
@@ -758,7 +754,6 @@ gpg_invalid_token_signature=Verilen GPG anahtarı, imza ve anahtar uyuşmuyor ve
 gpg_token_required=Aşağıdaki anahtar için bir imza sağlamalısınız
 gpg_token=Anahtar
 gpg_token_help=Şunu kullanarak bir imza oluşturabilirsiniz:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Korumalı GPG imzası
 key_signature_gpg_placeholder='-----PGP İMZA BAŞLAT -----' ile başlar
 verify_gpg_key_success=GPG anahtarı "%s" doğrulandı.
@@ -952,7 +947,6 @@ fork_branch=Çatala klonlanacak dal
 all_branches=Tüm dallar
 fork_no_valid_owners=Geçerli bir sahibi olmadığı için bu depo çatallanamaz.
 use_template=Bu şablonu kullan
-clone_in_vsc=VS Code'ta klonla
 download_zip=ZIP indir
 download_tar=TAR.GZ indir
 download_bundle=BUNDLE indir
@@ -973,9 +967,9 @@ readme_helper=Bir README dosyası şablonu seçin.
 readme_helper_desc=Projeniz için eksiksiz bir açıklama yazabileceğiniz yer burasıdır.
 auto_init=Depoyu başlat (.gitignore, Lisans ve README dosyalarını ekler)
 trust_model_helper=İmza doğrulaması için güven modelini seçin. Olası seçenekler şunlardır:
-trust_model_helper_collaborator=Ortak çalışan: Ortak çalışanların imzalarına güven
+trust_model_helper_collaborator=Katkıcı: Katkıcıların imzalarına güven
 trust_model_helper_committer=İşleyen: İşleyenlerle eşleşen imzalara güven
-trust_model_helper_collaborator_committer=Ortak çalışan+İşleyen: İşleyenle eşleşen ortak çalışanların imzalarına güven
+trust_model_helper_collaborator_committer=Katkıcı+İşleyen: İşleyenle eşleşen ortak çalışanların imzalarına güven
 trust_model_helper_default=Varsayılan: Bu kurulum için varsayılan güven modelini kullan
 create_repo=Depo Oluştur
 default_branch=Varsayılan Dal
@@ -1271,9 +1265,7 @@ commits.desc=Kaynak kodu değişiklik geçmişine göz atın.
 commits.commits=İşleme
 commits.no_commits=Ortak bir işleme yok. "%s" ve "%s" tamamen farklı geçmişlere sahip.
 commits.nothing_to_compare=Bu dallar eşit.
-commits.search=İşlemeleri ara…
 commits.search.tooltip=Anahtar kelimeleri "author:", "committer:", "after:" veya "before:" ile kullanabilirsiniz, örneğin "revert author:Alice before:2019-01-13".
-commits.find=Ara
 commits.search_all=Tüm Dallar
 commits.author=Yazar
 commits.message=Mesaj
@@ -1324,7 +1316,6 @@ projects.type.basic_kanban=Kanban Tabanı
 projects.type.bug_triage=Hata Triyajı
 projects.template.desc=Proje şablonu
 projects.template.desc_helper=Başlamak için bir proje şablonu seçin
-projects.type.uncategorized=Kategorize edilmemiş
 projects.column.edit=Sütun Düzenle
 projects.column.edit_title=İsim
 projects.column.new_title=İsim
@@ -1332,10 +1323,7 @@ projects.column.new_submit=Sütun Oluştur
 projects.column.new=Yeni Sütun
 projects.column.set_default=Varsayılanı Ayarla
 projects.column.set_default_desc=Bu sütunu kategorize edilmemiş konular ve değişiklik istekleri için varsayılan olarak ayarlayın
-projects.column.unset_default=Varsayılanları Geri Al
-projects.column.unset_default_desc=Bu sütunu varsayılan olarak geri al
 projects.column.delete=Sutün Sil
-projects.column.deletion_desc=Bir proje sütununun silinmesi, ilgili tüm konuları 'Kategorize edilmemiş'e taşır. Devam edilsin mi?
 projects.column.color=Renk
 projects.open=Aç
 projects.close=Kapat
@@ -1447,7 +1435,6 @@ issues.filter_sort.moststars=En çok yıldızlılar
 issues.filter_sort.feweststars=En az yıldızlılar
 issues.filter_sort.mostforks=En çok çatallananlar
 issues.filter_sort.fewestforks=En az çatallananlar
-issues.keyword_search_unavailable=Anahtar kelime ile arama şu an mevcut değil. Lütfen site yöneticisiyle iletişime geçin.
 issues.action_open=Açık
 issues.action_close=Kapat
 issues.action_label=Etiket
@@ -1699,7 +1686,6 @@ pulls.compare_compare=şuradan çek
 pulls.switch_comparison_type=Karşılaştırma türünü değiştir
 pulls.switch_head_and_base=Ana ve temeli değiştir
 pulls.filter_branch=Dal filtrele
-pulls.no_results=Sonuç bulunamadı.
 pulls.show_all_commits=Tüm işlemeleri göster
 pulls.show_changes_since_your_last_review=Son incelemenizden sonraki değişiklikleri göster
 pulls.showing_only_single_commit=Sadece %[1]s işlemesindeki değişiklikler gösteriliyor
@@ -1969,17 +1955,6 @@ activity.git_stats_deletion_n=%d silme oldu
 
 contributors.contribution_type.commits=İşleme
 
-search=Ara
-search.search_repo=Depo ara
-search.type.tooltip=Arama türü
-search.fuzzy=Belirsiz
-search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer
-search.match=Eşleştir
-search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
-search.results=`"%s" için <a href="%s">%s</a> içinde sonuçları ara`
-search.code_no_results=Arama teriminizi içeren kaynak kod bulunamadı.
-search.code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticinizle bağlantıya geçin.
-
 settings=Ayarlar
 settings.desc=Ayarlar, deponun ayarlarını yönetebileceğiniz yerdir
 settings.options=Depo
@@ -2057,6 +2032,7 @@ settings.pulls.default_allow_edits_from_maintainers=Bakımcıların düzenlemele
 settings.releases_desc=Depo Sürümlerini Etkinleştir
 settings.packages_desc=Depo Paket Kütüğünü Etkinleştir
 settings.projects_desc=Depo Projelerini Etkinleştir
+settings.projects_mode_all=Tüm projeler
 settings.actions_desc=Depo İşlemlerini Etkinleştir
 settings.admin_settings=Yönetici Ayarları
 settings.admin_enable_health_check=Depo Sağlık Kontrollerini Etkinleştir (git fsck)
@@ -2131,7 +2107,6 @@ settings.delete_collaborator=Sil
 settings.collaborator_deletion=Katkıcıyı Sil
 settings.collaborator_deletion_desc=Bir katkıcıyı silmek, bu depoya erişimini iptal edecektir. Devam et?
 settings.remove_collaborator_success=Katkıcı silindi.
-settings.search_user_placeholder=Kullanıcı ara…
 settings.org_not_allowed_to_be_collaborator=Organizasyonlar katkıcı olarak eklenemez.
 settings.change_team_access_not_allowed=Depo için takım erişimini değiştirmek, organizasyon sahibiyle sınırlandırıldı
 settings.team_not_in_organization=Takım, depo ile aynı organizasyonda değil
@@ -2139,7 +2114,6 @@ settings.teams=Takımlar
 settings.add_team=Takım Ekle
 settings.add_team_duplicate=Takım zaten bu depoya sahip
 settings.add_team_success=Takım artık bu depoya erişebilir.
-settings.search_team=Takım Ara…
 settings.change_team_permission_tip=Takımın izni takım ayarı sayfasında ayarlanır ve depo başına değiştirilemez
 settings.delete_team_tip=Bu takımın tüm depolara erişimi var ve kaldırılamıyor
 settings.remove_team_success=Takımın depoya erişimi kaldırıldı.
@@ -2292,9 +2266,7 @@ settings.protect_whitelist_committers=Beyaz Liste Kısıtlı Gönderme
 settings.protect_whitelist_committers_desc=Sadece beyaz listeye alınmış kullanıcıların veya takımların bu dala göndermesine izin verilir (ancak zorla gönderim yapmayın).
 settings.protect_whitelist_deploy_keys=Beyaz liste göndermek için yazma erişimi olan anahtarları dağıtır.
 settings.protect_whitelist_users=İtme için beyaz listedeki kullanıcılar:
-settings.protect_whitelist_search_users=Kullanıcı ara…
 settings.protect_whitelist_teams=İtme için beyaz listedeki takımlar:
-settings.protect_whitelist_search_teams=Takımları ara…
 settings.protect_merge_whitelist_committers=Birleştirme Beyaz Listesini Etkinleştir
 settings.protect_merge_whitelist_committers_desc=Yalnızca beyaz listedeki kullanıcıların veya takımların bu daldaki değişiklik isteklerini birleştirmesine izin verin.
 settings.protect_merge_whitelist_users=Birleştirme için beyaz listedeki kullanıcılar:
@@ -2536,7 +2508,6 @@ branch.default_deletion_failed=`"%s" dalı varsayılan daldır. Silinemez.`
 branch.restore=`"%s" Dalını Geri Yükle`
 branch.download=`"%s" Dalını İndir`
 branch.rename=`"%s" Dalının Adını Değiştir`
-branch.search=Dal Ara
 branch.included_desc=Bu dal varsayılan dalın bir parçasıdır
 branch.included=Dahil
 branch.create_new_branch=Şu daldan dal oluştur:
@@ -2674,7 +2645,6 @@ teams.write_permission_desc=Bu takım <strong>Yazma</strong> erişimi veriyor. 
 teams.admin_permission_desc=Bu takım <strong>Yönetici</strong> erişimi veriyor. Üyeler takım depolarını okuyabilir, itebilir ve katkıcı ekleyebilir.
 teams.create_repo_permission_desc=Ayrıca, bu takım <strong>Depo oluşturma</strong> izni verir: üyeler organizasyonda yeni depolar oluşturabilir.
 teams.repositories=Takım Depoları
-teams.search_repo_placeholder=Depo ara…
 teams.remove_all_repos_title=Tüm takım depolarını kaldır
 teams.remove_all_repos_desc=Bu, tüm depoları takımdan kaldıracaktır.
 teams.add_all_repos_title=Tüm depoları ekle
@@ -2706,6 +2676,8 @@ integrations=Bütünleştirmeler
 authentication=Yetkilendirme Kaynakları
 emails=Kullanıcı E-postaları
 config=Yapılandırma
+config_summary=Özet
+config_settings=Ayarlar
 notices=Sistem Bildirimler
 monitor=İzleme
 first_page=İlk
@@ -2880,9 +2852,6 @@ repos.unadopted.no_more=Kabul edilmemiş başka depo bulunamadı
 repos.owner=Sahibi
 repos.name=İsim
 repos.private=Özel
-repos.watches=İzlemeler
-repos.stars=Yıldızlar
-repos.forks=Çatallar
 repos.issues=Konular
 repos.size=Boyut
 repos.lfs_size=LFS Boyutu
@@ -3007,7 +2976,6 @@ auths.tip.nextcloud=Aşağıdaki "Ayarlar -> Güvenlik -> OAuth 2.0 istemcisi" m
 auths.tip.dropbox=https://www.dropbox.com/developers/apps adresinde yeni bir uygulama oluştur
 auths.tip.facebook=https://developers.facebook.com/apps adresinde yeni bir uygulama kaydedin ve "Facebook Giriş" ürününü ekleyin
 auths.tip.github=https://github.com/settings/applications/new adresinde yeni bir OAuth uygulaması kaydedin
-auths.tip.gitlab=https://gitlab.com/profile/applications adresinde yeni bir uygulama kaydedin
 auths.tip.google_plus=OAuth2 istemci kimlik bilgilerini https://console.developers.google.com/ adresindeki Google API konsolundan edinin
 auths.tip.openid_connect=Bitiş noktalarını belirlemek için OpenID Connect Discovery URL'sini kullanın (<server>/.well-known/openid-configuration)
 auths.tip.twitter=https://dev.twitter.com/apps adresine gidin, bir uygulama oluşturun ve “Bu uygulamanın Twitter ile oturum açmak için kullanılmasına izin ver” seçeneğinin etkin olduğundan emin olun
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 9aa6d6a16e..09561a7902 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -101,6 +101,15 @@ concept_user_organization=Організація
 
 name=Назва
 
+filter=Фільтр
+filter.is_archived=Архівовані
+filter.is_template=Шаблон
+filter.public=Публічний
+filter.private=Приватний
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -236,7 +245,6 @@ collaborative_repos=Спільні репозиторії
 my_orgs=Мої організації
 my_mirrors=Мої дзеркала
 view_home=Переглянути %s
-search_repos=Шукати репозиторій…
 filter=Інші фільтри
 filter_by_team_repositories=Фільтрувати за репозиторіями команд
 feed_of=`Стрічка "%s"`
@@ -257,14 +265,7 @@ issues.in_your_repos=В ваших репозиторіях
 repos=Репозиторії
 users=Користувачі
 organizations=Організації
-search=Пошук
 code=Код
-search.fuzzy=Неточний
-search.match=Відповідність
-repo_no_results=Відповідних репозиторіїв не знайдено.
-user_no_results=Відповідних користувачів не знайдено.
-org_no_results=Відповідних організацій не знайдено.
-code_no_results=Відповідний пошуковому запитанню код не знайдено.
 code_last_indexed_at=Останні індексовані %s
 
 [auth]
@@ -277,7 +278,6 @@ remember_me=Запам’ятати цей пристрій
 forgot_password_title=Забув пароль
 forgot_password=Забули пароль?
 sign_up_now=Потрібен обліковий запис? Зареєструйтеся зараз.
-confirmation_mail_sent_prompt=Новий лист для підтвердження було відправлено на <b>%s</b>, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації.
 must_change_password=Оновіть свій пароль
 allow_password_change=Вимагати в користувача змінити пароль (рекомендується)
 reset_password_mail_sent_prompt=Електронний лист із підтвердженням надіслано <b>%s</b>. Перевірте папку 'Вхідні' в межах наступних %s, щоб завершити процес відновлення облікового запису.
@@ -483,6 +483,7 @@ user_bio=Біографія
 disabled_public_activity=Цей користувач вимкнув публічний показ діяльності.
 
 
+
 [settings]
 profile=Профіль
 account=Обліковий запис
@@ -599,7 +600,6 @@ gpg_invalid_token_signature=Наданий ключ GPG, підпис і ток
 gpg_token_required=Вам потрібно надати підпис для нижчевказаного токена
 gpg_token=Токен
 gpg_token_help=Ви можете створити підпис за допомогою:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Текстовий (armored) підпис GPG
 key_signature_gpg_placeholder=`Починається з "-----BEGIN PGP SIGNATURE-----"`
 ssh_key_verified=Перевірений ключ
@@ -738,7 +738,6 @@ fork_repo=Форкнути репозиторій
 fork_from=Форк з
 fork_visibility_helper=Неможливо змінити видимість форкнутого репозиторію.
 use_template=Застосувати цей шаблон
-clone_in_vsc=Клонувати у VS Code
 download_zip=Завантажити ZIP
 download_tar=Завантажити TAR.GZ
 download_bundle=Завантажити BUNDLE
@@ -980,8 +979,6 @@ editor.require_signed_commit=Гілка вимагає підписаного к
 commits.desc=Переглянути історію зміни коду.
 commits.commits=Коміти
 commits.nothing_to_compare=Ці гілки однакові.
-commits.search=Знайти коміт…
-commits.find=Пошук
 commits.search_all=Усі гілки
 commits.author=Автор
 commits.message=Повідомлення
@@ -1019,7 +1016,6 @@ projects.type.basic_kanban=Спрощений канбан
 projects.type.bug_triage=Сортування помилок
 projects.template.desc=Шаблон проєкту
 projects.template.desc_helper=Оберіть шаблон проєкту, аби почати
-projects.type.uncategorized=Без категорії
 projects.column.edit_title=Назва
 projects.column.new_title=Назва
 projects.column.color=Колір
@@ -1311,7 +1307,6 @@ pulls.compare_compare=pull з
 pulls.switch_comparison_type=Перемкнути вигляд порівняння
 pulls.switch_head_and_base=Поміняти місцями основну та базову гілку
 pulls.filter_branch=Фільтр по гілці
-pulls.no_results=Результатів не знайдено.
 pulls.nothing_to_compare=Ці гілки однакові. Немає необхідності створювати запитів на злиття.
 pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім.
 pulls.has_pull_request=`Запит злиття для цих гілок вже існує: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1511,13 +1506,6 @@ activity.git_stats_deletion_n=%d видалені
 
 contributors.contribution_type.commits=Коміти
 
-search=Пошук
-search.search_repo=Пошук репозиторію
-search.fuzzy=Неточний
-search.match=Збігається
-search.results=Результати пошуку для "%s" в <a href="%s">%s</a>
-search.code_no_results=Відповідний пошуковому запитанню код не знайдено.
-
 settings=Налаштування
 settings.desc=У налаштуваннях ви можете змінювати різні параметри цього репозиторія
 settings.options=Репозиторій
@@ -1633,7 +1621,6 @@ settings.delete_collaborator=Видалити
 settings.collaborator_deletion=Видалити співавтора
 settings.collaborator_deletion_desc=Цей користувач більше не матиме доступу для спільної роботи в цьому репозиторії після видалення. Ви хочете продовжити?
 settings.remove_collaborator_success=Співавтор видалений.
-settings.search_user_placeholder=Пошук користувача…
 settings.org_not_allowed_to_be_collaborator=Організації не можуть бути додані як співавтори.
 settings.change_team_access_not_allowed=Зміна доступу команди до репозитарію обмежена власником організації
 settings.team_not_in_organization=Команда та репозитарій мають привязки до різних організацій
@@ -1641,7 +1628,6 @@ settings.teams=Команди
 settings.add_team=Додати Команду
 settings.add_team_duplicate=Команда вже має привязку до репозитарію
 settings.add_team_success=Команда отримала доступ до репозиторію.
-settings.search_team=Знайти команду…
 settings.change_team_permission_tip=Дозволи команди встановлюються на сторінці налаштувань команди та не можуть бути заданими для кожного з репозиторіїв окремо
 settings.delete_team_tip=Ця команда має доступ до всіх репозиторіїв та не може бути видалена
 settings.remove_team_success=Доступ команди до репозиторію видалений.
@@ -1758,9 +1744,7 @@ settings.protect_whitelist_committers=Білий список обмеження
 settings.protect_whitelist_committers_desc=Лише користувачі та команди з білого списку зможуть виконувати push в цій гілці (за виключеням force push).
 settings.protect_whitelist_deploy_keys=Білий список ключів розгортання з правом на запис.
 settings.protect_whitelist_users=Користувачі, які можуть робити push в цю гілку:
-settings.protect_whitelist_search_users=Пошук користувачів…
 settings.protect_whitelist_teams=Команди, учасники яких можуть робити push в цю гілку:
-settings.protect_whitelist_search_teams=Пошук команд…
 settings.protect_merge_whitelist_committers=Обмежити право на прийняття Pull Request'ів в цю гілку списком
 settings.protect_merge_whitelist_committers_desc=Ви можете додавати користувачів або цілі команди в 'білий' список цієї гілки. Тільки присутні в списку зможуть приймати запити на злиття. В іншому випадку будь-хто з правами запису до головного репозиторію буде володіти такою можливістю.
 settings.protect_merge_whitelist_users=Користувачі з правом на прийняття Pull Request'ів в цю гілку:
@@ -2057,7 +2041,6 @@ teams.write_permission_desc=Ця команда надає доступ на <st
 teams.admin_permission_desc=Ця команда надає <strong>адміністраторський</strong> доступ: учасники можуть читати, виконувати push команди та додавати співробітників до репозиторію.
 teams.create_repo_permission_desc=Крім того, ця команда надає дозвіл <strong>Створити репозиторій</strong>: учасники можуть створювати нові репозиторії в організації.
 teams.repositories=Репозиторії команди
-teams.search_repo_placeholder=Пошук репозиторію…
 teams.remove_all_repos_title=Видалити всі репозиторії команди
 teams.remove_all_repos_desc=Це видалить усі репозиторії команди.
 teams.add_all_repos_title=Додати всі репозиторії
@@ -2082,6 +2065,8 @@ hooks=Веб-хуки
 authentication=Джерела автентифікації
 emails=Електронні адреси Користувача
 config=Конфігурація
+config_summary=Підсумок
+config_settings=Налаштування
 notices=Сповіщення системи
 monitor=Моніторинг
 first_page=Перша
@@ -2230,9 +2215,6 @@ repos.unadopted.no_more=Не знайдено більше неприйняти
 repos.owner=Власник
 repos.name=Назва
 repos.private=Приватний
-repos.watches=Стежать
-repos.stars=В обраному
-repos.forks=Форки
 repos.issues=Задачі
 repos.size=Розмір
 
@@ -2330,7 +2312,6 @@ auths.tip.nextcloud=`Зареєструйте нового споживача OA
 auths.tip.dropbox=Додайте новий додаток на https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Створіть новий додаток на https://developers.facebook.com/apps і додайте модуль "Facebook Login"`
 auths.tip.github=Додайте OAuth додаток на https://github.com/settings/applications/new
-auths.tip.gitlab=Додайте новий додаток на https://gitlab.com/profile/applications
 auths.tip.google_plus=Отримайте облікові дані клієнта OAuth2 в консолі Google API на сторінці https://console.developers.google.com/
 auths.tip.openid_connect=Використовуйте OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) для автоматичної настройки входу OAuth
 auths.tip.twitter=Перейдіть на https://dev.twitter.com/apps, створіть програму і переконайтеся, що включена опція «Дозволити цю програму для входу в систему за допомогою Twitter»
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 89f237a117..406e9ac8f2 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -142,6 +142,15 @@ confirm_delete_selected=确认删除所有选中项目?
 name=名称
 value=值
 
+filter=过滤
+filter.is_archived=已归档
+filter.is_template=模板
+filter.public=公开
+filter.private=私有库
+
+
+[search]
+
 [aria]
 navbar=导航栏
 footer=页脚
@@ -315,7 +324,6 @@ collaborative_repos=参与协作的仓库
 my_orgs=我的组织
 my_mirrors=我的镜像
 view_home=访问 %s
-search_repos=查找仓库…
 filter=其他过滤器
 filter_by_team_repositories=按团队仓库筛选
 feed_of=`"%s"的源`
@@ -336,20 +344,8 @@ issues.in_your_repos=在您的仓库中
 repos=仓库
 users=用户
 organizations=组织
-search=搜索
 go_to=转到
 code=代码
-search.type.tooltip=搜索类型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似匹配搜索词的结果
-search.match=匹配
-search.match.tooltip=仅包含精确匹配搜索词的结果
-code_search_unavailable=目前代码搜索不可用。请与网站管理员联系。
-repo_no_results=未找到匹配的仓库。
-user_no_results=未找到匹配的用户。
-org_no_results=未找到匹配的组织。
-code_no_results=未找到与搜索字词匹配的源代码。
-code_search_results=“%s” 的搜索结果是
 code_last_indexed_at=最后索引于 %s
 relevant_repositories_tooltip=派生的仓库,以及缺少主题、图标和描述的仓库将被隐藏。
 relevant_repositories=只显示相关的仓库, <a href="%s">显示未过滤结果</a>。
@@ -367,7 +363,6 @@ forgot_password_title=忘记密码
 forgot_password=忘记密码?
 sign_up_now=还没帐户?马上注册。
 sign_up_successful=帐户创建成功。欢迎!
-confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %s 内完成确认注册操作。
 must_change_password=更新您的密码
 allow_password_change=要求用户更改密码(推荐)
 reset_password_mail_sent_prompt=确认电子邮件已被发送到 <b>%s</b>。请您在 %s 内检查您的收件箱 ,完成密码重置过程。
@@ -617,6 +612,7 @@ form.name_reserved=用户名 "%s" 被保留。
 form.name_pattern_not_allowed=用户名中不允许使用 "%s" 格式。
 form.name_chars_not_allowed=用户名 "%s" 包含无效字符。
 
+
 [settings]
 profile=个人信息
 account=账号
@@ -761,7 +757,6 @@ gpg_invalid_token_signature=提供的 GPG 密钥、签名和令牌不匹配或
 gpg_token_required=您必须为下面的令牌提供签名
 gpg_token=令牌
 gpg_token_help=您可以使用以下方式生成签名:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=GPG 增强签名
 key_signature_gpg_placeholder=以 '-----BEGIN PGP PUBLIC KEY BLOCK-----' 开头
 verify_gpg_key_success=GPG 密钥 %s 已被验证。
@@ -955,7 +950,6 @@ fork_branch=要克隆到 Fork 的分支
 all_branches=所有分支
 fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的所有者。
 use_template=使用此模板
-clone_in_vsc=在 VS Code 中克隆
 download_zip=下载 ZIP
 download_tar=下载 TAR.GZ
 download_bundle=下载 BUNDLE
@@ -1280,9 +1274,7 @@ commits.desc=浏览代码修改历史
 commits.commits=次代码提交
 commits.no_commits=没有共同的提交。%s 和 %s 的历史完全不同。
 commits.nothing_to_compare=这些分支是相同的。
-commits.search=搜索提交历史
 commits.search.tooltip=`您可以在关键词前加上前缀,如"author:", "committer:", "after:", 或"before:", 例如 "retrin author:Alice before:2019-01-13"`
-commits.find=搜索
 commits.search_all=所有分支
 commits.author=作者
 commits.message=备注
@@ -1333,7 +1325,6 @@ projects.type.basic_kanban=基础看板
 projects.type.bug_triage=Bug分类看板
 projects.template.desc=项目模板
 projects.template.desc_helper=选择一个项目模板以开始
-projects.type.uncategorized=未分类
 projects.column.edit=编辑列
 projects.column.edit_title=名称
 projects.column.new_title=名称
@@ -1341,10 +1332,7 @@ projects.column.new_submit=创建列
 projects.column.new=创建列
 projects.column.set_default=设为默认
 projects.column.set_default_desc=设置此列为未分类问题和合并请求的默认值
-projects.column.unset_default=取消设为默认
-projects.column.unset_default_desc=取消此列为默认值
 projects.column.delete=删除列
-projects.column.deletion_desc=删除项目列会将所有相关问题移到“未分类”。是否继续?
 projects.column.color=彩色
 projects.open=开启
 projects.close=关闭
@@ -1456,7 +1444,6 @@ issues.filter_sort.moststars=点赞由多到少
 issues.filter_sort.feweststars=点赞由少到多
 issues.filter_sort.mostforks=派生由多到少
 issues.filter_sort.fewestforks=派生由少到多
-issues.keyword_search_unavailable=关键词搜索目前不可用。请联系网站管理员。
 issues.action_open=开启
 issues.action_close=关闭
 issues.action_label=标签
@@ -1708,7 +1695,6 @@ pulls.compare_compare=拉取从
 pulls.switch_comparison_type=切换比较类型
 pulls.switch_head_and_base=切换 head 和 base
 pulls.filter_branch=过滤分支
-pulls.no_results=未找到结果
 pulls.show_all_commits=显示所有提交
 pulls.show_changes_since_your_last_review=显示自您上次审核以来的更改
 pulls.showing_only_single_commit=仅显示提交 %[1]s 的更改
@@ -1983,17 +1969,6 @@ contributors.contribution_type.commits=提交
 contributors.contribution_type.additions=更多
 contributors.contribution_type.deletions=删除
 
-search=搜索
-search.search_repo=搜索仓库...
-search.type.tooltip=搜索类型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似匹配搜索词的结果
-search.match=匹配
-search.match.tooltip=仅包含精确匹配搜索词的结果
-search.results=在 <a href="%[2]s"> %[3]s </a> 中搜索 "%[1]s" 的结果
-search.code_no_results=未找到与搜索字词匹配的源代码。
-search.code_search_unavailable=当前代码搜索不可用。请与网站管理员联系。
-
 settings=设置
 settings.desc=设置是你可以管理仓库设置的地方
 settings.options=仓库
@@ -2072,6 +2047,7 @@ settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者
 settings.releases_desc=启用发布
 settings.packages_desc=启用仓库软件包注册中心
 settings.projects_desc=启用仓库项目
+settings.projects_mode_all=所有项目
 settings.actions_desc=启用 Actions
 settings.admin_settings=管理员设置
 settings.admin_enable_health_check=启用仓库健康检查 (git fsck)
@@ -2146,7 +2122,6 @@ settings.delete_collaborator=删除
 settings.collaborator_deletion=删除协作者
 settings.collaborator_deletion_desc=删除协作者后他将无法再对此仓库的访问。继续?
 settings.remove_collaborator_success=协作者删除成功!
-settings.search_user_placeholder=搜索用户...
 settings.org_not_allowed_to_be_collaborator=组织不允许被添加为仓库协作者!
 settings.change_team_access_not_allowed=更改仓库的团队访问权限仅限于组织所有者
 settings.team_not_in_organization=团队不在与仓库相同的组织中
@@ -2154,7 +2129,6 @@ settings.teams=团队
 settings.add_team=添加团队
 settings.add_team_duplicate=团队已经拥有仓库
 settings.add_team_success=团队现在可以访问仓库。
-settings.search_team=搜索团队...
 settings.change_team_permission_tip=团队权限设置于团队设置页面,不能根据仓库更改
 settings.delete_team_tip=该团队仍有仓库, 无法删除
 settings.remove_team_success=团队访问仓库的权限已被删除。
@@ -2307,9 +2281,7 @@ settings.protect_whitelist_committers=受白名单限制的推送
 settings.protect_whitelist_committers_desc=只有列入白名单的用户或团队才能被允许推送到此分支(但不能强行推送)。
 settings.protect_whitelist_deploy_keys=具有推送权限的部署密钥白名单。
 settings.protect_whitelist_users=推送白名单用户:
-settings.protect_whitelist_search_users=搜索用户...
 settings.protect_whitelist_teams=推送白名单团队:
-settings.protect_whitelist_search_teams=搜索团队...
 settings.protect_merge_whitelist_committers=启用合并白名单
 settings.protect_merge_whitelist_committers_desc=仅允许白名单用户或团队合并合并请求到此分支。
 settings.protect_merge_whitelist_users=合并白名单用户:
@@ -2554,7 +2526,6 @@ branch.default_deletion_failed=不能删除默认分支"%s"。
 branch.restore=`还原分支 "%s"`
 branch.download=`下载分支 "%s"`
 branch.rename=`重命名分支 "%s"`
-branch.search=搜索分支
 branch.included_desc=此分支是默认分支的一部分
 branch.included=已包含
 branch.create_new_branch=从下列分支创建分支:
@@ -2697,7 +2668,6 @@ teams.write_permission_desc=该团队拥有对所属仓库的 <strong>读取</st
 teams.admin_permission_desc=该团队拥有一定的 <strong>管理</strong> 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。
 teams.create_repo_permission_desc=此外,该团队拥有了 <strong>创建仓库</strong> 的权限:成员可以在组织中创建新的仓库。
 teams.repositories=团队仓库
-teams.search_repo_placeholder=搜索仓库...
 teams.remove_all_repos_title=移除所有团队仓库
 teams.remove_all_repos_desc=这将从团队中移除所有仓库。
 teams.add_all_repos_title=添加所有仓库
@@ -2730,6 +2700,8 @@ integrations=集成
 authentication=认证源
 emails=用户邮件
 config=应用配置
+config_summary=摘要
+config_settings=组织设置
 notices=系统提示
 monitor=监控面板
 first_page=首页
@@ -2906,9 +2878,6 @@ repos.unadopted.no_more=找不到更多未被收录的仓库
 repos.owner=所有者
 repos.name=名称
 repos.private=私有库
-repos.watches=关注数
-repos.stars=点赞数
-repos.forks=派生数
 repos.issues=工单数
 repos.size=大小
 repos.lfs_size=LFS 大小
@@ -3033,7 +3002,6 @@ auths.tip.nextcloud=使用下面的菜单“设置(Settings) -> 安全(Sec
 auths.tip.dropbox=在 https://www.dropbox.com/developers/apps 上创建一个新的应用程序
 auths.tip.facebook=`在 https://developers.facebook.com/apps 注册一个新的应用,并添加产品"Facebook 登录"`
 auths.tip.github=在 https://github.com/settings/applications/new 注册一个 OAuth 应用程序
-auths.tip.gitlab=在 https://gitlab.com/profile/applications 上注册新应用程序
 auths.tip.google_plus=从谷歌 API 控制台 (https://console.developers.google.com/) 获得 OAuth2 客户端凭据
 auths.tip.openid_connect=使用 OpenID 连接发现 URL (<server>/.well-known/openid-configuration) 来指定终点
 auths.tip.twitter=访问 https://dev.twitter.com/apps,创建应用并确保启用了"允许此应用程序用于登录 Twitter"的选项。
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index 8c45e3157f..d4b65239a6 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -61,6 +61,12 @@ concept_code_repository=儲存庫
 
 name=組織名稱
 
+filter.is_template=樣板
+filter.private=私有庫
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -116,13 +122,11 @@ issues.in_your_repos=屬於該用戶儲存庫的
 repos=儲存庫
 users=使用者
 organizations=組織
-search=搜尋
 
 [auth]
 register_helper_msg=已經註冊?立即登錄!
 forgot_password_title=忘記密碼
 forgot_password=忘記密碼?
-confirmation_mail_sent_prompt=一封新的確認郵件已發送至 <b>%s</b>。請檢查您的收件箱並在 %s 小時內完成確認註冊操作。
 active_your_account=啟用您的帳戶
 has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到啟用郵件,或需要重新發送,請單擊下方的按鈕。
 resend_mail=單擊此處重新發送確認郵件
@@ -205,6 +209,7 @@ follow=關注
 unfollow=取消關注
 
 
+
 [settings]
 profile=個人訊息
 password=修改密碼
@@ -375,7 +380,6 @@ editor.cancel=取消
 editor.no_changes_to_show=沒有可以顯示的變更。
 
 commits.commits=次程式碼提交
-commits.find=搜尋
 commits.author=作者
 commits.message=備註
 commits.date=提交日期
@@ -481,7 +485,6 @@ issues.dependency.remove=移除成員
 pulls.new=建立合併請求
 pulls.compare_changes=建立合併請求
 pulls.filter_branch=過濾分支
-pulls.no_results=未找到結果
 pulls.create=建立合併請求
 pulls.merged_title_desc=於 %[4]s 將 %[1]d 次代碼提交從 <code>%[2]s</code>合併至 <code>%[3]s</code>
 pulls.tab_conversation=對話內容
@@ -540,8 +543,6 @@ activity.new_issues_count_1=建立問題
 
 contributors.contribution_type.commits=提交歷史
 
-search=搜尋
-
 settings=儲存庫設定
 settings.desc=設定是您可以管理儲存庫設定的地方
 settings.options=儲存庫
@@ -698,6 +699,7 @@ dashboard=控制面版
 organizations=組織管理
 repositories=儲存庫管理
 config=應用設定管理
+config_settings=組織設定
 notices=系統提示管理
 monitor=應用監控面版
 first_page=首頁
@@ -760,8 +762,6 @@ repos.repo_manage_panel=儲存庫管理
 repos.owner=所有者
 repos.name=儲存庫名稱
 repos.private=私有庫
-repos.watches=關註數
-repos.stars=讚好數
 repos.issues=問題數
 repos.size=大小
 
@@ -809,7 +809,6 @@ auths.tip.oauth2_provider=OAuth2 提供者
 auths.tip.dropbox=建立新 App 在 https://www.dropbox.com/developers/apps
 auths.tip.facebook=`在 https://developers.facebook.com/apps 註冊一個新的應用,並且新增一個產品 "Facebook Login"`
 auths.tip.github=在 https://github.com/settings/applications/new 註冊一個新的 OAuth 應用程式
-auths.tip.gitlab=在 https://gitlab.com/profile/applications 註冊一個新的應用程式
 auths.tip.openid_connect=使用 OpenID 連接探索 URL (<server>/.well-known/openid-configuration) 來指定節點
 auths.delete=刪除認證來源
 auths.delete_auth_title=刪除認證來源
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 09eb262212..0511fa44ae 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -125,6 +125,15 @@ concept_user_organization=組織
 name=名稱
 value=值
 
+filter=篩選
+filter.is_archived=已封存
+filter.is_template=模板
+filter.public=公開
+filter.private=私有
+
+
+[search]
+
 [aria]
 navbar=導航列
 footer=頁尾
@@ -292,7 +301,6 @@ collaborative_repos=參與協作的儲存庫
 my_orgs=我的組織
 my_mirrors=我的鏡像
 view_home=訪問 %s
-search_repos=搜尋儲存庫...
 filter=其他篩選條件
 filter_by_team_repositories=以團隊儲存庫篩選
 feed_of=「%s」的訊息來源
@@ -313,19 +321,7 @@ issues.in_your_repos=在您的儲存庫中
 repos=儲存庫
 users=使用者
 organizations=組織
-search=搜尋
 code=程式碼
-search.type.tooltip=搜尋類型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似關鍵字的結果
-search.match=符合
-search.match.tooltip=只包含完全符合關鍵字的結果
-code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。
-repo_no_results=沒有找到符合的儲存庫。
-user_no_results=沒有找到符合的使用者。
-org_no_results=沒有找到符合的組織。
-code_no_results=找不到符合您關鍵字的原始碼。
-code_search_results=「%s」的搜尋結果
 code_last_indexed_at=最後索引 %s
 relevant_repositories_tooltip=已隱藏缺少主題、圖示、說明、Fork 的儲存庫。
 relevant_repositories=只顯示相關的儲存庫,<a href="%s">顯示未篩選的結果</a>。
@@ -341,7 +337,6 @@ remember_me=記得這個裝置
 forgot_password_title=忘記密碼
 forgot_password=忘記密碼?
 sign_up_now=還沒有帳戶?馬上註冊。
-confirmation_mail_sent_prompt=新的確認信已發送至 <b>%s</b>。請在 %s內檢查您的收件匣並完成註冊作業。
 must_change_password=更新您的密碼
 allow_password_change=要求使用者更改密碼 (推薦)
 reset_password_mail_sent_prompt=確認信已發送至 <b>%s</b>。請在 %s內檢查您的收件匣並完成帳戶救援作業。
@@ -578,6 +573,7 @@ form.name_reserved=「%s」是保留的帳號。
 form.name_pattern_not_allowed=帳號不可包含字元「%s」。
 form.name_chars_not_allowed=帳號「%s」包含無效字元。
 
+
 [settings]
 profile=個人資料
 account=帳戶
@@ -707,7 +703,6 @@ gpg_invalid_token_signature=提供的 GPG 金鑰、簽署、Token 不符合或 T
 gpg_token_required=您必須為下列的 Token 提供簽署
 gpg_token=Token
 gpg_token_help=您可以使用以下方法產生簽署:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Armored GPG 簽署
 key_signature_gpg_placeholder=以「-----BEGIN PGP SIGNATURE-----」開頭
 verify_gpg_key_success=已驗證 GPG 金鑰「%s」。
@@ -867,7 +862,6 @@ already_forked=您已經 fork 過 %s
 fork_to_different_account=Fork 到其他帳戶
 fork_visibility_helper=無法更改 fork 儲存庫的瀏覽權限。
 use_template=使用此範本
-clone_in_vsc=在 VS Code 中 Clone
 download_zip=下載 ZIP
 download_tar=下載 TAR.GZ
 download_bundle=下載 BUNDLE
@@ -1155,9 +1149,7 @@ commits.desc=瀏覽原始碼修改歷程。
 commits.commits=次程式碼提交
 commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。
 commits.nothing_to_compare=這些分支是相同的。
-commits.search=搜尋提交歷史...
 commits.search.tooltip=你可以用「author:」、「committer:」、「after:」、「before:」等作為關鍵字的前綴,例如: 「revert author:Alice before:2019-01-13」。
-commits.find=搜尋
 commits.search_all=所有分支
 commits.author=作者
 commits.message=備註
@@ -1207,7 +1199,6 @@ projects.type.basic_kanban=基本看板
 projects.type.bug_triage=Bug 檢傷分類
 projects.template.desc=範本
 projects.template.desc_helper=選擇專案範本以開始
-projects.type.uncategorized=未分類
 projects.column.edit=編輯欄位
 projects.column.edit_title=名稱
 projects.column.new_title=名稱
@@ -1216,7 +1207,6 @@ projects.column.new=新增欄位
 projects.column.set_default=設為預設
 projects.column.set_default_desc=將此欄位設定為未分類問題及合併請求的預設預設值
 projects.column.delete=刪除欄位
-projects.column.deletion_desc=刪除專案欄位會將所有相關的問題移動到「未分類」,是否繼續?
 projects.column.color=顏色
 projects.open=開啟
 projects.close=關閉
@@ -1552,7 +1542,6 @@ pulls.compare_compare=拉取自
 pulls.switch_comparison_type=切換比較類型
 pulls.switch_head_and_base=切換 head 和 base
 pulls.filter_branch=過濾分支
-pulls.no_results=未找到結果
 pulls.nothing_to_compare=這些分支的內容相同,無需建立合併請求。
 pulls.nothing_to_compare_and_allow_empty_pr=這些分支的內容相同,此合併請求將會是空白的。
 pulls.has_pull_request=`已有介於這些分支間的合併請求:<a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1779,17 +1768,6 @@ activity.git_stats_deletion_n=刪除 %d 行
 
 contributors.contribution_type.commits=提交歷史
 
-search=搜尋
-search.search_repo=搜尋儲存庫
-search.type.tooltip=搜尋類型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似關鍵字的結果
-search.match=符合
-search.match.tooltip=只包含完全符合關鍵字的結果
-search.results=在 <a href="%s"> %s </a> 中搜尋 "%s" 的结果
-search.code_no_results=找不到符合您關鍵字的原始碼。
-search.code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。
-
 settings=設定
 settings.desc=設定是您可以管理儲存庫設定的地方
 settings.options=儲存庫
@@ -1850,6 +1828,7 @@ settings.pulls.default_allow_edits_from_maintainers=預設允許維護者進行
 settings.releases_desc=啟用儲存庫版本發佈
 settings.packages_desc=啟用儲存庫套件註冊中心
 settings.projects_desc=啟用儲存庫專案
+settings.projects_mode_all=所有專案
 settings.actions_desc=啟用儲存庫 Actions
 settings.admin_settings=管理員設定
 settings.admin_enable_health_check=啟用儲存庫的健康檢查 (git fsck)
@@ -1922,7 +1901,6 @@ settings.delete_collaborator=移除
 settings.collaborator_deletion=移除協作者
 settings.collaborator_deletion_desc=移除協作者將拒絕他存取此儲存庫。是否繼續?
 settings.remove_collaborator_success=已移除協作者。
-settings.search_user_placeholder=搜尋使用者...
 settings.org_not_allowed_to_be_collaborator=不可加入組織為協作者。
 settings.change_team_access_not_allowed=只有組織擁有者可修改團隊的儲存庫存取權限
 settings.team_not_in_organization=團隊和儲存庫不在相同的組織內
@@ -1930,7 +1908,6 @@ settings.teams=團隊
 settings.add_team=增加團隊
 settings.add_team_duplicate=團隊已擁有該儲存庫
 settings.add_team_success=團隊現在可存取該儲存庫了。
-settings.search_team=搜尋團隊...
 settings.change_team_permission_tip=團隊權限可於團隊設定頁面修改,不能針對儲存庫分別調整。
 settings.delete_team_tip=此團隊可存取所有儲存庫,無法移除
 settings.remove_team_success=已移除團隊存取儲存庫的權限。
@@ -2077,9 +2054,7 @@ settings.protect_whitelist_committers=使用白名單控管推送
 settings.protect_whitelist_committers_desc=僅允許白名單內的使用者或團隊推送至該分支(但不可使用force push)。
 settings.protect_whitelist_deploy_keys=將擁有寫入權限的部署金鑰加入白名單。
 settings.protect_whitelist_users=允許推送的使用者:
-settings.protect_whitelist_search_users=搜尋使用者...
 settings.protect_whitelist_teams=允許推送的團隊:
-settings.protect_whitelist_search_teams=搜尋團隊...
 settings.protect_merge_whitelist_committers=啟用合併白名單
 settings.protect_merge_whitelist_committers_desc=僅允許白名單內的使用者或團隊將合併請求合併至該分支。
 settings.protect_merge_whitelist_users=允許合併的使用者:
@@ -2427,7 +2402,6 @@ teams.write_permission_desc=這個團隊擁有<strong>寫入</strong> 權限:
 teams.admin_permission_desc=這個團隊擁有<strong>管理員</strong> 權限:成員可以讀取、推送和增加協作者到儲存庫。
 teams.create_repo_permission_desc=此外,這個團隊還擁有<strong>建立儲存庫</strong>的權限:成員可以在組織中新增儲存庫。
 teams.repositories=團隊儲存庫
-teams.search_repo_placeholder=搜尋儲存庫...
 teams.remove_all_repos_title=移除所有團隊儲存庫
 teams.remove_all_repos_desc=這將從團隊中移除所有儲存庫。
 teams.add_all_repos_title=增加所有儲存庫
@@ -2455,6 +2429,8 @@ hooks=Webhook
 authentication=認證來源
 emails=使用者電子信箱
 config=組態
+config_summary=摘要
+config_settings=設定
 notices=系統提示
 monitor=應用監控面版
 first_page=首頁
@@ -2616,9 +2592,6 @@ repos.unadopted.no_more=找不到其他未接管的儲存庫
 repos.owner=擁有者
 repos.name=名稱
 repos.private=私有
-repos.watches=關注數
-repos.stars=星號數
-repos.forks=Fork 數
 repos.issues=問題數
 repos.size=大小
 
@@ -2737,7 +2710,6 @@ auths.tip.nextcloud=在您的執行個體中,於選單「設定 -> 安全性 -
 auths.tip.dropbox=建立新的 App。網址:https://www.dropbox.com/developers/apps
 auths.tip.facebook=註冊新的應用程式並新增產品「Facebook 登入」。網址:https://developers.facebook.com/apps
 auths.tip.github=註冊新的 OAuth 應用程式。網址:https://github.com/settings/applications/new
-auths.tip.gitlab=註冊新的應用程式。網址:https://gitlab.com/profile/applications
 auths.tip.google_plus=從 Google API 控制台取得 OAuth2 用戶端憑證。網址:https://console.developers.google.com/
 auths.tip.openid_connect=使用 OpenID 連接探索 URL (<server>/.well-known/openid-configuration) 來指定節點
 auths.tip.twitter=建立應用程式並確保有啟用「Allow this application to be used to Sign in with Twitter」。網址:https://dev.twitter.com/apps

From 2b3f7d3e966ab60cb147115303d1992e8b50d4df Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 31 Mar 2024 00:30:00 +0300
Subject: [PATCH 587/679] Remove jQuery class from the repository branch
 settings (#30184)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the repository branch settings functionality and it works as
before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-settings.js | 31 ++++++++++++++++------------
 1 file changed, 18 insertions(+), 13 deletions(-)

diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 0ea44130d0..52c5de2bfa 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -77,18 +77,24 @@ export function initRepoSettingGitHook() {
 }
 
 export function initRepoSettingBranches() {
-  if (!$('.repository.settings.branches').length) return;
-  $('.toggle-target-enabled').on('change', function () {
-    const $target = $(this.getAttribute('data-target'));
-    $target.toggleClass('disabled', !this.checked);
-  });
-  $('.toggle-target-disabled').on('change', function () {
-    const $target = $(this.getAttribute('data-target'));
-    if (this.checked) $target.addClass('disabled'); // only disable, do not auto enable
-  });
-  $('#dismiss_stale_approvals').on('change', function () {
-    const $target = $('#ignore_stale_approvals_box');
-    $target.toggleClass('disabled', this.checked);
+  if (!document.querySelector('.repository.settings.branches')) return;
+
+  for (const el of document.getElementsByClassName('toggle-target-enabled')) {
+    el.addEventListener('change', function () {
+      const target = document.querySelector(this.getAttribute('data-target'));
+      target?.classList.toggle('disabled', !this.checked);
+    });
+  }
+
+  for (const el of document.getElementsByClassName('toggle-target-disabled')) {
+    el.addEventListener('change', function () {
+      const target = document.querySelector(this.getAttribute('data-target'));
+      if (this.checked) target?.classList.add('disabled'); // only disable, do not auto enable
+    });
+  }
+
+  document.getElementById('dismiss_stale_approvals')?.addEventListener('change', function () {
+    document.getElementById('ignore_stale_approvals_box')?.classList.toggle('disabled', this.checked);
   });
 
   // show the `Matched` mark for the status checks that match the pattern
@@ -106,7 +112,6 @@ export function initRepoSettingBranches() {
           break;
         }
       }
-
       toggleElem(el, matched);
     }
   };

From 6aeff21b76fcbb10d5ce9009ed4243c14633d899 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 31 Mar 2024 01:09:46 +0300
Subject: [PATCH 588/679] Remove jQuery class from the comment edit history
 (#30186)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the comment edit history functionality and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-issue-content.js | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index 3c4efe0447..cef2f49008 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
 import {svg} from '../svg.js';
 import {showErrorToast} from '../modules/toast.js';
 import {GET, POST} from '../modules/fetch.js';
+import {showElem} from '../utils/dom.js';
 
 const {appSubUrl} = window.config;
 let i18nTextEdited;
@@ -73,10 +74,12 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
         const response = await GET(url);
         const resp = await response.json();
 
-        $dialog.find('.comment-diff-data').removeClass('is-loading').html(resp.diffHtml);
+        const commentDiffData = $dialog.find('.comment-diff-data')[0];
+        commentDiffData?.classList.remove('is-loading');
+        commentDiffData.innerHTML = resp.diffHtml;
         // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden.
         if (resp.canSoftDelete) {
-          $dialog.find('.dialog-header-options').removeClass('tw-hidden');
+          showElem($dialog.find('.dialog-header-options'));
         }
       } catch (error) {
         console.error('Error:', error);

From 72a5d3faa8b65042a4fc7525d511d8942a47dafe Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 31 Mar 2024 01:14:57 +0300
Subject: [PATCH 589/679] Remove jQuery class from the issue author dropdown
 (#30188)

- Switched from jQuery class functions to plain JavaScript `classList`
- Tested the issue author dropdown functionality and it works as before

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-issue-list.js | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 4582f87425..ccd13bbcf5 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -6,6 +6,7 @@ import {confirmModal} from './comp/ConfirmModal.js';
 import {showErrorToast} from '../modules/toast.js';
 import {createSortable} from '../modules/sortable.js';
 import {DELETE, POST} from '../modules/fetch.js';
+import {parseDom} from '../utils.js';
 
 function initRepoIssueListCheckboxes() {
   const issueSelectAll = document.querySelector('.issue-checkbox-all');
@@ -129,22 +130,27 @@ function initRepoIssueListAuthorDropdown() {
   const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
   $searchDropdown.dropdown('internal', 'setup', dropdownSetup);
   dropdownSetup.menu = function (values) {
-    const $menu = $searchDropdown.find('> .menu');
-    $menu.find('> .dynamic-item').remove(); // remove old dynamic items
+    const menu = $searchDropdown.find('> .menu')[0];
+    // remove old dynamic items
+    for (const el of menu.querySelectorAll(':scope > .dynamic-item')) {
+      el.remove();
+    }
 
     const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className'));
     if (newMenuHtml) {
-      const $newMenuItems = $(newMenuHtml);
-      $newMenuItems.addClass('dynamic-item');
+      const newMenuItems = parseDom(newMenuHtml, 'text/html').querySelectorAll('body > div');
+      for (const newMenuItem of newMenuItems) {
+        newMenuItem.classList.add('dynamic-item');
+      }
       const div = document.createElement('div');
       div.classList.add('divider', 'dynamic-item');
-      $menu[0].append(div, ...$newMenuItems);
+      menu.append(div, ...newMenuItems);
     }
     $searchDropdown.dropdown('refresh');
     // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
     setTimeout(() => {
-      $menu.find('.item.active, .item.selected').removeClass('active selected');
-      $menu.find(`.item[data-value="${selectedUserId}"]`).addClass('selected');
+      menu.querySelector('.item.active, .item.selected')?.classList.remove('active', 'selected');
+      menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected');
     }, 0);
   };
 }

From 640850e15f56bbe01f5d8ea407f99c79dc38457e Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 31 Mar 2024 01:00:58 +0100
Subject: [PATCH 590/679] Fix unclickable checkboxes (#30195)

Fix https://github.com/go-gitea/gitea/issues/30185, regression from
https://github.com/go-gitea/gitea/pull/30162.

The checkboxes were unclickable because the label was positioned over
the checkbox with `padding`. Now it uses `margin` so the checkbox itself
will be clickable in all cases.

Secondly, I changed the for/id linking to also add missing `for`
attributes when `id` is present. The other way around (only `for`
present) is currently not handled and I think there are likey no
occurences in the code and introducing new non-generated `id`s might
cause problems elsewhere if we do, so I skipped on that.
---
 web_src/css/modules/checkbox.css        |  2 +-
 web_src/js/modules/fomantic/checkbox.js | 17 +++++++++++++----
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css
index fc44a7c115..9238e0b3f3 100644
--- a/web_src/css/modules/checkbox.css
+++ b/web_src/css/modules/checkbox.css
@@ -41,7 +41,7 @@ input[type="radio"] {
 
 .ui.checkbox label,
 .ui.radio.checkbox label {
-  padding-left: 1.85714em;
+  margin-left: 1.85714em;
 }
 
 .ui.checkbox + label {
diff --git a/web_src/js/modules/fomantic/checkbox.js b/web_src/js/modules/fomantic/checkbox.js
index ffe853b28f..7f2b340296 100644
--- a/web_src/js/modules/fomantic/checkbox.js
+++ b/web_src/js/modules/fomantic/checkbox.js
@@ -6,10 +6,19 @@ export function initAriaCheckboxPatch() {
     if (el.hasAttribute('data-checkbox-patched')) continue;
     const label = el.querySelector('label');
     const input = el.querySelector('input');
-    if (!label || !input || input.getAttribute('id') || label.getAttribute('for')) continue;
-    const id = generateAriaId();
-    input.setAttribute('id', id);
-    label.setAttribute('for', id);
+    if (!label || !input) continue;
+    const inputId = input.getAttribute('id');
+    const labelFor = label.getAttribute('for');
+
+    if (inputId && !labelFor) { // missing "for"
+      label.setAttribute('for', inputId);
+    } else if (!inputId && !labelFor) { // missing both "id" and "for"
+      const id = generateAriaId();
+      input.setAttribute('id', id);
+      label.setAttribute('for', id);
+    } else {
+      continue;
+    }
     el.setAttribute('data-checkbox-patched', 'true');
   }
 }

From 7eb3ab076549f83fadf4034f77e7794e785725fa Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Sun, 31 Mar 2024 00:27:17 +0000
Subject: [PATCH 591/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_de-DE.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 1dacb0e0ee..4d446db86f 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -102,7 +102,7 @@ copy=Kopieren
 copy_url=URL kopieren
 copy_hash=Hash kopieren
 copy_content=Inhalt kopieren
-copy_branch=Branchennamen kopieren
+copy_branch=Branchnamen kopieren
 copy_success=Kopiert!
 copy_error=Kopieren fehlgeschlagen
 copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden

From 82ffd91607ba03907ebad31ec9a38555b153a331 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sun, 31 Mar 2024 04:35:19 +0200
Subject: [PATCH 592/679] Fix GPG subkey verify (#30193)

Fixes #30189

Can't verify subkeys if they are not loaded.
---
 models/asymkey/gpg_key_verify.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/models/asymkey/gpg_key_verify.go b/models/asymkey/gpg_key_verify.go
index 4cf46ab556..01812a2d54 100644
--- a/models/asymkey/gpg_key_verify.go
+++ b/models/asymkey/gpg_key_verify.go
@@ -46,6 +46,10 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
 		return "", ErrGPGKeyNotExist{}
 	}
 
+	if err := key.LoadSubKeys(ctx); err != nil {
+		return "", err
+	}
+
 	sig, err := extractSignature(signature)
 	if err != nil {
 		return "", ErrGPGInvalidTokenSignature{

From 6d34ce25b16cdfd6e2e364aebe546e3c2fbb76c6 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 31 Mar 2024 11:03:24 +0800
Subject: [PATCH 593/679] Do not allow different storage configurations to
 point to the same directory (#30169)

Replace #29171
---
 modules/setting/indexer.go      |  2 +-
 modules/setting/path.go         |  4 ---
 modules/setting/repository.go   |  2 +-
 modules/setting/server.go       |  3 +-
 modules/setting/session.go      |  2 +-
 modules/setting/setting.go      | 13 ++++---
 modules/setting/storage.go      |  2 +-
 options/locale/locale_en-US.ini |  4 ++-
 routers/web/admin/admin.go      |  8 +++++
 templates/admin/dashboard.tmpl  |  2 +-
 templates/admin/navbar.tmpl     | 18 ++++++----
 templates/admin/self_check.tmpl | 62 ++++++++++++++++++++-------------
 12 files changed, 75 insertions(+), 47 deletions(-)

diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index 15f6150242..cec364d370 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -58,7 +58,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
 		if !filepath.IsAbs(Indexer.IssuePath) {
 			Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
 		}
-		fatalDuplicatedPath("issue_indexer", Indexer.IssuePath)
+		checkOverlappedPath("indexer.ISSUE_INDEXER_PATH", Indexer.IssuePath)
 	} else {
 		Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
 		if Indexer.IssueType == "meilisearch" {
diff --git a/modules/setting/path.go b/modules/setting/path.go
index b2cca0acbf..0fdc305aa1 100644
--- a/modules/setting/path.go
+++ b/modules/setting/path.go
@@ -66,12 +66,8 @@ func init() {
 		AppWorkPath = filepath.Dir(AppPath)
 	}
 
-	fatalDuplicatedPath("app_work_path", AppWorkPath)
-
 	appWorkPathBuiltin = AppWorkPath
 	customPathBuiltin = CustomPath
-
-	fatalDuplicatedPath("custom_path", CustomPath)
 	customConfBuiltin = CustomConf
 }
 
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 7990021aaa..a332d6adb3 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -286,7 +286,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
 		RepoRootPath = filepath.Clean(RepoRootPath)
 	}
 
-	fatalDuplicatedPath("repository.ROOT", RepoRootPath)
+	checkOverlappedPath("repository.ROOT", RepoRootPath)
 
 	defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
 	for _, charset := range Repository.DetectedCharsetsOrder {
diff --git a/modules/setting/server.go b/modules/setting/server.go
index 0dea4e1ac7..315faaeb21 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -324,7 +324,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
 	if !filepath.IsAbs(AppDataPath) {
 		AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
 	}
-	fatalDuplicatedPath("app_data_path", AppDataPath)
 
 	EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
 	EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
@@ -332,7 +331,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
 	if !filepath.IsAbs(PprofDataPath) {
 		PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
 	}
-	fatalDuplicatedPath("pprof_data_path", PprofDataPath)
+	checkOverlappedPath("server.PPROF_DATA_PATH", PprofDataPath)
 
 	landingPage := sec.Key("LANDING_PAGE").MustString("home")
 	switch landingPage {
diff --git a/modules/setting/session.go b/modules/setting/session.go
index 70497e5eaa..3cb1bfe7b5 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -46,7 +46,7 @@ func loadSessionFrom(rootCfg ConfigProvider) {
 	SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
 	if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
 		SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
-		fatalDuplicatedPath("session", SessionConfig.ProviderConfig)
+		checkOverlappedPath("session.PROVIDER_CONFIG", SessionConfig.ProviderConfig)
 	}
 	SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
 	SessionConfig.CookiePath = AppSubURL
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 13821da44d..6aca9ec6cf 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -230,11 +230,14 @@ func LoadSettingsForInstall() {
 	loadMailerFrom(CfgProvider)
 }
 
-var uniquePaths = make(map[string]string)
+var configuredPaths = make(map[string]string)
 
-func fatalDuplicatedPath(name, p string) {
-	if targetName, ok := uniquePaths[p]; ok && targetName != name {
-		log.Fatal("storage path %q is being used by %q and %q and all storage paths must be unique to prevent data loss.", p, targetName, name)
+func checkOverlappedPath(name, path string) {
+	// TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path)
+	if targetName, ok := configuredPaths[path]; ok && targetName != name {
+		msg := fmt.Sprintf("Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
+		log.Error("%s", msg)
+		DeprecatedWarnings = append(DeprecatedWarnings, msg)
 	}
-	uniquePaths[p] = name
+	configuredPaths[path] = name
 }
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index 23b08df101..f4e33a53af 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -240,7 +240,7 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
 		}
 	}
 
-	fatalDuplicatedPath("storage."+name, storage.Path)
+	checkOverlappedPath("storage."+name+".PATH", storage.Path)
 
 	return &storage, nil
 }
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index b7bcf20d30..39b9855186 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2775,6 +2775,7 @@ teams.invite.by = Invited by %s
 teams.invite.description = Please click the button below to join the team.
 
 [admin]
+maintenance = Maintenance
 dashboard = Dashboard
 self_check = Self Check
 identity_access = Identity & Access
@@ -2798,7 +2799,7 @@ settings = Admin Settings
 
 dashboard.new_version_hint = Gitea %s is now available, you are running %s. Check <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">the blog</a> for more details.
 dashboard.statistic = Summary
-dashboard.operations = Maintenance Operations
+dashboard.maintenance_operations = Maintenance Operations
 dashboard.system_status = System Status
 dashboard.operation_name = Operation Name
 dashboard.operation_switch = Switch
@@ -3305,6 +3306,7 @@ notices.op = Op.
 notices.delete_success = The system notices have been deleted.
 
 self_check.no_problem_found = No problem found yet.
+self_check.startup_warnings = Startup warnings:
 self_check.database_collation_mismatch = Expect database to use collation: %s
 self_check.database_collation_case_insensitive = Database is using a collation %s, which is an insensitive collation. Although Gitea could work with it, there might be some rare cases which don't work as expected.
 self_check.database_inconsistent_collation_columns = Database is using collation %s, but these columns are using mismatched collations. It might cause some unexpected problems.
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index f3f10fd1b8..4dc0dfdef8 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -190,6 +190,14 @@ func DashboardPost(ctx *context.Context) {
 
 func SelfCheck(ctx *context.Context) {
 	ctx.Data["PageIsAdminSelfCheck"] = true
+
+	ctx.Data["DeprecatedWarnings"] = setting.DeprecatedWarnings
+	if len(setting.DeprecatedWarnings) == 0 && !setting.IsProd {
+		if time.Now().Unix()%2 == 0 {
+			ctx.Data["DeprecatedWarnings"] = []string{"This is a test warning message in dev mode"}
+		}
+	}
+
 	r, err := db.CheckCollationsDefaultEngine()
 	if err != nil {
 		ctx.Flash.Error(fmt.Sprintf("CheckCollationsDefaultEngine: %v", err), true)
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl
index bfd2ee6670..589fc5048a 100644
--- a/templates/admin/dashboard.tmpl
+++ b/templates/admin/dashboard.tmpl
@@ -6,7 +6,7 @@
 			</div>
 		{{end}}
 		<h4 class="ui top attached header">
-			{{ctx.Locale.Tr "admin.dashboard.operations"}}
+			{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
 		</h4>
 		<div class="ui attached table segment">
 			<form method="post" action="{{AppSubUrl}}/admin">
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl
index d01a6ab964..1b3b9d6efc 100644
--- a/templates/admin/navbar.tmpl
+++ b/templates/admin/navbar.tmpl
@@ -1,12 +1,18 @@
 <div class="flex-container-nav">
 	<div class="ui fluid vertical menu">
 		<div class="header item">{{ctx.Locale.Tr "admin.settings"}}</div>
-		<a class="{{if .PageIsAdminDashboard}}active {{end}}item" href="{{AppSubUrl}}/admin">
-			{{ctx.Locale.Tr "admin.dashboard"}}
-		</a>
-		<a class="{{if .PageIsAdminSelfCheck}}active {{end}}item" href="{{AppSubUrl}}/admin/self_check">
-			{{ctx.Locale.Tr "admin.self_check"}}
-		</a>
+
+		<details class="item toggleable-item" {{if or .PageIsAdminDashboard .PageIsAdminSelfCheck}}open{{end}}>
+			<summary>{{ctx.Locale.Tr "admin.maintenance"}}</summary>
+			<div class="menu">
+				<a class="{{if .PageIsAdminDashboard}}active {{end}}item" href="{{AppSubUrl}}/admin">
+					{{ctx.Locale.Tr "admin.dashboard"}}
+				</a>
+				<a class="{{if .PageIsAdminSelfCheck}}active {{end}}item" href="{{AppSubUrl}}/admin/self_check">
+					{{ctx.Locale.Tr "admin.self_check"}}
+				</a>
+			</div>
+		</details>
 		<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
 			<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
 			<div class="menu">
diff --git a/templates/admin/self_check.tmpl b/templates/admin/self_check.tmpl
index 94c4673a49..c100ffd504 100644
--- a/templates/admin/self_check.tmpl
+++ b/templates/admin/self_check.tmpl
@@ -4,33 +4,47 @@
 	<h4 class="ui top attached header">
 		{{ctx.Locale.Tr "admin.self_check"}}
 	</h4>
+
+	{{if .DeprecatedWarnings}}
 	<div class="ui attached segment">
-		{{if .DatabaseCheckHasProblems}}
-			{{if .DatabaseType.IsMySQL}}
-				<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
-			{{else if .DatabaseType.IsMSSQL}}
-				<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
-			{{end}}
-			{{if .DatabaseCheckCollationMismatch}}
-				<div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div>
-			{{end}}
-			{{if .DatabaseCheckCollationCaseInsensitive}}
-				<div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div>
-			{{end}}
-			{{if .DatabaseCheckInconsistentCollationColumns}}
-				<div class="ui red message">
-					{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}
-					<ul class="tw-w-full">
-					{{range .DatabaseCheckInconsistentCollationColumns}}
-						<li>{{.}}</li>
-					{{end}}
-					</ul>
-				</div>
-			{{end}}
-		{{else}}
-			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}</div>
+		<div class="ui warning message">
+			<div>{{ctx.Locale.Tr "admin.self_check.startup_warnings"}}</div>
+			<ul class="tw-w-full">{{range .DeprecatedWarnings}}<li>{{.}}</li>{{end}}</ul>
+		</div>
+	</div>
+	{{end}}
+
+	{{if .DatabaseCheckHasProblems}}
+	<div class="ui attached segment">
+		{{if .DatabaseType.IsMySQL}}
+			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
+		{{else if .DatabaseType.IsMSSQL}}
+			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
+		{{end}}
+		{{if .DatabaseCheckCollationMismatch}}
+			<div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div>
+		{{end}}
+		{{if .DatabaseCheckCollationCaseInsensitive}}
+			<div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div>
+		{{end}}
+		{{if .DatabaseCheckInconsistentCollationColumns}}
+			<div class="ui red message">
+				{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}
+				<ul class="tw-w-full">
+				{{range .DatabaseCheckInconsistentCollationColumns}}
+					<li>{{.}}</li>
+				{{end}}
+				</ul>
+			</div>
 		{{end}}
 	</div>
+	{{end}}
+
+	{{if and (not .DeprecatedWarnings) (not .DatabaseCheckHasProblems)}}
+	<div class="ui attached segment">
+		{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}
+	</div>
+	{{end}}
 </div>
 
 {{template "admin/layout_footer" .}}

From ab028356c7f4f29adb99505c078c162a317c7c37 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 31 Mar 2024 19:17:34 +0800
Subject: [PATCH 594/679] Fix markdown color code detection (#30208)

When reviewing PRs, some color names might be mentioned, the
`transformCodeSpan` (which calls `css.ColorHandler`) considered it as a
valid color, but actually it shouldn't be rendered as a color codespan.
---
 modules/markup/markdown/markdown_test.go      |  8 +++++--
 modules/markup/markdown/transform_codespan.go | 21 ++++++++++++++++++-
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index ebac3fbe9e..c664758a27 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -436,6 +436,10 @@ func TestColorPreview(t *testing.T) {
 		testcase string
 		expected string
 	}{
+		{ // do not render color names
+			"The CSS class `red` is there",
+			"<p>The CSS class <code>red</code> is there</p>\n",
+		},
 		{ // hex
 			"`#FF0000`",
 			`<p><code>#FF0000<span class="color-preview" style="background-color: #FF0000"></span></code></p>` + nl,
@@ -445,8 +449,8 @@ func TestColorPreview(t *testing.T) {
 			`<p><code>rgb(16, 32, 64)<span class="color-preview" style="background-color: rgb(16, 32, 64)"></span></code></p>` + nl,
 		},
 		{ // short hex
-			"This is the color white `#000`",
-			`<p>This is the color white <code>#000<span class="color-preview" style="background-color: #000"></span></code></p>` + nl,
+			"This is the color white `#0a0`",
+			`<p>This is the color white <code>#0a0<span class="color-preview" style="background-color: #0a0"></span></code></p>` + nl,
 		},
 		{ // hsl
 			"HSL stands for hue, saturation, and lightness. An example: `hsl(0, 100%, 50%)`.",
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
index bfff2897b0..5b07d72999 100644
--- a/modules/markup/markdown/transform_codespan.go
+++ b/modules/markup/markdown/transform_codespan.go
@@ -49,9 +49,28 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
 	return ast.WalkContinue, nil
 }
 
+// cssColorHandler checks if a string is a render-able CSS color value.
+// The code is from "github.com/microcosm-cc/bluemonday/css.ColorHandler", except that it doesn't handle color words like "red".
+func cssColorHandler(value string) bool {
+	value = strings.ToLower(value)
+	if css.HexRGB.MatchString(value) {
+		return true
+	}
+	if css.RGB.MatchString(value) {
+		return true
+	}
+	if css.RGBA.MatchString(value) {
+		return true
+	}
+	if css.HSL.MatchString(value) {
+		return true
+	}
+	return css.HSLA.MatchString(value)
+}
+
 func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
 	colorContent := v.Text(reader.Source())
-	if css.ColorHandler(strings.ToLower(string(colorContent))) {
+	if cssColorHandler(string(colorContent)) {
 		v.AppendChild(v, NewColorPreview(colorContent))
 	}
 }

From 44dd6d6927180a4d36b3811fd2fb7557d0b44adb Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 31 Mar 2024 13:22:28 +0200
Subject: [PATCH 595/679] Move and simplify tab-size helpers (#30196)

Tailwind does not support. Dropped the vendor-prefix.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/helpers.css | 17 +++++++++
 web_src/css/repo.css    | 80 -----------------------------------------
 2 files changed, 17 insertions(+), 80 deletions(-)

diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 13962f19d7..118c058b19 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -63,3 +63,20 @@ only use:
     display: none !important;
   }
 }
+
+.tab-size-1 { tab-size: 1 !important; }
+.tab-size-2 { tab-size: 2 !important; }
+.tab-size-3 { tab-size: 3 !important; }
+.tab-size-4 { tab-size: 4 !important; }
+.tab-size-5 { tab-size: 5 !important; }
+.tab-size-6 { tab-size: 6 !important; }
+.tab-size-7 { tab-size: 7 !important; }
+.tab-size-8 { tab-size: 8 !important; }
+.tab-size-9 { tab-size: 9 !important; }
+.tab-size-10 { tab-size: 10 !important; }
+.tab-size-11 { tab-size: 11 !important; }
+.tab-size-12 { tab-size: 12 !important; }
+.tab-size-13 { tab-size: 13 !important; }
+.tab-size-14 { tab-size: 14 !important; }
+.tab-size-15 { tab-size: 15 !important; }
+.tab-size-16 { tab-size: 16 !important; }
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 780093fb7f..eab90c10d3 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2260,86 +2260,6 @@
   padding-top: 15px;
 }
 
-.tab-size-1 {
-  tab-size: 1 !important;
-  -moz-tab-size: 1 !important;
-}
-
-.tab-size-2 {
-  tab-size: 2 !important;
-  -moz-tab-size: 2 !important;
-}
-
-.tab-size-3 {
-  tab-size: 3 !important;
-  -moz-tab-size: 3 !important;
-}
-
-.tab-size-4 {
-  tab-size: 4 !important;
-  -moz-tab-size: 4 !important;
-}
-
-.tab-size-5 {
-  tab-size: 5 !important;
-  -moz-tab-size: 5 !important;
-}
-
-.tab-size-6 {
-  tab-size: 6 !important;
-  -moz-tab-size: 6 !important;
-}
-
-.tab-size-7 {
-  tab-size: 7 !important;
-  -moz-tab-size: 7 !important;
-}
-
-.tab-size-8 {
-  tab-size: 8 !important;
-  -moz-tab-size: 8 !important;
-}
-
-.tab-size-9 {
-  tab-size: 9 !important;
-  -moz-tab-size: 9 !important;
-}
-
-.tab-size-10 {
-  tab-size: 10 !important;
-  -moz-tab-size: 10 !important;
-}
-
-.tab-size-11 {
-  tab-size: 11 !important;
-  -moz-tab-size: 11 !important;
-}
-
-.tab-size-12 {
-  tab-size: 12 !important;
-  -moz-tab-size: 12 !important;
-}
-
-.tab-size-13 {
-  tab-size: 13 !important;
-  -moz-tab-size: 13 !important;
-}
-
-.tab-size-14 {
-  tab-size: 14 !important;
-  -moz-tab-size: 14 !important;
-}
-
-.tab-size-15 {
-  tab-size: 15 !important;
-  -moz-tab-size: 15 !important;
-}
-
-.tab-size-16 {
-  tab-size: 16 !important;
-  -moz-tab-size: 16 !important;
-}
-
 .stats-table {
   display: table;
   width: 100%;

From f8fbaaf26fa7798fde690f4400910069fbccd40e Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 31 Mar 2024 14:27:39 +0300
Subject: [PATCH 596/679] Make a distinction between `active` and `selected` in
 the issue author dropdown (#30207)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
---
 web_src/js/features/repo-issue-list.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index ccd13bbcf5..92f058c4d2 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -149,7 +149,9 @@ function initRepoIssueListAuthorDropdown() {
     $searchDropdown.dropdown('refresh');
     // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
     setTimeout(() => {
-      menu.querySelector('.item.active, .item.selected')?.classList.remove('active', 'selected');
+      for (const el of menu.querySelectorAll('.item.active, .item.selected')) {
+        el.classList.remove('active', 'selected');
+      }
       menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected');
     }, 0);
   };

From f691721714cba2a1a11e69c2b3da323b031620ff Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 31 Mar 2024 13:35:11 +0200
Subject: [PATCH 597/679] Remove `modifies/frontend` from labeler (#30198)

Remove this label, I find it barely useful and we already have more
useful labels like `modifies/js`. Backport so that we can eventually
delete that label.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 .github/labeler.yml | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/.github/labeler.yml b/.github/labeler.yml
index 4acdb6f6f5..d1b4d00d80 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -4,13 +4,6 @@ modifies/docs:
           - "**/*.md"
           - "docs/**"
 
-modifies/frontend:
-  - changed-files:
-      - any-glob-to-any-file:
-          - "web_src/**"
-          - "tailwind.config.js"
-          - "webpack.config.js"
-
 modifies/templates:
   - changed-files:
       - all-globs-to-any-file:

From 38d56ca10600bdb867b363be717f7cf5d176297a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 31 Mar 2024 13:41:28 +0200
Subject: [PATCH 598/679] Ignore fomantic folder in linters (#30200)

We are not linting these files but editor integrations will still try to
lint, disable that.
---
 .eslintrc.yaml      | 1 +
 stylelint.config.js | 1 +
 2 files changed, 2 insertions(+)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 99ce2e97d6..43edd14cec 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -3,6 +3,7 @@ reportUnusedDisableDirectives: true
 
 ignorePatterns:
   - /web_src/js/vendor
+  - /web_src/fomantic
 
 parserOptions:
   sourceType: module
diff --git a/stylelint.config.js b/stylelint.config.js
index c34181233e..523b18841e 100644
--- a/stylelint.config.js
+++ b/stylelint.config.js
@@ -16,6 +16,7 @@ export default {
   ],
   ignoreFiles: [
     '**/*.go',
+    '/web_src/fomantic',
   ],
   overrides: [
     {

From ef5892d988f71743c7f5446bc6ce69cb4384455b Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 31 Mar 2024 15:01:21 +0300
Subject: [PATCH 599/679] Remove jQuery class from the `repo-issue.js` file
 (#30192)

Switched from jQuery class functions to plain JavaScript `classList`.

Tested the following functionalities and they work as before:
- delete issue comment
- cancel code comment
- update (merge or rebase) pull request
- re-request review
- reply to code comment
- show/hide outdated comments
- add code comment
- edit issue title

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/repo-issue.js | 155 ++++++++++++++++--------------
 1 file changed, 85 insertions(+), 70 deletions(-)

diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index bb45c6ae57..0d326aae58 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -158,17 +158,22 @@ export function initRepoIssueSidebarList() {
 
 export function initRepoIssueCommentDelete() {
   // Delete comment
-  $(document).on('click', '.delete-comment', async function () {
-    const $this = $(this);
-    if (window.confirm($this.data('locale'))) {
+  document.addEventListener('click', async (e) => {
+    if (!e.target.matches('.delete-comment')) return;
+    e.preventDefault();
+
+    const deleteButton = e.target;
+    if (window.confirm(deleteButton.getAttribute('data-locale'))) {
       try {
-        const response = await POST($this.data('url'));
+        const response = await POST(deleteButton.getAttribute('data-url'));
         if (!response.ok) throw new Error('Failed to delete comment');
-        const $conversationHolder = $this.closest('.conversation-holder');
-        const $parentTimelineItem = $this.closest('.timeline-item');
-        const $parentTimelineGroup = $this.closest('.timeline-item-group');
+
+        const conversationHolder = deleteButton.closest('.conversation-holder');
+        const parentTimelineItem = deleteButton.closest('.timeline-item');
+        const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
+
         // Check if this was a pending comment.
-        if ($conversationHolder.find('.pending-label').length) {
+        if (conversationHolder?.querySelector('.pending-label')) {
           const counter = document.querySelector('#review-box .review-comments-counter');
           let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
           num = Math.max(num, 0);
@@ -176,29 +181,32 @@ export function initRepoIssueCommentDelete() {
           counter.textContent = String(num);
         }
 
-        $(`#${$this.data('comment-id')}`).remove();
-        if ($conversationHolder.length && !$conversationHolder.find('.comment').length) {
-          const path = $conversationHolder.data('path');
-          const side = $conversationHolder.data('side');
-          const idx = $conversationHolder.data('idx');
-          const lineType = $conversationHolder.closest('tr').data('line-type');
+        document.getElementById(deleteButton.getAttribute('data-comment-id'))?.remove();
+
+        if (conversationHolder && !conversationHolder.querySelector('.comment')) {
+          const path = conversationHolder.getAttribute('data-path');
+          const side = conversationHolder.getAttribute('data-side');
+          const idx = conversationHolder.getAttribute('data-idx');
+          const lineType = conversationHolder.closest('tr').getAttribute('data-line-type');
+
           if (lineType === 'same') {
-            $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).removeClass('tw-invisible');
+            document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible');
           } else {
-            $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).removeClass('tw-invisible');
+            document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible');
           }
-          $conversationHolder.remove();
+
+          conversationHolder.remove();
         }
+
         // Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
-        if (!$parentTimelineGroup.find('.timeline-item.comment').length && !$parentTimelineItem.find('.conversation-holder').length) {
-          const $timelineAvatar = $parentTimelineGroup.find('.timeline-avatar');
-          $timelineAvatar.removeClass('timeline-avatar-offset');
+        if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
+          const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
+          timelineAvatar?.classList.remove('timeline-avatar-offset');
         }
       } catch (error) {
         console.error(error);
       }
     }
-    return false;
   });
 }
 
@@ -222,32 +230,35 @@ export function initRepoIssueDependencyDelete() {
 
 export function initRepoIssueCodeCommentCancel() {
   // Cancel inline code comment
-  $(document).on('click', '.cancel-code-comment', (e) => {
-    const $form = $(e.currentTarget).closest('form');
-    if ($form.length > 0 && $form.hasClass('comment-form')) {
-      $form.addClass('tw-hidden');
-      showElem($form.closest('.comment-code-cloud').find('button.comment-form-reply'));
+  document.addEventListener('click', (e) => {
+    if (!e.target.matches('.cancel-code-comment')) return;
+
+    const form = e.target.closest('form');
+    if (form?.classList.contains('comment-form')) {
+      hideElem(form);
+      showElem(form.closest('.comment-code-cloud')?.querySelectorAll('button.comment-form-reply'));
     } else {
-      $form.closest('.comment-code-cloud').remove();
+      form.closest('.comment-code-cloud')?.remove();
     }
   });
 }
 
 export function initRepoPullRequestUpdate() {
   // Pull Request update button
-  const $pullUpdateButton = $('.update-button > button');
-  $pullUpdateButton.on('click', async function (e) {
+  const pullUpdateButton = document.querySelector('.update-button > button');
+  if (!pullUpdateButton) return;
+
+  pullUpdateButton.addEventListener('click', async function (e) {
     e.preventDefault();
-    const $this = $(this);
-    const redirect = $this.data('redirect');
-    $this.addClass('is-loading');
+    const redirect = this.getAttribute('data-redirect');
+    this.classList.add('is-loading');
     let response;
     try {
-      response = await POST($this.data('do'));
+      response = await POST(this.getAttribute('data-do'));
     } catch (error) {
       console.error(error);
     } finally {
-      $this.removeClass('is-loading');
+      this.classList.remove('is-loading');
     }
     let data;
     try {
@@ -266,10 +277,13 @@ export function initRepoPullRequestUpdate() {
 
   $('.update-button > .dropdown').dropdown({
     onChange(_text, _value, $choice) {
-      const $url = $choice.data('do');
-      if ($url) {
-        $pullUpdateButton.find('.button-text').text($choice.text());
-        $pullUpdateButton.data('do', $url);
+      const url = $choice[0].getAttribute('data-do');
+      if (url) {
+        const buttonText = pullUpdateButton.querySelector('.button-text');
+        if (buttonText) {
+          buttonText.textContent = $choice.text();
+        }
+        pullUpdateButton.setAttribute('data-do', url);
       }
     },
   });
@@ -367,10 +381,10 @@ export function initRepoIssueComments() {
 
   $('.re-request-review').on('click', async function (e) {
     e.preventDefault();
-    const url = $(this).data('update-url');
-    const issueId = $(this).data('issue-id');
-    const id = $(this).data('id');
-    const isChecked = $(this).hasClass('checked');
+    const url = this.getAttribute('data-update-url');
+    const issueId = this.getAttribute('data-issue-id');
+    const id = this.getAttribute('data-id');
+    const isChecked = this.classList.contains('checked');
 
     await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
     window.location.reload();
@@ -397,7 +411,7 @@ export function initRepoIssueComments() {
 export async function handleReply($el) {
   hideElem($el);
   const $form = $el.closest('.comment-code-cloud').find('.comment-form');
-  $form.removeClass('tw-hidden');
+  showElem($form);
 
   const $textarea = $form.find('textarea');
   let editor = getComboMarkdownEditor($textarea);
@@ -454,20 +468,20 @@ export function initRepoPullRequestReview() {
 
   $(document).on('click', '.show-outdated', function (e) {
     e.preventDefault();
-    const id = $(this).data('comment');
-    $(this).addClass('tw-hidden');
-    $(`#code-comments-${id}`).removeClass('tw-hidden');
-    $(`#code-preview-${id}`).removeClass('tw-hidden');
-    $(`#hide-outdated-${id}`).removeClass('tw-hidden');
+    const id = this.getAttribute('data-comment');
+    hideElem(this);
+    showElem(`#code-comments-${id}`);
+    showElem(`#code-preview-${id}`);
+    showElem(`#hide-outdated-${id}`);
   });
 
   $(document).on('click', '.hide-outdated', function (e) {
     e.preventDefault();
-    const id = $(this).data('comment');
-    $(this).addClass('tw-hidden');
-    $(`#code-comments-${id}`).addClass('tw-hidden');
-    $(`#code-preview-${id}`).addClass('tw-hidden');
-    $(`#show-outdated-${id}`).removeClass('tw-hidden');
+    const id = this.getAttribute('data-comment');
+    hideElem(this);
+    hideElem(`#code-comments-${id}`);
+    hideElem(`#code-preview-${id}`);
+    showElem(`#show-outdated-${id}`);
   });
 
   $(document).on('click', 'button.comment-form-reply', async function (e) {
@@ -504,18 +518,19 @@ export function initRepoPullRequestReview() {
   }
 
   $(document).on('click', '.add-code-comment', async function (e) {
-    if ($(e.target).hasClass('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
+    if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
     e.preventDefault();
 
-    const isSplit = $(this).closest('.code-diff').hasClass('code-diff-split');
-    const side = $(this).data('side');
-    const idx = $(this).data('idx');
-    const path = $(this).closest('[data-path]').data('path');
-    const $tr = $(this).closest('tr');
-    const lineType = $tr.data('line-type');
+    const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
+    const side = this.getAttribute('data-side');
+    const idx = this.getAttribute('data-idx');
+    const path = this.closest('[data-path]')?.getAttribute('data-path');
+    const tr = this.closest('tr');
+    const lineType = tr.getAttribute('data-line-type');
 
-    let $ntr = $tr.next();
-    if (!$ntr.hasClass('add-comment')) {
+    const ntr = tr.nextElementSibling;
+    let $ntr = $(ntr);
+    if (!ntr?.classList.contains('add-comment')) {
       $ntr = $(`
         <tr class="add-comment" data-line-type="${lineType}">
           ${isSplit ? `
@@ -525,7 +540,7 @@ export function initRepoPullRequestReview() {
             <td class="add-comment-left add-comment-right" colspan="5"></td>
           `}
         </tr>`);
-      $tr.after($ntr);
+      $(tr).after($ntr);
     }
 
     const $td = $ntr.find(`.add-comment-${side}`);
@@ -611,13 +626,13 @@ export function initRepoIssueTitleEdit() {
 
   const editTitleToggle = function () {
     toggleElem($issueTitle);
-    toggleElem($('.not-in-edit'));
-    toggleElem($('#edit-title-input'));
-    toggleElem($('#pull-desc'));
-    toggleElem($('#pull-desc-edit'));
-    toggleElem($('.in-edit'));
-    toggleElem($('.new-issue-button'));
-    $('#issue-title-wrapper').toggleClass('edit-active');
+    toggleElem('.not-in-edit');
+    toggleElem('#edit-title-input');
+    toggleElem('#pull-desc');
+    toggleElem('#pull-desc-edit');
+    toggleElem('.in-edit');
+    toggleElem('.new-issue-button');
+    document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
     $editInput[0].focus();
     $editInput[0].select();
     return false;

From 8da9130c1ffe93e0e97290fddb908ae5b67432e2 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 31 Mar 2024 16:58:55 +0200
Subject: [PATCH 600/679] Prevent flash of dropdown menu on labels list
 (#30215)

On the labels list, This `left` class caused the dropdown content to
flash on page load until JS had hidden it. Remove it as I see no purpose
to it.

<img width="215" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/9e1de97f-dd89-41e0-9229-5c4a786ba762">

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/issue/labels/label_list.tmpl | 2 +-
 web_src/css/modules/header.css              | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index d84f14242a..8d7fc2c3db 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -8,7 +8,7 @@
 					{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 				</span>
 				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-				<div class="left menu">
+				<div class="menu">
 					<a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
 					<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
 					<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css
index 091d536cfc..05381e1185 100644
--- a/web_src/css/modules/header.css
+++ b/web_src/css/modules/header.css
@@ -135,6 +135,12 @@ h4.ui.header .sub.header {
   font-weight: var(--font-weight-normal);
 }
 
+/* open dropdown menus to the left in right-attached headers */
+.ui.attached.header > .ui.right .ui.dropdown .menu {
+  right: 0;
+  left: auto;
+}
+
 /* if a .top.attached.header is followed by a .segment, add some margin */
 .ui.segments + .ui.top.attached.header,
 .ui.attached.segment + .ui.top.attached.header {

From 0497b2607d1052e771af4017c2c4180adb7d86b2 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Sun, 31 Mar 2024 18:39:50 +0300
Subject: [PATCH 601/679] Remove most jQuery function calls from the repository
 topic box (#30191)

Remove most jQuery function calls

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/home.tmpl         | 23 ++++-----
 web_src/css/repo.css             |  1 +
 web_src/js/features/repo-home.js | 86 ++++++++++----------------------
 web_src/js/modules/toast.js      |  1 +
 web_src/js/utils/dom.js          | 18 ++++++-
 5 files changed, 54 insertions(+), 75 deletions(-)

diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 4241f77ead..ab37f7e318 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -18,22 +18,21 @@
 				</div>
 			</form>
 		</div>
-		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics">
-			{{range .Topics}}<a class="ui repo-topic large label topic tw-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
+		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
+			{{/* it should match the code in issue-home.js */}}
+			{{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
 			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
 		</div>
 		{{end}}
 		{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-		<div class="ui form tw-hidden tw-flex tw-flex-col tw-mt-4" id="topic_edit">
-			<div class="field tw-flex-1 tw-mb-1">
-				<div class="ui fluid multiple search selection dropdown tw-flex-wrap" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
-					<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
-					{{range .Topics}}
-						{{/* keey the same layout as Fomantic UI generated labels */}}
-						<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
-					{{end}}
-					<div class="text"></div>
-				</div>
+		<div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit">
+			<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
+				<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
+				{{range .Topics}}
+					{{/* keep the same layout as Fomantic UI generated labels */}}
+					<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
+				{{end}}
+				<div class="text"></div>
 			</div>
 			<div>
 				<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index eab90c10d3..705d652b54 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2437,6 +2437,7 @@ tbody.commit-list {
 #repo-topics .repo-topic {
   font-weight: var(--font-weight-normal);
   cursor: pointer;
+  margin: 0;
 }
 
 #new-dependency-drop-list.ui.selection.dropdown {
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index e195c23c37..6a5bce8268 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -1,55 +1,53 @@
 import $ from 'jquery';
 import {stripTags} from '../utils.js';
-import {hideElem, showElem} from '../utils/dom.js';
+import {hideElem, queryElemChildren, showElem} from '../utils/dom.js';
 import {POST} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
 
 const {appSubUrl} = window.config;
 
 export function initRepoTopicBar() {
   const mgrBtn = document.getElementById('manage_topic');
   if (!mgrBtn) return;
+
   const editDiv = document.getElementById('topic_edit');
   const viewDiv = document.getElementById('repo-topics');
-  const saveBtn = document.getElementById('save_topic');
-  const topicDropdown = editDiv.querySelector('.dropdown');
-  const $topicDropdown = $(topicDropdown);
-  const $topicForm = $(editDiv);
-  const $topicDropdownSearch = $topicDropdown.find('input.search');
-  const topicPrompts = {
-    countPrompt: topicDropdown.getAttribute('data-text-count-prompt') ?? undefined,
-    formatPrompt: topicDropdown.getAttribute('data-text-format-prompt') ?? undefined,
-  };
+  const topicDropdown = editDiv.querySelector('.ui.dropdown');
+  let lastErrorToast;
 
   mgrBtn.addEventListener('click', () => {
     hideElem(viewDiv);
     showElem(editDiv);
-    $topicDropdownSearch.trigger('focus');
+    topicDropdown.querySelector('input.search').focus();
   });
 
-  $('#cancel_topic_edit').on('click', () => {
+  document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
+    lastErrorToast?.hideToast();
     hideElem(editDiv);
     showElem(viewDiv);
     mgrBtn.focus();
   });
 
-  saveBtn.addEventListener('click', async () => {
-    const topics = $('input[name=topics]').val();
+  document.getElementById('save_topic').addEventListener('click', async (e) => {
+    lastErrorToast?.hideToast();
+    const topics = editDiv.querySelector('input[name=topics]').value;
 
     const data = new FormData();
     data.append('topics', topics);
 
-    const response = await POST(saveBtn.getAttribute('data-link'), {data});
+    const response = await POST(e.target.getAttribute('data-link'), {data});
 
     if (response.ok) {
       const responseData = await response.json();
       if (responseData.status === 'ok') {
-        $(viewDiv).children('.topic').remove();
+        queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove());
         if (topics.length) {
           const topicArray = topics.split(',');
           topicArray.sort();
           for (const topic of topicArray) {
+            // it should match the code in repo/home.tmpl
             const link = document.createElement('a');
-            link.classList.add('ui', 'repo-topic', 'large', 'label', 'topic', 'tw-m-0');
+            link.classList.add('repo-topic', 'ui', 'large', 'label');
             link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
             link.textContent = topic;
             mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button
@@ -59,27 +57,23 @@ export function initRepoTopicBar() {
         showElem(viewDiv);
       }
     } else if (response.status === 422) {
+      // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save"
       const responseData = await response.json();
+      lastErrorToast = showErrorToast(responseData.message, {duration: 5000});
       if (responseData.invalidTopics.length > 0) {
-        topicPrompts.formatPrompt = responseData.message;
-
         const {invalidTopics} = responseData;
-        const $topicLabels = $topicDropdown.children('a.ui.label');
+        const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label');
         for (const [index, value] of topics.split(',').entries()) {
           if (invalidTopics.includes(value)) {
-            $topicLabels.eq(index).removeClass('green').addClass('red');
+            topicLabels[index].classList.remove('green');
+            topicLabels[index].classList.add('red');
           }
         }
-      } else {
-        topicPrompts.countPrompt = responseData.message;
       }
     }
-
-    // Always validate the form
-    $topicForm.form('validate form');
   });
 
-  $topicDropdown.dropdown({
+  $(topicDropdown).dropdown({
     allowAdditions: true,
     forceSelection: false,
     fullTextSearch: 'exact',
@@ -102,9 +96,9 @@ export function initRepoTopicBar() {
         const query = stripTags(this.urlData.query.trim());
         let found_query = false;
         const current_topics = [];
-        $topicDropdown.find('a.label.visible').each((_, el) => {
+        for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) {
           current_topics.push(el.getAttribute('data-value'));
-        });
+        }
 
         if (res.topics) {
           let found = false;
@@ -146,38 +140,8 @@ export function initRepoTopicBar() {
     },
     onAdd(addedValue, _addedText, $addedChoice) {
       addedValue = addedValue.toLowerCase().trim();
-      $($addedChoice)[0].setAttribute('data-value', addedValue);
-      $($addedChoice)[0].setAttribute('data-text', addedValue);
-    },
-  });
-
-  $.fn.form.settings.rules.validateTopic = function (_values, regExp) {
-    const $topics = $topicDropdown.children('a.ui.label');
-    const status = !$topics.length || $topics.last()[0].getAttribute('data-value').match(regExp);
-    if (!status) {
-      $topics.last().removeClass('green').addClass('red');
-    }
-    return status && !$topicDropdown.children('a.ui.label.red').length;
-  };
-
-  $topicForm.form({
-    on: 'change',
-    inline: true,
-    fields: {
-      topics: {
-        identifier: 'topics',
-        rules: [
-          {
-            type: 'validateTopic',
-            value: /^\s*[a-z0-9][-.a-z0-9]{0,35}\s*$/,
-            prompt: topicPrompts.formatPrompt,
-          },
-          {
-            type: 'maxCount[25]',
-            prompt: topicPrompts.countPrompt,
-          },
-        ],
-      },
+      $addedChoice[0].setAttribute('data-value', addedValue);
+      $addedChoice[0].setAttribute('data-text', addedValue);
     },
   });
 }
diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js
index d64359799c..d12d203718 100644
--- a/web_src/js/modules/toast.js
+++ b/web_src/js/modules/toast.js
@@ -39,6 +39,7 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, ..
 
   toast.showToast();
   toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
+  return toast;
 }
 
 export function showInfoToast(message, opts) {
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index fffe9c6109..fb23a71725 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -51,8 +51,22 @@ export function isElemHidden(el) {
   return res[0];
 }
 
-export function queryElemSiblings(el, selector = '*') {
-  return Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector));
+function applyElemsCallback(elems, fn) {
+  if (fn) {
+    for (const el of elems) {
+      fn(el);
+    }
+  }
+  return elems;
+}
+
+export function queryElemSiblings(el, selector = '*', fn) {
+  return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn);
+}
+
+// it works like jQuery.children: only the direct children are selected
+export function queryElemChildren(parent, selector = '*', fn) {
+  return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
 }
 
 export function onDomReady(cb) {

From ff334749f58c71980ec19143bc21c0a799074b30 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 31 Mar 2024 18:06:06 +0200
Subject: [PATCH 602/679] Remove fomantic input module (#30194)

Another pure CSS module. Some styling is part of the `form` module which
will likely follow next.
---
 templates/devtest/gitea-ui.tmpl               |   2 +-
 .../repo/issue/view_content/sidebar.tmpl      |   2 +-
 web_src/css/base.css                          |  57 --
 web_src/css/index.css                         |   1 +
 web_src/css/modules/animations.css            |   8 +-
 web_src/css/modules/input.css                 | 192 +++++
 web_src/fomantic/build/semantic.css           | 744 ------------------
 web_src/fomantic/semantic.json                |   1 -
 web_src/js/components/DashboardRepoList.vue   |   4 +-
 web_src/js/features/common-global.js          |   4 +-
 web_src/js/features/copycontent.js            |   4 +-
 11 files changed, 207 insertions(+), 812 deletions(-)
 create mode 100644 web_src/css/modules/input.css

diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 76de4a93d7..bb4fc77a74 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -102,7 +102,7 @@
 
 	<div>
 		<h1>Loading</h1>
-		<div class="is-loading small-loading-icon tw-border tw-border-secondary tw-py-1"><span>loading ...</span></div>
+		<div class="is-loading loading-icon-2px tw-border tw-border-secondary tw-py-1"><span>loading ...</span></div>
 		<div class="is-loading tw-border tw-border-secondary tw-py-4">
 			<p>loading ...</p>
 			<p>loading ...</p>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index c917c78e68..7040c2849a 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -677,7 +677,7 @@
 		{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
 			<div class="divider"></div>
 			<div class="inline field">
-				<div class="ui checkbox small-loading-icon" id="allow-edits-from-maintainers"
+				<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
 						data-url="{{.Issue.Link}}"
 						data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
 						data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
diff --git a/web_src/css/base.css b/web_src/css/base.css
index cd0f883138..96c90ee692 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -305,12 +305,6 @@ a.label,
   background-color: var(--color-label-bg);
 }
 
-/* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */
-.ui.input > input {
-  line-height: var(--line-height-default);
-  text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */
-}
-
 /* fix Fomantic's line-height causing vertical scrollbars to appear */
 ul.ui.list li,
 ol.ui.list li,
@@ -319,47 +313,6 @@ ol.ui.list li,
   line-height: var(--line-height-default);
 }
 
-.ui.input.focus > input,
-.ui.input > input:focus {
-  border-color: var(--color-primary);
-}
-
-.ui.action.input .ui.ui.button {
-  border-color: var(--color-input-border);
-  padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */
-  padding-bottom: 0;
-}
-
-/* currently used for search bar dropdowns in repo search and explore code */
-.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
-  min-width: 10em;
-}
-.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) {
-  border-right: none;
-}
-.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover {
-  border-color: var(--color-input-border);
-}
-.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible {
-  border-bottom-left-radius: 0 !important;
-  border-bottom-right-radius: 0 !important;
-}
-.ui.action.input:not([class*="left action"]) > input,
-.ui.action.input:not([class*="left action"]) > input:hover {
-  border-right: none;
-}
-.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
-.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
-.ui.action.input:not([class*="left action"]) > input:focus + .button,
-.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
-.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
-.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
-  border-left-color: var(--color-primary);
-}
-.ui.action.input:not([class*="left action"]) > input:focus {
-  border-right-color: var(--color-primary);
-}
-
 .ui.menu {
   display: flex;
 }
@@ -1599,16 +1552,6 @@ table th[data-sortt-desc] .svg {
   align-items: stretch;
 }
 
-.ui.ui.icon.input .icon {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.ui.icon.input > i.icon {
-  transition: none;
-}
-
 .flex-items-block > .item,
 .flex-text-block {
   display: flex;
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 373a84cf6a..40b1d3c881 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -6,6 +6,7 @@
 @import "./modules/container.css";
 @import "./modules/divider.css";
 @import "./modules/header.css";
+@import "./modules/input.css";
 @import "./modules/label.css";
 @import "./modules/segment.css";
 @import "./modules/grid.css";
diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index 0f78ad25cb..361618c449 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -34,10 +34,14 @@
   border-radius: var(--border-radius-circle);
 }
 
-.is-loading.small-loading-icon::after {
+.is-loading.loading-icon-2px::after {
   border-width: 2px;
 }
 
+.is-loading.loading-icon-3px::after {
+  border-width: 3px;
+}
+
 /* for single form button, the loading state should be on the button, but not go semi-transparent, just replace the text on the button with the loader. */
 form.single-button-form.is-loading > * {
   opacity: 1;
@@ -62,7 +66,7 @@ form.single-button-form.is-loading .button {
   background: transparent;
 }
 
-/* TODO: not needed, use "is-loading small-loading-icon" instead */
+/* TODO: not needed, use "is-loading loading-icon-2px" instead */
 code.language-math.is-loading::after {
   padding: 0;
   border-width: 2px;
diff --git a/web_src/css/modules/input.css b/web_src/css/modules/input.css
new file mode 100644
index 0000000000..48cd2fa9ff
--- /dev/null
+++ b/web_src/css/modules/input.css
@@ -0,0 +1,192 @@
+/* based on Fomantic UI input module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.input {
+  position: relative;
+  font-weight: var(--font-weight-normal);
+  display: inline-flex;
+  color: var(--color-input-text);
+}
+.ui.input > input {
+  margin: 0;
+  max-width: 100%;
+  flex: 1 0 auto;
+  outline: none;
+  font-family: var(--fonts-regular);
+  padding: 0.67857143em 1em;
+  border: 1px solid var(--color-input-border);
+  color: var(--color-input-text);
+  border-radius: 0.28571429rem;
+  line-height: var(--line-height-default);
+  text-align: start;
+}
+
+.ui.disabled.input,
+.ui.input:not(.disabled) input[disabled] {
+  opacity: var(--opacity-disabled);
+}
+.ui.disabled.input > input,
+.ui.input:not(.disabled) input[disabled] {
+  pointer-events: none;
+}
+
+.ui.input.focus > input,
+.ui.input > input:focus {
+  border-color: var(--color-primary);
+}
+
+.ui.input.error > input {
+  background: var(--color-error-bg);
+  border-color: var(--color-error-border);
+  color: var(--color-error-text);
+}
+
+.ui.icon.input > i.icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: default;
+  position: absolute;
+  text-align: center;
+  top: 50%;
+  transform: translateY(-50%);
+  opacity: 0.5;
+  border-radius: 0 0.28571429rem 0.28571429rem 0;
+  pointer-events: none;
+  padding: 4px;
+}
+
+.ui.icon.input > i.icon.is-loading {
+  position: absolute !important;
+}
+
+.ui.icon.input > i.icon.is-loading > * {
+  visibility: hidden;
+}
+
+.ui.ui.ui.ui.icon.input > textarea,
+.ui.ui.ui.ui.icon.input > input {
+  padding-right: 2.67142857em;
+}
+.ui.icon.input > i.link.icon {
+  cursor: pointer;
+}
+.ui.icon.input > i.circular.icon {
+  top: 0.35em;
+  right: 0.5em;
+}
+
+.ui[class*="left icon"].input > i.icon {
+  right: auto;
+  left: 8px;
+  border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui[class*="left icon"].input > i.circular.icon {
+  right: auto;
+  left: 0.5em;
+}
+.ui.ui.ui.ui[class*="left icon"].input > textarea,
+.ui.ui.ui.ui[class*="left icon"].input > input {
+  padding-left: 2.67142857em;
+  padding-right: 1em;
+}
+
+.ui.icon.input > textarea:focus ~ .icon,
+.ui.icon.input > input:focus ~ .icon {
+  opacity: 1;
+}
+
+.ui.icon.input > textarea ~ i.icon {
+  height: 3em;
+}
+
+.ui.form .field.error > .ui.action.input > .ui.button,
+.ui.action.input.error > .ui.button {
+  border-top: 1px solid var(--color-error-border);
+  border-bottom: 1px solid var(--color-error-border);
+}
+
+.ui.action.input > .button,
+.ui.action.input > .buttons {
+  display: flex;
+  align-items: center;
+  flex: 0 0 auto;
+}
+.ui.action.input > .button,
+.ui.action.input > .buttons > .button {
+  padding-top: 0.78571429em;
+  padding-bottom: 0.78571429em;
+  margin: 0;
+}
+
+.ui.action.input:not([class*="left action"]) > input {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+  border-right-color: transparent;
+}
+
+.ui.action.input > .dropdown:first-child,
+.ui.action.input > .button:first-child,
+.ui.action.input > .buttons:first-child > .button {
+  border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui.action.input > .dropdown:not(:first-child),
+.ui.action.input > .button:not(:first-child),
+.ui.action.input > .buttons:not(:first-child) > .button {
+  border-radius: 0;
+}
+.ui.action.input > .dropdown:last-child,
+.ui.action.input > .button:last-child,
+.ui.action.input > .buttons:last-child > .button {
+  border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.fluid.input {
+  display: flex;
+}
+.ui.fluid.input > input {
+  width: 0 !important;
+}
+
+.ui.tiny.input {
+  font-size: 0.85714286em;
+}
+.ui.small.input {
+  font-size: 0.92857143em;
+}
+
+.ui.action.input .ui.ui.button {
+  border-color: var(--color-input-border);
+  padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */
+  padding-bottom: 0;
+}
+
+/* currently used for search bar dropdowns in repo search and explore code */
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
+  min-width: 10em;
+}
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) {
+  border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover {
+  border-color: var(--color-input-border);
+}
+.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible {
+  border-bottom-left-radius: 0 !important;
+  border-bottom-right-radius: 0 !important;
+}
+.ui.action.input:not([class*="left action"]) > input,
+.ui.action.input:not([class*="left action"]) > input:hover {
+  border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
+  border-left-color: var(--color-primary);
+}
+.ui.action.input:not([class*="left action"]) > input:focus {
+  border-right-color: var(--color-primary);
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 525a3af8c6..5cb6a371e5 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -6474,750 +6474,6 @@ select.ui.dropdown {
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Input
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-           Standard
-*******************************/
-
-/*--------------------
-        Inputs
----------------------*/
-
-.ui.input {
-  position: relative;
-  font-weight: normal;
-  font-style: normal;
-  display: inline-flex;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.input > input {
-  margin: 0;
-  max-width: 100%;
-  flex: 1 0 auto;
-  outline: none;
-  -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
-  text-align: left;
-  line-height: 1.21428571em;
-  font-family: var(--fonts-regular);
-  padding: 0.67857143em 1em;
-  background: #FFFFFF;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  color: rgba(0, 0, 0, 0.87);
-  border-radius: 0.28571429rem;
-  transition: box-shadow 0.1s ease, border-color 0.1s ease;
-  box-shadow: none;
-}
-
-/*--------------------
-      Placeholder
----------------------*/
-
-/* browsers require these rules separate */
-
-.ui.input > input::-webkit-input-placeholder {
-  color: rgba(191, 191, 191, 0.87);
-}
-
-.ui.input > input::-moz-placeholder {
-  color: rgba(191, 191, 191, 0.87);
-}
-
-.ui.input > input:-ms-input-placeholder {
-  color: rgba(191, 191, 191, 0.87);
-}
-
-/*******************************
-            States
-*******************************/
-
-/*--------------------
-          Disabled
-  ---------------------*/
-
-.ui.disabled.input,
-.ui.input:not(.disabled) input[disabled] {
-  opacity: var(--opacity-disabled);
-}
-
-.ui.disabled.input > input,
-.ui.input:not(.disabled) input[disabled] {
-  pointer-events: none;
-}
-
-/*--------------------
-        Active
----------------------*/
-
-.ui.input > input:active,
-.ui.input.down input {
-  border-color: rgba(0, 0, 0, 0.3);
-  background: #FAFAFA;
-  color: rgba(0, 0, 0, 0.87);
-  box-shadow: none;
-}
-
-/*--------------------
-         Loading
-  ---------------------*/
-
-.ui.loading.loading.input > i.icon:before {
-  position: absolute;
-  content: '';
-  top: 50%;
-  left: 50%;
-  margin: -0.64285714em 0 0 -0.64285714em;
-  width: 1.28571429em;
-  height: 1.28571429em;
-  border-radius: 500rem;
-  border: 0.2em solid rgba(0, 0, 0, 0.1);
-}
-
-.ui.loading.loading.input > i.icon:after {
-  position: absolute;
-  content: '';
-  top: 50%;
-  left: 50%;
-  margin: -0.64285714em 0 0 -0.64285714em;
-  width: 1.28571429em;
-  height: 1.28571429em;
-  animation: loader 0.6s infinite linear;
-  border: 0.2em solid #767676;
-  border-radius: 500rem;
-  box-shadow: 0 0 0 1px transparent;
-}
-
-/*--------------------
-        Focus
----------------------*/
-
-.ui.input.focus > input,
-.ui.input > input:focus {
-  border-color: #85B7D9;
-  background: #FFFFFF;
-  color: rgba(0, 0, 0, 0.8);
-  box-shadow: none;
-}
-
-.ui.input.focus > input::-webkit-input-placeholder,
-.ui.input > input:focus::-webkit-input-placeholder {
-  color: rgba(115, 115, 115, 0.87);
-}
-
-.ui.input.focus > input::-moz-placeholder,
-.ui.input > input:focus::-moz-placeholder {
-  color: rgba(115, 115, 115, 0.87);
-}
-
-.ui.input.focus > input:-ms-input-placeholder,
-.ui.input > input:focus:-ms-input-placeholder {
-  color: rgba(115, 115, 115, 0.87);
-}
-
-/*--------------------
-          States
-  ---------------------*/
-
-.ui.input.error > input {
-  background-color: #FFF6F6;
-  border-color: #E0B4B4;
-  color: #9F3A38;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.error > input::-webkit-input-placeholder {
-  color: #e7bdbc;
-}
-
-.ui.input.error > input::-moz-placeholder {
-  color: #e7bdbc;
-}
-
-.ui.input.error > input:-ms-input-placeholder {
-  color: #e7bdbc !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.error > input:focus::-webkit-input-placeholder {
-  color: #da9796;
-}
-
-.ui.input.error > input:focus::-moz-placeholder {
-  color: #da9796;
-}
-
-.ui.input.error > input:focus:-ms-input-placeholder {
-  color: #da9796 !important;
-}
-
-.ui.input.info > input {
-  background-color: #F8FFFF;
-  border-color: #A9D5DE;
-  color: #276F86;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.info > input::-webkit-input-placeholder {
-  color: #98cfe1;
-}
-
-.ui.input.info > input::-moz-placeholder {
-  color: #98cfe1;
-}
-
-.ui.input.info > input:-ms-input-placeholder {
-  color: #98cfe1 !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.info > input:focus::-webkit-input-placeholder {
-  color: #70bdd6;
-}
-
-.ui.input.info > input:focus::-moz-placeholder {
-  color: #70bdd6;
-}
-
-.ui.input.info > input:focus:-ms-input-placeholder {
-  color: #70bdd6 !important;
-}
-
-.ui.input.success > input {
-  background-color: #FCFFF5;
-  border-color: #A3C293;
-  color: #2C662D;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.success > input::-webkit-input-placeholder {
-  color: #8fcf90;
-}
-
-.ui.input.success > input::-moz-placeholder {
-  color: #8fcf90;
-}
-
-.ui.input.success > input:-ms-input-placeholder {
-  color: #8fcf90 !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.success > input:focus::-webkit-input-placeholder {
-  color: #6cbf6d;
-}
-
-.ui.input.success > input:focus::-moz-placeholder {
-  color: #6cbf6d;
-}
-
-.ui.input.success > input:focus:-ms-input-placeholder {
-  color: #6cbf6d !important;
-}
-
-.ui.input.warning > input {
-  background-color: #FFFAF3;
-  border-color: #C9BA9B;
-  color: #573A08;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.warning > input::-webkit-input-placeholder {
-  color: #edad3e;
-}
-
-.ui.input.warning > input::-moz-placeholder {
-  color: #edad3e;
-}
-
-.ui.input.warning > input:-ms-input-placeholder {
-  color: #edad3e !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.warning > input:focus::-webkit-input-placeholder {
-  color: #e39715;
-}
-
-.ui.input.warning > input:focus::-moz-placeholder {
-  color: #e39715;
-}
-
-.ui.input.warning > input:focus:-ms-input-placeholder {
-  color: #e39715 !important;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*--------------------
-        Transparent
-  ---------------------*/
-
-.ui.transparent.input > textarea,
-.ui.transparent.input > input {
-  border-color: transparent !important;
-  background-color: transparent !important;
-  padding: 0;
-  box-shadow: none !important;
-  border-radius: 0 !important;
-}
-
-.field .ui.transparent.input > textarea {
-  padding: 0.67857143em 1em;
-}
-
-/* Transparent Icon */
-
-:not(.field) > .ui.transparent.icon.input > i.icon {
-  width: 1.1em;
-}
-
-:not(.field) > .ui.ui.ui.transparent.icon.input > input {
-  padding-left: 0;
-  padding-right: 2em;
-}
-
-:not(.field) > .ui.ui.ui.transparent[class*="left icon"].input > input {
-  padding-left: 2em;
-  padding-right: 0;
-}
-
-/*--------------------
-           Icon
-  ---------------------*/
-
-.ui.icon.input > i.icon {
-  cursor: default;
-  position: absolute;
-  line-height: 1;
-  text-align: center;
-  top: 0;
-  right: 0;
-  margin: 0;
-  height: 100%;
-  width: 2.67142857em;
-  opacity: 0.5;
-  border-radius: 0 0.28571429rem 0.28571429rem 0;
-  transition: opacity 0.3s ease;
-}
-
-.ui.icon.input > i.icon:not(.link) {
-  pointer-events: none;
-}
-
-.ui.ui.ui.ui.icon.input > textarea,
-.ui.ui.ui.ui.icon.input > input {
-  padding-right: 2.67142857em;
-}
-
-.ui.icon.input > i.icon:before,
-.ui.icon.input > i.icon:after {
-  left: 0;
-  position: absolute;
-  text-align: center;
-  top: 50%;
-  width: 100%;
-  margin-top: -0.5em;
-}
-
-.ui.icon.input > i.link.icon {
-  cursor: pointer;
-}
-
-.ui.icon.input > i.circular.icon {
-  top: 0.35em;
-  right: 0.5em;
-}
-
-/* Left Icon Input */
-
-.ui[class*="left icon"].input > i.icon {
-  right: auto;
-  left: 1px;
-  border-radius: 0.28571429rem 0 0 0.28571429rem;
-}
-
-.ui[class*="left icon"].input > i.circular.icon {
-  right: auto;
-  left: 0.5em;
-}
-
-.ui.ui.ui.ui[class*="left icon"].input > textarea,
-.ui.ui.ui.ui[class*="left icon"].input > input {
-  padding-left: 2.67142857em;
-  padding-right: 1em;
-}
-
-/* Focus */
-
-.ui.icon.input > textarea:focus ~ i.icon,
-.ui.icon.input > input:focus ~ i.icon {
-  opacity: 1;
-}
-
-/*--------------------
-          Labeled
-  ---------------------*/
-
-/* Adjacent Label */
-
-.ui.labeled.input > .label {
-  flex: 0 0 auto;
-  margin: 0;
-  font-size: 1em;
-}
-
-.ui.labeled.input > .label:not(.corner) {
-  padding-top: 0.78571429em;
-  padding-bottom: 0.78571429em;
-}
-
-/* Regular Label on Left */
-
-.ui.labeled.input:not([class*="corner labeled"]) .label:first-child {
-  border-top-right-radius: 0;
-  border-bottom-right-radius: 0;
-}
-
-.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-  border-left-color: transparent;
-}
-
-.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input:focus {
-  border-left-color: #85B7D9;
-}
-
-/* Regular Label on Right */
-
-.ui[class*="right labeled"].input > input {
-  border-top-right-radius: 0 !important;
-  border-bottom-right-radius: 0 !important;
-  border-right-color: transparent !important;
-}
-
-.ui[class*="right labeled"].input > input + .label {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-}
-
-.ui[class*="right labeled"].input > input:focus {
-  border-right-color: #85B7D9 !important;
-}
-
-/* Corner Label */
-
-.ui.labeled.input .corner.label {
-  top: 1px;
-  right: 1px;
-  font-size: 0.64285714em;
-  border-radius: 0 0.28571429rem 0 0;
-}
-
-/* Spacing with corner label */
-
-.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > textarea,
-.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > input {
-  padding-right: 2.5em !important;
-}
-
-.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > textarea,
-.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > input {
-  padding-right: 3.25em !important;
-}
-
-.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > i.icon {
-  margin-right: 1.25em;
-}
-
-/* Left Labeled */
-
-.ui[class*="left corner labeled"].labeled.input > textarea,
-.ui[class*="left corner labeled"].labeled.input > input {
-  padding-left: 2.5em !important;
-}
-
-.ui[class*="left corner labeled"].icon.input > textarea,
-.ui[class*="left corner labeled"].icon.input > input {
-  padding-left: 3.25em !important;
-}
-
-.ui[class*="left corner labeled"].icon.input > i.icon {
-  margin-left: 1.25em;
-}
-
-.ui.icon.input > textarea ~ i.icon {
-  height: 3em;
-}
-
-:not(.field) > .ui.transparent.icon.input > textarea ~ i.icon {
-  height: 1.3em;
-}
-
-/* Corner Label Position  */
-
-.ui.input > .ui.corner.label {
-  top: 1px;
-  right: 1px;
-}
-
-.ui.input > .ui.left.corner.label {
-  right: auto;
-  left: 1px;
-}
-
-/* Labeled and action input states */
-
-.ui.form .field.error > .ui.action.input > .ui.button,
-.ui.form .field.error > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.error > .ui.button,
-.ui.labeled.input.error:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #E0B4B4;
-  border-bottom: 1px solid #E0B4B4;
-}
-
-.ui.form .field.error > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.error > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.error > .ui.button,
-.ui.labeled.input.error:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #E0B4B4;
-}
-
-.ui.form .field.error > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.error:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.error:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #E0B4B4;
-}
-
-.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.error:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #E0B4B4;
-}
-
-.ui.form .field.info > .ui.action.input > .ui.button,
-.ui.form .field.info > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.info > .ui.button,
-.ui.labeled.input.info:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #A9D5DE;
-  border-bottom: 1px solid #A9D5DE;
-}
-
-.ui.form .field.info > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.info > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.info > .ui.button,
-.ui.labeled.input.info:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #A9D5DE;
-}
-
-.ui.form .field.info > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.info:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.info:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #A9D5DE;
-}
-
-.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.info:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #A9D5DE;
-}
-
-.ui.form .field.success > .ui.action.input > .ui.button,
-.ui.form .field.success > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.success > .ui.button,
-.ui.labeled.input.success:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #A3C293;
-  border-bottom: 1px solid #A3C293;
-}
-
-.ui.form .field.success > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.success > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.success > .ui.button,
-.ui.labeled.input.success:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #A3C293;
-}
-
-.ui.form .field.success > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.success:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.success:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #A3C293;
-}
-
-.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.success:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #A3C293;
-}
-
-.ui.form .field.warning > .ui.action.input > .ui.button,
-.ui.form .field.warning > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.warning > .ui.button,
-.ui.labeled.input.warning:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #C9BA9B;
-  border-bottom: 1px solid #C9BA9B;
-}
-
-.ui.form .field.warning > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.warning > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.warning > .ui.button,
-.ui.labeled.input.warning:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #C9BA9B;
-}
-
-.ui.form .field.warning > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.warning:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.warning:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #C9BA9B;
-}
-
-.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.warning:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #C9BA9B;
-}
-
-/*--------------------
-          Action
-  ---------------------*/
-
-.ui.action.input > .button,
-.ui.action.input > .buttons {
-  display: flex;
-  align-items: center;
-  flex: 0 0 auto;
-}
-
-.ui.action.input > .button,
-.ui.action.input > .buttons > .button {
-  padding-top: 0.78571429em;
-  padding-bottom: 0.78571429em;
-  margin: 0;
-}
-
-/* Input when ui Left*/
-
-.ui[class*="left action"].input > input {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-  border-left-color: transparent;
-}
-
-/* Input when ui Right*/
-
-.ui.action.input:not([class*="left action"]) > input {
-  border-top-right-radius: 0;
-  border-bottom-right-radius: 0;
-  border-right-color: transparent;
-}
-
-/* Button and Dropdown */
-
-.ui.action.input > .dropdown:first-child,
-.ui.action.input > .button:first-child,
-.ui.action.input > .buttons:first-child > .button {
-  border-radius: 0.28571429rem 0 0 0.28571429rem;
-}
-
-.ui.action.input > .dropdown:not(:first-child),
-.ui.action.input > .button:not(:first-child),
-.ui.action.input > .buttons:not(:first-child) > .button {
-  border-radius: 0;
-}
-
-.ui.action.input > .dropdown:last-child,
-.ui.action.input > .button:last-child,
-.ui.action.input > .buttons:last-child > .button {
-  border-radius: 0 0.28571429rem 0.28571429rem 0;
-}
-
-/* Input Focus */
-
-.ui.action.input:not([class*="left action"]) > input:focus {
-  border-right-color: #85B7D9;
-}
-
-.ui.ui[class*="left action"].input > input:focus {
-  border-left-color: #85B7D9;
-}
-
-/*--------------------
-          Fluid
-  ---------------------*/
-
-.ui.fluid.input {
-  display: flex;
-}
-
-.ui.fluid.input > input {
-  width: 0 !important;
-}
-
-/*--------------------
-        Size
----------------------*/
-
-.ui.input {
-  font-size: 1em;
-}
-
-.ui.mini.input {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.input {
-  font-size: 0.85714286em;
-}
-
-.ui.small.input {
-  font-size: 0.92857143em;
-}
-
-.ui.large.input {
-  font-size: 1.14285714em;
-}
-
-.ui.big.input {
-  font-size: 1.28571429em;
-}
-
-.ui.huge.input {
-  font-size: 1.42857143em;
-}
-
-.ui.massive.input {
-  font-size: 1.71428571em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 151273f3ca..7ec520f315 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -26,7 +26,6 @@
     "dimmer",
     "dropdown",
     "form",
-    "input",
     "list",
     "menu",
     "modal",
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index ffdcef2bc8..2d980a1b18 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -355,9 +355,9 @@ export default sfc; // activate the IDE's Vue plugin
         </a>
       </h4>
       <div class="ui attached segment repos-search">
-        <div class="ui small fluid action left icon input" :class="{loading: isLoading}">
+        <div class="ui small fluid action left icon input">
           <input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
-          <i class="icon"><svg-icon name="octicon-search" :size="16"/></i>
+          <i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i>
           <div class="ui dropdown icon button" :title="textFilter">
             <svg-icon name="octicon-filter" :size="16"/>
             <div class="menu">
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 009dbd9421..e7db9b2336 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -109,7 +109,7 @@ async function fetchActionDoRequest(actionElem, url, opt) {
       showErrorToast(`${i18n.network_error} ${e}`);
     }
   }
-  actionElem.classList.remove('is-loading', 'small-loading-icon');
+  actionElem.classList.remove('is-loading', 'loading-icon-2px');
 }
 
 async function formFetchAction(e) {
@@ -121,7 +121,7 @@ async function formFetchAction(e) {
 
   formEl.classList.add('is-loading');
   if (formEl.clientHeight < 50) {
-    formEl.classList.add('small-loading-icon');
+    formEl.classList.add('loading-icon-2px');
   }
 
   const formMethod = formEl.getAttribute('method') || 'get';
diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js
index 3d3b2a697e..03efe00701 100644
--- a/web_src/js/features/copycontent.js
+++ b/web_src/js/features/copycontent.js
@@ -19,7 +19,7 @@ export function initCopyContent() {
     // the text to copy is not in the DOM or it is an image which should be
     // fetched to copy in full resolution
     if (link) {
-      btn.classList.add('is-loading', 'small-loading-icon');
+      btn.classList.add('is-loading', 'loading-icon-2px');
       try {
         const res = await GET(link, {credentials: 'include', redirect: 'follow'});
         const contentType = res.headers.get('content-type');
@@ -33,7 +33,7 @@ export function initCopyContent() {
       } catch {
         return showTemporaryTooltip(btn, i18n.copy_error);
       } finally {
-        btn.classList.remove('is-loading', 'small-loading-icon');
+        btn.classList.remove('is-loading', 'loading-icon-2px');
       }
     } else { // text, read from DOM
       const lineEls = document.querySelectorAll('.file-view .lines-code');

From 934fa46f769f0b90fc319054612d4f5c9a4c46ba Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 31 Mar 2024 22:22:29 +0200
Subject: [PATCH 603/679] Add `/options/license` and `/options/gitignore` to
 `.ignore` (#30219)

Ignore this folder in tools like `rg` or `ag`. Also sorted the entries
alphabetically.
---
 .ignore | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.ignore b/.ignore
index 5c945ab981..5b96dabd38 100644
--- a/.ignore
+++ b/.ignore
@@ -4,6 +4,8 @@
 /modules/options/bindata.go
 /modules/public/bindata.go
 /modules/templates/bindata.go
-/vendor
+/options/gitignore
+/options/license
 /public/assets
+/vendor
 node_modules

From 3607f827fb432378dbe7d90bdedbff4fd4565e13 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 1 Apr 2024 00:27:21 +0000
Subject: [PATCH 604/679] [skip ci] Updated licenses and gitignores

---
 options/license/AMD-newlib | 11 +++++++++++
 options/license/OAR        | 12 ++++++++++++
 options/license/xzoom      | 12 ++++++++++++
 3 files changed, 35 insertions(+)
 create mode 100644 options/license/AMD-newlib
 create mode 100644 options/license/OAR
 create mode 100644 options/license/xzoom

diff --git a/options/license/AMD-newlib b/options/license/AMD-newlib
new file mode 100644
index 0000000000..1b2f1abd6f
--- /dev/null
+++ b/options/license/AMD-newlib
@@ -0,0 +1,11 @@
+Copyright 1989, 1990 Advanced Micro Devices, Inc.
+
+This software is the property of Advanced Micro Devices, Inc  (AMD)  which
+specifically  grants the user the right to modify, use and distribute this
+software provided this notice is not removed or altered.  All other rights
+are reserved by AMD.
+
+AMD MAKES NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THIS
+SOFTWARE.  IN NO EVENT SHALL AMD BE LIABLE FOR INCIDENTAL OR CONSEQUENTIAL
+DAMAGES IN CONNECTION WITH OR ARISING FROM THE FURNISHING, PERFORMANCE, OR
+USE OF THIS SOFTWARE.
diff --git a/options/license/OAR b/options/license/OAR
new file mode 100644
index 0000000000..ca5c4b9617
--- /dev/null
+++ b/options/license/OAR
@@ -0,0 +1,12 @@
+COPYRIGHT (c) 1989-2013, 2015.
+On-Line Applications Research Corporation (OAR).
+
+Permission to use, copy, modify, and distribute this software for any
+purpose without fee is hereby granted, provided that this entire notice
+is included in all copies of any software which is or includes a copy
+or modification of this software.
+
+THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTY.  IN PARTICULAR,  THE AUTHOR MAKES NO REPRESENTATION
+OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS
+SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
diff --git a/options/license/xzoom b/options/license/xzoom
new file mode 100644
index 0000000000..f312dedbc2
--- /dev/null
+++ b/options/license/xzoom
@@ -0,0 +1,12 @@
+Copyright Itai Nahshon 1995, 1996.
+This program is distributed with no warranty.
+
+Source files for this program may be distributed freely.
+Modifications to this file are okay as long as:
+ a. This copyright notice and comment are preserved and
+    left at the top of the file.
+ b. The man page is fixed to reflect the change.
+ c. The author of this change adds his name and change
+    description to the list of changes below.
+Executable files may be distributed with sources, or with
+exact location where the source code can be obtained.

From a008486f5c5acfe2d2acb009f41dc660ee8348eb Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 1 Apr 2024 10:06:35 +0800
Subject: [PATCH 605/679] Refactor DeleteInactiveUsers, fix bug and add tests
 (#30206)

1. check `IsActive` before calling `IsLastAdminUser`.
2. Fix some comments and error messages.
3. Don't `return err` if "removing file" fails in `DeleteUser`.
4. Remove incorrect `DeleteInactiveEmailAddresses`. Active users could
also have inactive emails, and inactive emails do not support
"olderThan"
5. Add tests
---
 models/user/email_address.go |  8 -------
 services/user/user.go        | 45 ++++++++++++++++--------------------
 services/user/user_test.go   | 25 ++++++++++++++++++++
 3 files changed, 45 insertions(+), 33 deletions(-)

diff --git a/models/user/email_address.go b/models/user/email_address.go
index d26549f383..08771efe99 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -256,14 +256,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
 	return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
 }
 
-// DeleteInactiveEmailAddresses deletes inactive email addresses
-func DeleteInactiveEmailAddresses(ctx context.Context) error {
-	_, err := db.GetEngine(ctx).
-		Where("is_activated = ?", false).
-		Delete(new(EmailAddress))
-	return err
-}
-
 // ActivateEmail activates the email address to given user.
 func ActivateEmail(ctx context.Context, email *EmailAddress) error {
 	ctx, committer, err := db.TxContext(ctx)
diff --git a/services/user/user.go b/services/user/user.go
index 4fcb81581d..2287e36c71 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -126,7 +126,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 		return fmt.Errorf("%s is an organization not a user", u.Name)
 	}
 
-	if user_model.IsLastAdminUser(ctx, u) {
+	if u.IsActive && user_model.IsLastAdminUser(ctx, u) {
 		return models.ErrDeleteLastAdminUser{UID: u.ID}
 	}
 
@@ -250,7 +250,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 	if err := committer.Commit(); err != nil {
 		return err
 	}
-	committer.Close()
+	_ = committer.Close()
 
 	if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 		return err
@@ -259,50 +259,45 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 		return err
 	}
 
-	// Note: There are something just cannot be roll back,
-	//	so just keep error logs of those operations.
+	// Note: There are something just cannot be roll back, so just keep error logs of those operations.
 	path := user_model.UserPath(u.Name)
-	if err := util.RemoveAll(path); err != nil {
-		err = fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
+	if err = util.RemoveAll(path); err != nil {
+		err = fmt.Errorf("failed to RemoveAll %s: %w", path, err)
 		_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
-		return err
 	}
 
 	if u.Avatar != "" {
 		avatarPath := u.CustomAvatarRelativePath()
-		if err := storage.Avatars.Delete(avatarPath); err != nil {
-			err = fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
+		if err = storage.Avatars.Delete(avatarPath); err != nil {
+			err = fmt.Errorf("failed to remove %s: %w", avatarPath, err)
 			_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
-			return err
 		}
 	}
 
 	return nil
 }
 
-// DeleteInactiveUsers deletes all inactive users and email addresses.
+// DeleteInactiveUsers deletes all inactive users and their email addresses.
 func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
-	users, err := user_model.GetInactiveUsers(ctx, olderThan)
+	inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
 	if err != nil {
 		return err
 	}
 
 	// FIXME: should only update authorized_keys file once after all deletions.
-	for _, u := range users {
-		select {
-		case <-ctx.Done():
-			return db.ErrCancelledf("Before delete inactive user %s", u.Name)
-		default:
-		}
-		if err := DeleteUser(ctx, u, false); err != nil {
-			// Ignore users that were set inactive by admin.
-			if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
-				models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
+	for _, u := range inactiveUsers {
+		if err = DeleteUser(ctx, u, false); err != nil {
+			// Ignore inactive users that were ever active but then were set inactive by admin
+			if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
 				continue
 			}
-			return err
+			select {
+			case <-ctx.Done():
+				return db.ErrCancelledf("when deleting inactive user %q", u.Name)
+			default:
+				return err
+			}
 		}
 	}
-
-	return user_model.DeleteInactiveEmailAddresses(ctx)
+	return nil // TODO: there could be still inactive users left, and the number would increase gradually
 }
diff --git a/services/user/user_test.go b/services/user/user_test.go
index f110bd26d0..bd6019a14f 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"strings"
 	"testing"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/auth"
@@ -16,6 +17,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -185,3 +187,26 @@ func TestCreateUser_Issue5882(t *testing.T) {
 		assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
 	}
 }
+
+func TestDeleteInactiveUsers(t *testing.T) {
+	addUser := func(name, email string, createdUnix timeutil.TimeStamp, active bool) {
+		inactiveUser := &user_model.User{Name: name, LowerName: strings.ToLower(name), Email: email, CreatedUnix: createdUnix, IsActive: active}
+		_, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(inactiveUser)
+		assert.NoError(t, err)
+		inactiveUserEmail := &user_model.EmailAddress{UID: inactiveUser.ID, IsPrimary: true, Email: email, LowerEmail: strings.ToLower(email), IsActivated: active}
+		err = db.Insert(db.DefaultContext, inactiveUserEmail)
+		assert.NoError(t, err)
+	}
+	addUser("user-inactive-10", "user-inactive-10@test.com", timeutil.TimeStampNow().Add(-600), false)
+	addUser("user-inactive-5", "user-inactive-5@test.com", timeutil.TimeStampNow().Add(-300), false)
+	addUser("user-active-10", "user-active-10@test.com", timeutil.TimeStampNow().Add(-600), true)
+	addUser("user-active-5", "user-active-5@test.com", timeutil.TimeStampNow().Add(-300), true)
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-10"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+	assert.NoError(t, DeleteInactiveUsers(db.DefaultContext, 8*time.Minute))
+	unittest.AssertNotExistsBean(t, &user_model.User{Name: "user-inactive-10"})
+	unittest.AssertNotExistsBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-5"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-10"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-5"})
+}

From 751997ad34fdd52b9f3956b14395560b059c9ac1 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 1 Apr 2024 21:11:30 +0800
Subject: [PATCH 606/679] Refactor file view & render (#30227)

The old code is inconsistent and fragile, and the UI isn't right.
---
 routers/web/repo/blame.go             | 10 +++++++++-
 routers/web/repo/setting/lfs.go       | 17 ++++++++---------
 routers/web/repo/view.go              | 12 +++++++-----
 templates/repo/blame.tmpl             |  4 ++++
 templates/repo/settings/lfs_file.tmpl | 10 ++++------
 templates/repo/view_file.tmpl         | 16 ++++------------
 templates/shared/filetoolarge.tmpl    |  4 ++++
 7 files changed, 40 insertions(+), 33 deletions(-)
 create mode 100644 templates/shared/filetoolarge.tmpl

diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 935e6d78fc..1887e4d95d 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -87,9 +88,16 @@ func RefBlame(ctx *context.Context) {
 
 	ctx.Data["IsBlame"] = true
 
-	ctx.Data["FileSize"] = blob.Size()
+	fileSize := blob.Size()
+	ctx.Data["FileSize"] = fileSize
 	ctx.Data["FileName"] = blob.Name()
 
+	if fileSize >= setting.UI.MaxDisplayFileSize {
+		ctx.Data["IsFileTooLarge"] = true
+		ctx.HTML(http.StatusOK, tplRepoHome)
+		return
+	}
+
 	ctx.Data["NumLines"], err = blob.GetBlobLineCount()
 	ctx.Data["NumLinesSet"] = true
 
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 32049cf0a4..6dddade066 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -287,22 +287,19 @@ func LFSFileGet(ctx *context.Context) {
 
 	st := typesniffer.DetectContentType(buf)
 	ctx.Data["IsTextFile"] = st.IsText()
-	isRepresentableAsText := st.IsRepresentableAsText()
-
-	fileSize := meta.Size
 	ctx.Data["FileSize"] = meta.Size
 	ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
 	switch {
-	case isRepresentableAsText:
-		if st.IsSvgImage() {
-			ctx.Data["IsImageFile"] = true
-		}
-
-		if fileSize >= setting.UI.MaxDisplayFileSize {
+	case st.IsRepresentableAsText():
+		if meta.Size >= setting.UI.MaxDisplayFileSize {
 			ctx.Data["IsFileTooLarge"] = true
 			break
 		}
 
+		if st.IsSvgImage() {
+			ctx.Data["IsImageFile"] = true
+		}
+
 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 
 		// Building code view blocks with line number on server side.
@@ -338,6 +335,8 @@ func LFSFileGet(ctx *context.Context) {
 		ctx.Data["IsAudioFile"] = true
 	case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
 		ctx.Data["IsImageFile"] = true
+	default:
+		// TODO: the logic is not the same as "renderFile" in "view.go"
 	}
 	ctx.HTML(http.StatusOK, tplSettingsLFSFile)
 }
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 93e0f5bcbd..8aa9dbb1be 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -482,17 +482,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 
 	switch {
 	case isRepresentableAsText:
+		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+			ctx.Data["IsFileTooLarge"] = true
+			break
+		}
+
 		if fInfo.st.IsSvgImage() {
 			ctx.Data["IsImageFile"] = true
 			ctx.Data["CanCopyContent"] = true
 			ctx.Data["HasSourceRenderedToggle"] = true
 		}
 
-		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
-			ctx.Data["IsFileTooLarge"] = true
-			break
-		}
-
 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 
 		shouldRenderSource := ctx.FormString("display") == "source"
@@ -606,6 +606,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			break
 		}
 
+		// TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
+		// maybe for this case, the file is a binary file, and shouldn't be rendered?
 		if markupType := markup.Type(blob.Name()); markupType != "" {
 			rd := io.MultiReader(bytes.NewReader(buf), dataRc)
 			ctx.Data["IsMarkup"] = true
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 1a148a2d1c..30d1a3d78d 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -30,6 +30,9 @@
 	</h4>
 	<div class="ui attached table unstackable segment">
 		<div class="file-view code-view unicode-escaped">
+			{{if .IsFileTooLarge}}
+				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else}}
 			<table>
 				<tbody>
 					{{range $row := .BlameRows}}
@@ -75,6 +78,7 @@
 					{{end}}
 				</tbody>
 			</table>
+			{{end}}{{/* end if .IsFileTooLarge */}}
 			<div class="code-line-menu tippy-target">
 				{{if $.Permission.CanRead $.UnitTypeIssues}}
 					<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
index 43afba96c3..cb65236f23 100644
--- a/templates/repo/settings/lfs_file.tmpl
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -14,7 +14,9 @@
 			<div class="ui attached table unstackable segment">
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
 				<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
-					{{if .IsMarkup}}
+					{{if .IsFileTooLarge}}
+						{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+					{{else if .IsMarkup}}
 						{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}
 					{{else if .IsPlainText}}
 						<pre>{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}</pre>
@@ -33,19 +35,15 @@
 							{{else if .IsPDFFile}}
 								<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div>
 							{{else}}
-								<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
+								<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
 							{{end}}
 						</div>
 					{{else if .FileSize}}
 						<table>
 							<tbody>
 								<tr>
-								{{if .IsFileTooLarge}}
-									<td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td>
-								{{else}}
 									<td class="lines-num">{{.LineNums}}</td>
 									<td class="lines-code"><pre><code class="{{.HighlightClass}}"><ol>{{.FileContent}}</ol></code></pre></td>
-								{{end}}
 								</tr>
 							</tbody>
 						</table>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index b7c1b9eeae..9c5bd9094d 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -89,7 +89,9 @@
 			{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
 		{{end}}
 		<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
-			{{if .IsMarkup}}
+			{{if .IsFileTooLarge}}
+				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else if .IsMarkup}}
 				{{if .FileContent}}{{.FileContent}}{{end}}
 			{{else if .IsPlainText}}
 				<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
@@ -108,19 +110,10 @@
 					{{else if .IsPDFFile}}
 						<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
 					{{else}}
-						<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
+						<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
 					{{end}}
 				</div>
 			{{else if .FileSize}}
-				{{if .IsFileTooLarge}}
-				<table>
-					<tbody>
-						<tr>
-							<td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td>
-						</tr>
-					</tbody>
-				</table>
-				{{else}}
 				<table>
 					<tbody>
 						{{range $idx, $code := .FileContent}}
@@ -142,7 +135,6 @@
 					<a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
 					<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
 				</div>
-				{{end}}
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/shared/filetoolarge.tmpl b/templates/shared/filetoolarge.tmpl
new file mode 100644
index 0000000000..8842fb1b91
--- /dev/null
+++ b/templates/shared/filetoolarge.tmpl
@@ -0,0 +1,4 @@
+<div class="tw-p-4">
+	{{ctx.Locale.Tr "repo.file_too_large"}}
+	{{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}}
+</div>

From 1ef2eb50d82d07b1e4ff312ef58953d1bba2437a Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Mon, 1 Apr 2024 21:48:14 +0800
Subject: [PATCH 607/679] Remove scheduled action tasks if the repo is archived
 (#30224)

Fix #30220
---
 routers/api/v1/repo/repo.go         | 10 ++++++++++
 routers/web/repo/setting/setting.go | 12 ++++++++++++
 services/actions/notifier_helper.go |  4 ++--
 services/actions/schedule_tasks.go  |  5 +++++
 4 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 80504b9c33..822e368fa8 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"time"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
@@ -31,6 +32,7 @@ import (
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/issue"
@@ -1035,6 +1037,9 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
 				ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
 				return err
 			}
+			if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+				log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+			}
 			log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 		} else {
 			if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
@@ -1042,6 +1047,11 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
 				ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
 				return err
 			}
+			if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+				if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+					log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+				}
+			}
 			log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 		}
 	}
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index e045e3b8dc..00a5282f34 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -13,6 +13,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -29,6 +30,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
+	actions_service "code.gitea.io/gitea/services/actions"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
@@ -897,6 +899,10 @@ func SettingsPost(ctx *context.Context) {
 			return
 		}
 
+		if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+			log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+		}
+
 		ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
 
 		log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
@@ -915,6 +921,12 @@ func SettingsPost(ctx *context.Context) {
 			return
 		}
 
+		if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+			if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+				log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+			}
+		}
+
 		ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
 
 		log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 66a19844c2..8c98f56af5 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -117,7 +117,7 @@ func notify(ctx context.Context, input *notifyInput) error {
 		log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
 		return nil
 	}
-	if input.Repo.IsEmpty {
+	if input.Repo.IsEmpty || input.Repo.IsArchived {
 		return nil
 	}
 	if unit_model.TypeActions.UnitGlobalDisabled() {
@@ -501,7 +501,7 @@ func handleSchedules(
 
 // DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
 func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
-	if repo.IsEmpty {
+	if repo.IsEmpty || repo.IsArchived {
 		return nil
 	}
 
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 59862fd0d8..e4e56e5122 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -66,6 +66,11 @@ func startTasks(ctx context.Context) error {
 				}
 			}
 
+			if row.Repo.IsArchived {
+				// Skip if the repo is archived
+				continue
+			}
+
 			cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
 			if err != nil {
 				if repo_model.IsErrUnitTypeNotExist(err) {

From ca297a90fb1fec5b270fad1a3e575916510e7385 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 2 Apr 2024 02:16:38 +0800
Subject: [PATCH 608/679] Refactor dropzone (#30232)

Simplify code and use `.files` elements
---
 web_src/js/features/repo-legacy.js | 50 +++++++++++++-----------------
 1 file changed, 21 insertions(+), 29 deletions(-)

diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 34320de1de..4c7dd36920 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -358,11 +358,11 @@ async function onEditContent(event) {
           input.name = 'files';
           input.type = 'hidden';
           input.value = data.uuid;
-          dropzone.querySelector('.files').insertAdjacentHTML('beforeend', input.outerHTML);
+          dropzone.querySelector('.files').append(input);
         });
         this.on('removedfile', async (file) => {
-          if (disableRemovedfileEvent) return;
           document.getElementById(file.uuid)?.remove();
+          if (disableRemovedfileEvent) return;
           if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
             try {
               await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
@@ -384,6 +384,7 @@ async function onEditContent(event) {
             disableRemovedfileEvent = true;
             dz.removeAllFiles(true);
             dropzone.querySelector('.files').innerHTML = '';
+            for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
             fileUuidDict = {};
             disableRemovedfileEvent = false;
 
@@ -392,7 +393,6 @@ async function onEditContent(event) {
               dz.emit('addedfile', attachment);
               dz.emit('thumbnail', attachment, imgSrc);
               dz.emit('complete', attachment);
-              dz.files.push(attachment);
               fileUuidDict[attachment.uuid] = {submitted: true};
               dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
               const input = document.createElement('input');
@@ -400,7 +400,10 @@ async function onEditContent(event) {
               input.name = 'files';
               input.type = 'hidden';
               input.value = attachment.uuid;
-              dropzone.querySelector('.files').insertAdjacentHTML('beforeend', input.outerHTML);
+              dropzone.querySelector('.files').append(input);
+            }
+            if (!dropzone.querySelector('.dz-preview')) {
+              dropzone.classList.remove('dz-started');
             }
           } catch (error) {
             console.error(error);
@@ -412,24 +415,24 @@ async function onEditContent(event) {
     return dz;
   };
 
-  const cancelAndReset = (dz) => {
+  const cancelAndReset = (e) => {
+    e.preventDefault();
     showElem(renderContent);
     hideElem(editContentZone);
-    if (dz) {
-      dz.emit('reload');
-    }
+    comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
   };
 
-  const saveAndRefresh = async (dz) => {
+  const saveAndRefresh = async (e) => {
+    e.preventDefault();
     showElem(renderContent);
     hideElem(editContentZone);
-
+    const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
     try {
       const params = new URLSearchParams({
         content: comboMarkdownEditor.value(),
         context: editContentZone.getAttribute('data-context'),
       });
-      for (const file of dz.files) params.append('files[]', file.uuid);
+      for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
 
       const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
       const data = await response.json();
@@ -452,10 +455,8 @@ async function onEditContent(event) {
       } else {
         content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
       }
-      if (dz) {
-        dz.emit('submit');
-        dz.emit('reload');
-      }
+      dropzoneInst?.emit('submit');
+      dropzoneInst?.emit('reload');
       initMarkupContent();
       initCommentContent();
     } catch (error) {
@@ -463,22 +464,13 @@ async function onEditContent(event) {
     }
   };
 
-  if (!editContentZone.innerHTML) {
+  comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+  if (!comboMarkdownEditor) {
     editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
     comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
-
-    const dropzone = editContentZone.querySelector('.dropzone');
-    const dz = await setupDropzone(dropzone);
-    editContentZone.querySelector('.cancel.button').addEventListener('click', (e) => {
-      e.preventDefault();
-      cancelAndReset(dz);
-    });
-    editContentZone.querySelector('.save.button').addEventListener('click', (e) => {
-      e.preventDefault();
-      saveAndRefresh(dz);
-    });
-  } else {
-    comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+    comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
+    editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
+    editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
   }
 
   // Show write/preview tab and copy raw content as needed

From 0db554fa634737af59613768b2e01bfe3e239e68 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 2 Apr 2024 04:23:17 +0800
Subject: [PATCH 609/679] Refactor commit signature parser (#30228)

To make it more flexible and support SSH signature.

The existing tests are not changed, there are also tests covering
`parseTagRef` which also calls `parsePayloadSignature` now. Add some new
tests to `Test_parseTagData`
---
 modules/git/commit.go               |   6 +-
 modules/git/commit_convert_gogit.go |   4 +-
 modules/git/commit_reader.go        |   2 +-
 modules/git/repo_tag.go             |  10 ++-
 modules/git/repo_tag_test.go        |   2 +-
 modules/git/tag.go                  | 104 +++++++++++++++------------
 modules/git/tag_test.go             | 106 +++++++++++++++++-----------
 7 files changed, 135 insertions(+), 99 deletions(-)

diff --git a/modules/git/commit.go b/modules/git/commit.go
index ef2676762c..5f442b0e1a 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -26,14 +26,14 @@ type Commit struct {
 	Author        *Signature
 	Committer     *Signature
 	CommitMessage string
-	Signature     *CommitGPGSignature
+	Signature     *CommitSignature
 
 	Parents        []ObjectID // ID strings
 	submoduleCache *ObjectCache
 }
 
-// CommitGPGSignature represents a git commit signature part.
-type CommitGPGSignature struct {
+// CommitSignature represents a git commit signature part.
+type CommitSignature struct {
 	Signature string
 	Payload   string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
 }
diff --git a/modules/git/commit_convert_gogit.go b/modules/git/commit_convert_gogit.go
index 33ef2f4487..d7b945ed6b 100644
--- a/modules/git/commit_convert_gogit.go
+++ b/modules/git/commit_convert_gogit.go
@@ -13,7 +13,7 @@ import (
 	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
-func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
+func convertPGPSignature(c *object.Commit) *CommitSignature {
 	if c.PGPSignature == "" {
 		return nil
 	}
@@ -57,7 +57,7 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
 		return nil
 	}
 
-	return &CommitGPGSignature{
+	return &CommitSignature{
 		Signature: c.PGPSignature,
 		Payload:   w.String(),
 	}
diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go
index 56c41dc473..f1f4a0e588 100644
--- a/modules/git/commit_reader.go
+++ b/modules/git/commit_reader.go
@@ -99,7 +99,7 @@ readLoop:
 		}
 	}
 	commit.CommitMessage = messageSB.String()
-	commit.Signature = &CommitGPGSignature{
+	commit.Signature = &CommitSignature{
 		Signature: signatureSB.String(),
 		Payload:   payloadSB.String(),
 	}
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index e8c5ce6fb8..2026a4c9f5 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -185,17 +185,15 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) {
 
 	tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
 	tag.Message = ref["contents"]
-	// strip PGP signature if present in contents field
-	pgpStart := strings.Index(tag.Message, beginpgp)
-	if pgpStart >= 0 {
-		tag.Message = tag.Message[0:pgpStart]
-	}
+
+	// strip any signature if present in contents field
+	_, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0)
 
 	// annotated tag with GPG signature
 	if tag.Type == "tag" && ref["contents:signature"] != "" {
 		payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
 			tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
-		tag.Signature = &CommitGPGSignature{
+		tag.Signature = &CommitSignature{
 			Signature: ref["contents:signature"],
 			Payload:   payload,
 		}
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
index 785c3442a7..0117cb902d 100644
--- a/modules/git/repo_tag_test.go
+++ b/modules/git/repo_tag_test.go
@@ -315,7 +315,7 @@ qbHDASXl
 				Type:    "tag",
 				Tagger:  parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
 				Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
-				Signature: &CommitGPGSignature{
+				Signature: &CommitSignature{
 					Signature: `-----BEGIN PGP SIGNATURE-----
 
 aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3
diff --git a/modules/git/tag.go b/modules/git/tag.go
index 94e5cd7c63..f7666aa89b 100644
--- a/modules/git/tag.go
+++ b/modules/git/tag.go
@@ -6,16 +6,10 @@ package git
 import (
 	"bytes"
 	"sort"
-	"strings"
 
 	"code.gitea.io/gitea/modules/util"
 )
 
-const (
-	beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
-	endpgp   = "\n-----END PGP SIGNATURE-----"
-)
-
 // Tag represents a Git tag.
 type Tag struct {
 	Name      string
@@ -24,7 +18,7 @@ type Tag struct {
 	Type      string
 	Tagger    *Signature
 	Message   string
-	Signature *CommitGPGSignature
+	Signature *CommitSignature
 }
 
 // Commit return the commit of the tag reference
@@ -32,6 +26,36 @@ func (tag *Tag) Commit(gitRepo *Repository) (*Commit, error) {
 	return gitRepo.getCommit(tag.Object)
 }
 
+func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) {
+	pos := messageStart
+	signStart, signEnd := -1, -1
+	for {
+		eol := bytes.IndexByte(data[pos:], '\n')
+		if eol < 0 {
+			break
+		}
+		line := data[pos : pos+eol]
+		signType, hasPrefix := bytes.CutPrefix(line, []byte("-----BEGIN "))
+		signType, hasSuffix := bytes.CutSuffix(signType, []byte(" SIGNATURE-----"))
+		if hasPrefix && hasSuffix {
+			signEndBytes := append([]byte("\n-----END "), signType...)
+			signEndBytes = append(signEndBytes, []byte(" SIGNATURE-----")...)
+			signEnd = bytes.Index(data[pos:], signEndBytes)
+			if signEnd != -1 {
+				signStart = pos
+				signEnd = pos + signEnd + len(signEndBytes)
+			}
+		}
+		pos += eol + 1
+	}
+
+	if signStart != -1 && signEnd != -1 {
+		msgEnd := max(messageStart, signStart-1)
+		return string(data[:msgEnd]), string(data[messageStart:msgEnd]), string(data[signStart:signEnd])
+	}
+	return string(data), string(data[messageStart:]), ""
+}
+
 // Parse commit information from the (uncompressed) raw
 // data from the commit object.
 // \n\n separate headers from message
@@ -40,47 +64,37 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) {
 	tag.ID = objectFormat.EmptyObjectID()
 	tag.Object = objectFormat.EmptyObjectID()
 	tag.Tagger = &Signature{}
-	// we now have the contents of the commit object. Let's investigate...
-	nextline := 0
-l:
+
+	pos := 0
 	for {
-		eol := bytes.IndexByte(data[nextline:], '\n')
-		switch {
-		case eol > 0:
-			line := data[nextline : nextline+eol]
-			spacepos := bytes.IndexByte(line, ' ')
-			reftype := line[:spacepos]
-			switch string(reftype) {
-			case "object":
-				id, err := NewIDFromString(string(line[spacepos+1:]))
-				if err != nil {
-					return nil, err
-				}
-				tag.Object = id
-			case "type":
-				// A commit can have one or more parents
-				tag.Type = string(line[spacepos+1:])
-			case "tagger":
-				tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(line[spacepos+1:]))
-			}
-			nextline += eol + 1
-		case eol == 0:
-			tag.Message = string(data[nextline+1:])
-			break l
-		default:
-			break l
+		eol := bytes.IndexByte(data[pos:], '\n')
+		if eol == -1 {
+			break // shouldn't happen, but could just tolerate it
 		}
+		if eol == 0 {
+			pos++
+			break // end of headers
+		}
+		line := data[pos : pos+eol]
+		key, val, _ := bytes.Cut(line, []byte(" "))
+		switch string(key) {
+		case "object":
+			id, err := NewIDFromString(string(val))
+			if err != nil {
+				return nil, err
+			}
+			tag.Object = id
+		case "type":
+			tag.Type = string(val) // A commit can have one or more parents
+		case "tagger":
+			tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(val))
+		}
+		pos += eol + 1
 	}
-	idx := strings.LastIndex(tag.Message, beginpgp)
-	if idx > 0 {
-		endSigIdx := strings.Index(tag.Message[idx:], endpgp)
-		if endSigIdx > 0 {
-			tag.Signature = &CommitGPGSignature{
-				Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
-				Payload:   string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
-			}
-			tag.Message = tag.Message[:idx+1]
-		}
+	payload, msg, sign := parsePayloadSignature(data, pos)
+	tag.Message = msg
+	if len(sign) > 0 {
+		tag.Signature = &CommitSignature{Signature: sign, Payload: payload}
 	}
 	return tag, nil
 }
diff --git a/modules/git/tag_test.go b/modules/git/tag_test.go
index f980b0c560..ba02c28946 100644
--- a/modules/git/tag_test.go
+++ b/modules/git/tag_test.go
@@ -12,24 +12,28 @@ import (
 
 func Test_parseTagData(t *testing.T) {
 	testData := []struct {
-		data []byte
-		tag  Tag
+		data     string
+		expected Tag
 	}{
-		{data: []byte(`object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
+		{
+			data: `object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
 type commit
 tag 1.22.0
 tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100
 
-`), tag: Tag{
-			Name:      "",
-			ID:        Sha1ObjectFormat.EmptyObjectID(),
-			Object:    &Sha1Hash{0x3b, 0x11, 0x4a, 0xb8, 0x0, 0xc6, 0x43, 0x2a, 0xd4, 0x23, 0x87, 0xcc, 0xf6, 0xbc, 0x8d, 0x43, 0x88, 0xa2, 0x88, 0x5a},
-			Type:      "commit",
-			Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0)},
-			Message:   "",
-			Signature: nil,
-		}},
-		{data: []byte(`object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
+`,
+			expected: Tag{
+				Name:      "",
+				ID:        Sha1ObjectFormat.EmptyObjectID(),
+				Object:    MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"),
+				Type:      "commit",
+				Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
+				Message:   "",
+				Signature: nil,
+			},
+		},
+		{
+			data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
 type commit
 tag 1.22.1
 tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
@@ -37,37 +41,57 @@ tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
 test message
 o
 
-ono`), tag: Tag{
-			Name:      "",
-			ID:        Sha1ObjectFormat.EmptyObjectID(),
-			Object:    &Sha1Hash{0x7c, 0xdf, 0x42, 0xc0, 0xb1, 0xcc, 0x76, 0x3a, 0xb7, 0xe4, 0xc3, 0x3c, 0x47, 0xa2, 0x4e, 0x27, 0xc6, 0x6b, 0xfc, 0xcc},
-			Type:      "commit",
-			Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0)},
-			Message:   "test message\no\n\nono",
-			Signature: nil,
-		}},
+ono`,
+			expected: Tag{
+				Name:      "",
+				ID:        Sha1ObjectFormat.EmptyObjectID(),
+				Object:    MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"),
+				Type:      "commit",
+				Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))},
+				Message:   "test message\no\n\nono",
+				Signature: nil,
+			},
+		},
+		{
+			data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
+type commit
+tag v0
+tagger dummy user <dummy-email@example.com> 1484491741 +0100
+
+dummy message
+-----BEGIN SSH SIGNATURE-----
+dummy signature
+-----END SSH SIGNATURE-----
+`,
+			expected: Tag{
+				Name:    "",
+				ID:      Sha1ObjectFormat.EmptyObjectID(),
+				Object:  MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"),
+				Type:    "commit",
+				Tagger:  &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
+				Message: "dummy message",
+				Signature: &CommitSignature{
+					Signature: `-----BEGIN SSH SIGNATURE-----
+dummy signature
+-----END SSH SIGNATURE-----`,
+					Payload: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
+type commit
+tag v0
+tagger dummy user <dummy-email@example.com> 1484491741 +0100
+
+dummy message`,
+				},
+			},
+		},
 	}
 
 	for _, test := range testData {
-		tag, err := parseTagData(Sha1ObjectFormat, test.data)
+		tag, err := parseTagData(Sha1ObjectFormat, []byte(test.data))
 		assert.NoError(t, err)
-		assert.EqualValues(t, test.tag.ID, tag.ID)
-		assert.EqualValues(t, test.tag.Object, tag.Object)
-		assert.EqualValues(t, test.tag.Name, tag.Name)
-		assert.EqualValues(t, test.tag.Message, tag.Message)
-		assert.EqualValues(t, test.tag.Type, tag.Type)
-		if test.tag.Signature != nil && assert.NotNil(t, tag.Signature) {
-			assert.EqualValues(t, test.tag.Signature.Signature, tag.Signature.Signature)
-			assert.EqualValues(t, test.tag.Signature.Payload, tag.Signature.Payload)
-		} else {
-			assert.Nil(t, tag.Signature)
-		}
-		if test.tag.Tagger != nil && assert.NotNil(t, tag.Tagger) {
-			assert.EqualValues(t, test.tag.Tagger.Name, tag.Tagger.Name)
-			assert.EqualValues(t, test.tag.Tagger.Email, tag.Tagger.Email)
-			assert.EqualValues(t, test.tag.Tagger.When.Unix(), tag.Tagger.When.Unix())
-		} else {
-			assert.Nil(t, tag.Tagger)
-		}
+		assert.Equal(t, test.expected, *tag)
 	}
+
+	tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted..."))
+	assert.NoError(t, err)
+	assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message)
 }

From 8a5c597c1d53e7652f1f3fc59e64b46a04c5e20b Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 2 Apr 2024 00:24:02 +0000
Subject: [PATCH 610/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_cs-CZ.ini | 1 -
 options/locale/locale_de-DE.ini | 1 -
 options/locale/locale_el-GR.ini | 1 -
 options/locale/locale_es-ES.ini | 1 -
 options/locale/locale_fa-IR.ini | 1 -
 options/locale/locale_fi-FI.ini | 1 -
 options/locale/locale_fr-FR.ini | 1 -
 options/locale/locale_hu-HU.ini | 1 -
 options/locale/locale_it-IT.ini | 1 -
 options/locale/locale_ja-JP.ini | 1 -
 options/locale/locale_lv-LV.ini | 1 -
 options/locale/locale_nl-NL.ini | 1 -
 options/locale/locale_pl-PL.ini | 1 -
 options/locale/locale_pt-BR.ini | 1 -
 options/locale/locale_pt-PT.ini | 4 +++-
 options/locale/locale_ru-RU.ini | 1 -
 options/locale/locale_si-LK.ini | 1 -
 options/locale/locale_sv-SE.ini | 1 -
 options/locale/locale_tr-TR.ini | 1 -
 options/locale/locale_uk-UA.ini | 1 -
 options/locale/locale_zh-CN.ini | 1 -
 options/locale/locale_zh-TW.ini | 1 -
 22 files changed, 3 insertions(+), 22 deletions(-)

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 4abf813725..82a8fe5d45 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -2790,7 +2790,6 @@ settings=Nastavení správce
 
 dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací.
 dashboard.statistic=Souhrn
-dashboard.operations=Operace údržby
 dashboard.system_status=Status systému
 dashboard.operation_name=Název operace
 dashboard.operation_switch=Přepnout
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 4d446db86f..9a09c2922e 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -2798,7 +2798,6 @@ settings=Administratoreinstellungen
 
 dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">Blog</a>.
 dashboard.statistic=Übersicht
-dashboard.operations=Wartungsoperationen
 dashboard.system_status=System-Status
 dashboard.operation_name=Name der Operation
 dashboard.operation_switch=Wechseln
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 1199d84581..6ce5ae1ce9 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -2687,7 +2687,6 @@ settings=Ρυθμίσεις Διαχειριστή
 
 dashboard.new_version_hint=Το Gitea %s είναι διαθέσιμο, τώρα εκτελείτε το %s. Ανατρέξτε <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">στο blog</a> για περισσότερες λεπτομέρειες.
 dashboard.statistic=Περίληψη
-dashboard.operations=Λειτουργίες Συντήρησης
 dashboard.system_status=Κατάσταση Συστήματος
 dashboard.operation_name=Όνομα Λειτουργίας
 dashboard.operation_switch=Αλλαγή
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index ce50b71ec4..fc78e1d439 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -2672,7 +2672,6 @@ settings=Configuración de Admin
 
 dashboard.new_version_hint=Gitea %s ya está disponible, estás ejecutando %s. Revisa <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">el blog</a> para más detalles.
 dashboard.statistic=Resumen
-dashboard.operations=Operaciones de mantenimiento
 dashboard.system_status=Estado del sistema
 dashboard.operation_name=Nombre de la operación
 dashboard.operation_switch=Interruptor
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 31122841a7..d19eb356d2 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -2064,7 +2064,6 @@ last_page=واپسین
 total=مجموع: %d
 
 dashboard.statistic=چکیده
-dashboard.operations=عملیات‌های نگهداری
 dashboard.system_status=وضعیت سامانه
 dashboard.operation_name=نام عملیات
 dashboard.operation_switch=تعویض
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index 00581f49fc..f283209908 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -1407,7 +1407,6 @@ last_page=Viimeisin
 total=Yhteensä: %d
 
 dashboard.statistic=Yhteenveto
-dashboard.operations=Huoltotoimet
 dashboard.system_status=Järjestelmän tila
 dashboard.operation_name=Toiminnon nimi
 dashboard.operation_switch=Vaihda
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 062c818bd4..dc66402901 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -2712,7 +2712,6 @@ settings=Paramètres administrateur
 
 dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> pour plus de détails.
 dashboard.statistic=Résumé
-dashboard.operations=Opérations de maintenance
 dashboard.system_status=État du système
 dashboard.operation_name=Nom de l'Opération
 dashboard.operation_switch=Basculer
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 93e3b42115..fb229090d4 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -1266,7 +1266,6 @@ last_page=Utolsó
 total=Összesen: %d
 
 dashboard.statistic=Összefoglaló
-dashboard.operations=Karbantartási műveletek
 dashboard.system_status=Rendszer Állapota
 dashboard.operation_name=Művelet Neve
 dashboard.operation_switch=Váltás
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index cc379e8109..9a22995dfb 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -2233,7 +2233,6 @@ last_page=Ultima
 total=Totale: %d
 
 dashboard.statistic=Riepilogo
-dashboard.operations=Operazioni di manutenzione
 dashboard.system_status=Stato del sistema
 dashboard.operation_name=Nome Operazione
 dashboard.operation_switch=Cambia
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index d5c2885f00..eddad35073 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -2719,7 +2719,6 @@ settings=管理設定
 
 dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。
 dashboard.statistic=サマリー
-dashboard.operations=メンテナンス操作
 dashboard.system_status=システム状況
 dashboard.operation_name=操作の名称
 dashboard.operation_switch=切り替え
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 0a2729980b..9a15090012 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -2693,7 +2693,6 @@ settings=Administratora iestatījumi
 
 dashboard.new_version_hint=Ir pieejama Gitea versija %s, pašreizējā versija %s. Papildus informācija par jauno versiju ir pieejama <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">mājas lapā</a>.
 dashboard.statistic=Kopsavilkums
-dashboard.operations=Uzturēšanas darbības
 dashboard.system_status=Sistēmas statuss
 dashboard.operation_name=Darbības nosaukums
 dashboard.operation_switch=Pārslēgt
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 255a3db9fa..6b5122a86f 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -2135,7 +2135,6 @@ last_page=Laatste
 total=Totaal: %d
 
 dashboard.statistic=Overzicht
-dashboard.operations=Onderhoudswerkzaamheden
 dashboard.system_status=Systeemtatus
 dashboard.operation_name=Bewerking naam
 dashboard.operation_switch=Omschakelen
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 1496877fd5..a1d7e95842 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -2010,7 +2010,6 @@ last_page=Ostatnia
 total=Ogółem: %d
 
 dashboard.statistic=Podsumowanie
-dashboard.operations=Operacje konserwacji
 dashboard.system_status=Status strony
 dashboard.operation_name=Nazwa operacji
 dashboard.operation_switch=Przełącz
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 0d1614df3f..45f1c3b3f8 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -2648,7 +2648,6 @@ settings=Configurações de Administrador
 
 dashboard.new_version_hint=Uma nova versão está disponível: %s. Versão atual: %s. Visite <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">o blog</a> para mais informações.
 dashboard.statistic=Resumo
-dashboard.operations=Operações de manutenção
 dashboard.system_status=Status do sistema
 dashboard.operation_name=Nome da operação
 dashboard.operation_switch=Trocar
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index ea80cd7abb..09b9d4e3ce 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -2775,6 +2775,7 @@ teams.invite.by=Convidado(a) por %s
 teams.invite.description=Clique no botão abaixo para se juntar à equipa.
 
 [admin]
+maintenance=Manutenção
 dashboard=Painel de controlo
 self_check=Auto-verificação
 identity_access=Identidade e acesso
@@ -2798,7 +2799,7 @@ settings=Configurações de administração
 
 dashboard.new_version_hint=O Gitea %s está disponível, você está a correr a versão %s. Verifique o <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a> para mais detalhes.
 dashboard.statistic=Resumo
-dashboard.operations=Operações de manutenção
+dashboard.maintenance_operations=Operações de manutenção
 dashboard.system_status=Estado do sistema
 dashboard.operation_name=Nome da operação
 dashboard.operation_switch=Comutar
@@ -3305,6 +3306,7 @@ notices.op=Op.
 notices.delete_success=As notificações do sistema foram eliminadas.
 
 self_check.no_problem_found=Nenhum problema encontrado até agora.
+self_check.startup_warnings=Alertas do arranque:
 self_check.database_collation_mismatch=Supor que a base de dados usa a colação: %s
 self_check.database_collation_case_insensitive=A base de dados está a usar a colação %s, que é insensível à diferença entre maiúsculas e minúsculas. Embora o Gitea possa trabalhar com ela, pode haver alguns casos raros que não funcionem como esperado.
 self_check.database_inconsistent_collation_columns=A base de dados está a usar a colação %s, mas estas colunas estão a usar colações diferentes. Isso poderá causar alguns problemas inesperados.
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 74c4c9c935..818dad1147 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -2634,7 +2634,6 @@ total=Всего: %d
 
 dashboard.new_version_hint=Доступна новая версия Gitea %s, вы используете %s. Более подробную информацию читайте в <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">блоге</a>.
 dashboard.statistic=Статистика
-dashboard.operations=Операции
 dashboard.system_status=Состояние системы
 dashboard.operation_name=Имя операции
 dashboard.operation_switch=Переключить
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 7e82cfe3d6..99559802c5 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -2024,7 +2024,6 @@ last_page=පසුගිය
 total=මුළු: %d
 
 dashboard.statistic=සාරාංශය
-dashboard.operations=නඩත්තු මෙහෙයුම්
 dashboard.system_status=පද්ධතියේ තත්වය
 dashboard.operation_name=මෙහෙයුමේ නම
 dashboard.operation_switch=මාරුවන්න
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index e48d84ff78..9234e9aa58 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -1647,7 +1647,6 @@ last_page=Sista
 total=Totalt: %d
 
 dashboard.statistic=Översikt
-dashboard.operations=Operationer för underhåll
 dashboard.system_status=Status
 dashboard.operation_name=Operationsnamn
 dashboard.operation_switch=Byt till
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 5a5036f87d..119e1ef150 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -2687,7 +2687,6 @@ settings=Yönetici Ayarları
 
 dashboard.new_version_hint=Gitea %s şimdi hazır, %s çalıştırıyorsunuz. Ayrıntılar için <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a>'a bakabilirsiniz.
 dashboard.statistic=Özet
-dashboard.operations=Bakım İşlemleri
 dashboard.system_status=Sistem Durumu
 dashboard.operation_name=İşlem Adı
 dashboard.operation_switch=Geç
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 09561a7902..e8a3acedda 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -2074,7 +2074,6 @@ last_page=Остання
 total=Разом: %d
 
 dashboard.statistic=Підсумок
-dashboard.operations=Технічне обслуговування
 dashboard.system_status=Статус системи
 dashboard.operation_name=Назва операції
 dashboard.operation_switch=Перемкнути
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 406e9ac8f2..01058d48d2 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -2711,7 +2711,6 @@ settings=管理设置
 
 dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">博客</a> 了解详情。
 dashboard.statistic=摘要
-dashboard.operations=维护操作
 dashboard.system_status=系统状态
 dashboard.operation_name=操作名称
 dashboard.operation_switch=开关
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 0511fa44ae..0447a7d8b7 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -2439,7 +2439,6 @@ total=總計:%d
 
 dashboard.new_version_hint=現已推出 Gitea %s,您正在執行 %s。詳情請參閱<a target="_blank" rel="noreferrer" href="https://blog.gitea.io">部落格</a>的說明。
 dashboard.statistic=摘要
-dashboard.operations=維護作業
 dashboard.system_status=系統狀態
 dashboard.operation_name=作業名稱
 dashboard.operation_switch=開關

From b4825670596fe745cebdcc63a8ead4388602d42c Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 2 Apr 2024 16:02:05 +0800
Subject: [PATCH 611/679] Add unique index for project_issue to prevent
 duplicate data (#30190)

Fix #27639
---
 .../project_issue.yml                         |  9 ++++
 models/migrations/migrations.go               |  5 ++
 models/migrations/v1_23/main_test.go          | 14 +++++
 models/migrations/v1_23/v294.go               | 53 +++++++++++++++++++
 models/migrations/v1_23/v294_test.go          | 52 ++++++++++++++++++
 5 files changed, 133 insertions(+)
 create mode 100644 models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml
 create mode 100644 models/migrations/v1_23/main_test.go
 create mode 100644 models/migrations/v1_23/v294.go
 create mode 100644 models/migrations/v1_23/v294_test.go

diff --git a/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml
new file mode 100644
index 0000000000..6feaeb39f0
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml
@@ -0,0 +1,9 @@
+-
+  id: 1
+  project_id: 1
+  issue_id: 1
+
+-
+  id: 2
+  project_id: 1
+  issue_id: 1
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 0daa799ff6..387cd96a53 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/models/migrations/v1_20"
 	"code.gitea.io/gitea/models/migrations/v1_21"
 	"code.gitea.io/gitea/models/migrations/v1_22"
+	"code.gitea.io/gitea/models/migrations/v1_23"
 	"code.gitea.io/gitea/models/migrations/v1_6"
 	"code.gitea.io/gitea/models/migrations/v1_7"
 	"code.gitea.io/gitea/models/migrations/v1_8"
@@ -572,6 +573,10 @@ var migrations = []Migration{
 	NewMigration("Ensure every project has exactly one default column - No Op", noopMigration),
 	// v293 -> v294
 	NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
+
+	// Gitea 1.22.0 ends at 294
+
+	NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go
new file mode 100644
index 0000000000..b7948bd4dd
--- /dev/null
+++ b/models/migrations/v1_23/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+)
+
+func TestMain(m *testing.M) {
+	base.MainTest(m)
+}
diff --git a/models/migrations/v1_23/v294.go b/models/migrations/v1_23/v294.go
new file mode 100644
index 0000000000..f2a54f6d23
--- /dev/null
+++ b/models/migrations/v1_23/v294.go
@@ -0,0 +1,53 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"fmt"
+
+	"xorm.io/xorm"
+	"xorm.io/xorm/schemas"
+)
+
+// AddUniqueIndexForProjectIssue adds unique indexes for project issue table
+func AddUniqueIndexForProjectIssue(x *xorm.Engine) error {
+	// remove possible duplicated records in table project_issue
+	type result struct {
+		IssueID   int64
+		ProjectID int64
+		Cnt       int
+	}
+	var results []result
+	if err := x.Select("issue_id, project_id, count(*) as cnt").
+		Table("project_issue").
+		GroupBy("issue_id, project_id").
+		Having("count(*) > 1").
+		Find(&results); err != nil {
+		return err
+	}
+	for _, r := range results {
+		if x.Dialect().URI().DBType == schemas.MSSQL {
+			if _, err := x.Exec(fmt.Sprintf("delete from project_issue where id in (SELECT top %d id FROM project_issue WHERE issue_id = ? and project_id = ?)", r.Cnt-1), r.IssueID, r.ProjectID); err != nil {
+				return err
+			}
+		} else {
+			var ids []int64
+			if err := x.SQL("SELECT id FROM project_issue WHERE issue_id = ? and project_id = ? limit ?", r.IssueID, r.ProjectID, r.Cnt-1).Find(&ids); err != nil {
+				return err
+			}
+			if _, err := x.Table("project_issue").In("id", ids).Delete(); err != nil {
+				return err
+			}
+		}
+	}
+
+	// add unique index for project_issue table
+	type ProjectIssue struct { //revive:disable-line:exported
+		ID        int64 `xorm:"pk autoincr"`
+		IssueID   int64 `xorm:"INDEX unique(s)"`
+		ProjectID int64 `xorm:"INDEX unique(s)"`
+	}
+
+	return x.Sync(new(ProjectIssue))
+}
diff --git a/models/migrations/v1_23/v294_test.go b/models/migrations/v1_23/v294_test.go
new file mode 100644
index 0000000000..d9a44ad866
--- /dev/null
+++ b/models/migrations/v1_23/v294_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"slices"
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+
+	"github.com/stretchr/testify/assert"
+	"xorm.io/xorm/schemas"
+)
+
+func Test_AddUniqueIndexForProjectIssue(t *testing.T) {
+	type ProjectIssue struct { //revive:disable-line:exported
+		ID        int64 `xorm:"pk autoincr"`
+		IssueID   int64 `xorm:"INDEX"`
+		ProjectID int64 `xorm:"INDEX"`
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	cnt, err := x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 2, cnt)
+
+	assert.NoError(t, AddUniqueIndexForProjectIssue(x))
+
+	cnt, err = x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, cnt)
+
+	tables, err := x.DBMetas()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, len(tables))
+	found := false
+	for _, index := range tables[0].Indexes {
+		if index.Type == schemas.UniqueType {
+			found = true
+			slices.Equal(index.Cols, []string{"project_id", "issue_id"})
+			break
+		}
+	}
+	assert.True(t, found)
+}

From 944c76e78423405a33450eb3d07cd2b772f4a81c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 2 Apr 2024 13:48:07 +0200
Subject: [PATCH 612/679] Fix spacing in issue navbar (#30238)

Create a new `issue-navbar` class specifically for this bar, previous
class used in many places and I thought I had them all removed, but not
this one.

Fixes: https://github.com/go-gitea/gitea/issues/30226
---
 templates/repo/issue/choose.tmpl        | 2 +-
 templates/repo/issue/labels.tmpl        | 2 +-
 templates/repo/issue/milestone_new.tmpl | 2 +-
 web_src/css/modules/navbar.css          | 5 +++++
 4 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl
index a8037482be..38cf9e485f 100644
--- a/templates/repo/issue/choose.tmpl
+++ b/templates/repo/issue/choose.tmpl
@@ -3,7 +3,7 @@
 	{{template "repo/header" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
-		<div class="navbar">
+		<div class="issue-navbar">
 			{{template "repo/issue/navbar" .}}
 		</div>
 		<div class="divider"></div>
diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl
index 6dc7e4ef64..230777efcc 100644
--- a/templates/repo/issue/labels.tmpl
+++ b/templates/repo/issue/labels.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository labels">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="navbar tw-mb-4">
+		<div class="issue-navbar tw-mb-4">
 			{{template "repo/issue/navbar" .}}
 			{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
 				<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl
index 7a56d73ac9..9f32df00e3 100644
--- a/templates/repo/issue/milestone_new.tmpl
+++ b/templates/repo/issue/milestone_new.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository new milestone">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="navbar">
+		<div class="issue-navbar">
 			{{template "repo/issue/navbar" .}}
 			{{if and (or .CanWriteIssues .CanWritePulls) .PageIsEditMilestone}}
 				<div class="ui right floated secondary menu">
diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index f8553d7cf0..d7aa197e02 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -140,3 +140,8 @@
 .secondary-nav {
   background: var(--color-secondary-nav-bg) !important; /* important because of .ui.secondary.menu */
 }
+
+.issue-navbar {
+  display: flex;
+  justify-content: space-between;
+}

From eb505b128c7b9b2459f2a5d20b5740017125178b Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Tue, 2 Apr 2024 17:50:57 +0200
Subject: [PATCH 613/679] Fix missing 0 prefix of GPG key id (#30245)

Fixes #30235

If the key id "front" byte has a single digit, `%X` is missing the 0
prefix.
` 38D1A3EADDBEA9C` instead of
`038D1A3EADDBEA9C`
When using the `IssuerFingerprint` slice `%X` is enough but I changed it
to `%016X` too to be consistent.
---
 models/asymkey/gpg_key_commit_verification.go |  8 +-------
 models/asymkey/gpg_key_common.go              | 10 ++++++++++
 models/asymkey/gpg_key_test.go                | 12 ++++++++++++
 3 files changed, 23 insertions(+), 7 deletions(-)

diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 83fbab5d36..06ac31bc6f 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -139,13 +139,7 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific
 		}
 	}
 
-	keyID := ""
-	if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
-		keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
-	}
-	if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
-		keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
-	}
+	keyID := tryGetKeyIDFromSignature(sig)
 	defaultReason := NoKeyFound
 
 	// First check if the sig has a keyID and if so just look at that
diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go
index b02be2851a..9c015582f1 100644
--- a/models/asymkey/gpg_key_common.go
+++ b/models/asymkey/gpg_key_common.go
@@ -134,3 +134,13 @@ func extractSignature(s string) (*packet.Signature, error) {
 	}
 	return sig, nil
 }
+
+func tryGetKeyIDFromSignature(sig *packet.Signature) string {
+	if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
+		return fmt.Sprintf("%016X", *sig.IssuerKeyId)
+	}
+	if sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
+		return fmt.Sprintf("%016X", sig.IssuerFingerprint[12:20])
+	}
+	return ""
+}
diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go
index dee74bc281..d3fbb01d82 100644
--- a/models/asymkey/gpg_key_test.go
+++ b/models/asymkey/gpg_key_test.go
@@ -11,7 +11,9 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
 
+	"github.com/keybase/go-crypto/openpgp/packet"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -391,3 +393,13 @@ epiDVQ==
 		assert.Equal(t, time.Unix(1586105389, 0), expire)
 	}
 }
+
+func TestTryGetKeyIDFromSignature(t *testing.T) {
+	assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{}))
+	assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
+		IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)),
+	}))
+	assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
+		IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
+	}))
+}

From ca5c895efb91d2c2f17a83460e1753101c6f6bb1 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 3 Apr 2024 01:48:27 +0800
Subject: [PATCH 614/679] Render embedded code preview by permlink in markdown
 (#30234)

The permlink in markdown will be rendered as a code preview block, like GitHub

Co-authored-by: silverwind <me@silverwind.io>
---
 modules/charset/escape_test.go                |   6 +-
 modules/csv/csv_test.go                       |   4 +-
 modules/indexer/code/search.go                |  16 +--
 modules/markup/html.go                        |   1 +
 modules/markup/html_codepreview.go            |  92 ++++++++++++++
 modules/markup/html_codepreview_test.go       |  34 +++++
 modules/markup/renderer.go                    |   3 +
 modules/markup/sanitizer.go                   |  15 +++
 modules/translation/mock.go                   |  18 ++-
 options/locale/locale_en-US.ini               |   2 +
 routers/web/repo/search.go                    |   2 +-
 routers/web/repo/wiki_test.go                 |   2 +-
 services/contexttest/context_tests.go         |   1 +
 services/markup/main_test.go                  |   2 +-
 services/markup/processorhelper.go            |   2 +
 .../markup/processorhelper_codepreview.go     | 117 ++++++++++++++++++
 .../processorhelper_codepreview_test.go       |  83 +++++++++++++
 templates/base/markup_codepreview.tmpl        |  25 ++++
 web_src/css/base.css                          |   5 +-
 web_src/css/index.css                         |   1 +
 web_src/css/markup/codepreview.css            |  36 ++++++
 web_src/css/markup/content.css                |   4 +-
 22 files changed, 450 insertions(+), 21 deletions(-)
 create mode 100644 modules/markup/html_codepreview.go
 create mode 100644 modules/markup/html_codepreview_test.go
 create mode 100644 services/markup/processorhelper_codepreview.go
 create mode 100644 services/markup/processorhelper_codepreview_test.go
 create mode 100644 templates/base/markup_codepreview.tmpl
 create mode 100644 web_src/css/markup/codepreview.css

diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go
index a353ced631..9d796a0c18 100644
--- a/modules/charset/escape_test.go
+++ b/modules/charset/escape_test.go
@@ -4,6 +4,7 @@
 package charset
 
 import (
+	"regexp"
 	"strings"
 	"testing"
 
@@ -156,13 +157,16 @@ func TestEscapeControlReader(t *testing.T) {
 		tests = append(tests, test)
 	}
 
+	re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			output := &strings.Builder{}
 			status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{})
 			assert.NoError(t, err)
 			assert.Equal(t, tt.status, *status)
-			assert.Equal(t, tt.result, output.String())
+			outStr := output.String()
+			outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character")
+			assert.Equal(t, tt.result, outStr)
 		})
 	}
 }
diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go
index f6e782a5a4..3ddb47acbb 100644
--- a/modules/csv/csv_test.go
+++ b/modules/csv/csv_test.go
@@ -561,14 +561,14 @@ func TestFormatError(t *testing.T) {
 			err: &csv.ParseError{
 				Err: csv.ErrFieldCount,
 			},
-			expectedMessage: "repo.error.csv.invalid_field_count",
+			expectedMessage: "repo.error.csv.invalid_field_count:0",
 			expectsError:    false,
 		},
 		{
 			err: &csv.ParseError{
 				Err: csv.ErrBareQuote,
 			},
-			expectedMessage: "repo.error.csv.unexpected",
+			expectedMessage: "repo.error.csv.unexpected:0,0",
 			expectsError:    false,
 		},
 		{
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 5f35e8073b..74c957dde6 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -22,7 +22,7 @@ type Result struct {
 	UpdatedUnix timeutil.TimeStamp
 	Language    string
 	Color       string
-	Lines       []ResultLine
+	Lines       []*ResultLine
 }
 
 type ResultLine struct {
@@ -70,16 +70,18 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
 	return nil
 }
 
-func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine {
+func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine {
 	// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
-	hl, _ := highlight.Code(filename, "", code)
+	hl, _ := highlight.Code(filename, language, code)
 	highlightedLines := strings.Split(string(hl), "\n")
 
 	// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
-	lines := make([]ResultLine, min(len(highlightedLines), len(lineNums)))
+	lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums)))
 	for i := 0; i < len(lines); i++ {
-		lines[i].Num = lineNums[i]
-		lines[i].FormattedContent = template.HTML(highlightedLines[i])
+		lines[i] = &ResultLine{
+			Num:              lineNums[i],
+			FormattedContent: template.HTML(highlightedLines[i]),
+		}
 	}
 	return lines
 }
@@ -122,7 +124,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 		UpdatedUnix: result.UpdatedUnix,
 		Language:    result.Language,
 		Color:       result.Color,
-		Lines:       HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()),
+		Lines:       HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
 	}, nil
 }
 
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 21bd6206e0..56aa1cb49c 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
 var defaultProcessors = []processor{
 	fullIssuePatternProcessor,
 	comparePatternProcessor,
+	codePreviewPatternProcessor,
 	fullHashPatternProcessor,
 	shortLinkProcessor,
 	linkProcessor,
diff --git a/modules/markup/html_codepreview.go b/modules/markup/html_codepreview.go
new file mode 100644
index 0000000000..d9da24ea34
--- /dev/null
+++ b/modules/markup/html_codepreview.go
@@ -0,0 +1,92 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"html/template"
+	"net/url"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/httplib"
+	"code.gitea.io/gitea/modules/log"
+
+	"golang.org/x/net/html"
+)
+
+// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
+var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
+
+type RenderCodePreviewOptions struct {
+	FullURL   string
+	OwnerName string
+	RepoName  string
+	CommitID  string
+	FilePath  string
+
+	LineStart, LineStop int
+}
+
+func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
+	m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
+	if m == nil {
+		return 0, 0, "", nil
+	}
+
+	opts := RenderCodePreviewOptions{
+		FullURL:   node.Data[m[0]:m[1]],
+		OwnerName: node.Data[m[2]:m[3]],
+		RepoName:  node.Data[m[4]:m[5]],
+		CommitID:  node.Data[m[6]:m[7]],
+		FilePath:  node.Data[m[8]:m[9]],
+	}
+	if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
+		return 0, 0, "", nil
+	}
+	u, err := url.Parse(opts.FilePath)
+	if err != nil {
+		return 0, 0, "", err
+	}
+	opts.FilePath = strings.TrimPrefix(u.Path, "/")
+
+	lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
+	lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
+	lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
+	opts.LineStart, opts.LineStop = lineStart, lineStop
+	h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
+	return m[0], m[1], h, err
+}
+
+func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
+	for node != nil {
+		if node.Type != html.TextNode {
+			node = node.NextSibling
+			continue
+		}
+		urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
+		if err != nil || h == "" {
+			if err != nil {
+				log.Error("Unable to render code preview: %v", err)
+			}
+			node = node.NextSibling
+			continue
+		}
+		next := node.NextSibling
+		textBefore := node.Data[:urlPosStart]
+		textAfter := node.Data[urlPosEnd:]
+		// "textBefore" could be empty if there is only a URL in the text node, then an empty node (p, or li) will be left here.
+		// However, the empty node can't be simply removed, because:
+		// 1. the following processors will still try to access it (need to double-check undefined behaviors)
+		// 2. the new node is inserted as "<p>{TextBefore}<div NewNode/>{TextAfter}</p>" (the parent could also be "li")
+		//    then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
+		//    so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
+		node.Data = textBefore
+		node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
+		if textAfter != "" {
+			node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
+		}
+		node = next
+	}
+}
diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go
new file mode 100644
index 0000000000..d33630d040
--- /dev/null
+++ b/modules/markup/html_codepreview_test.go
@@ -0,0 +1,34 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup_test
+
+import (
+	"context"
+	"html/template"
+	"strings"
+	"testing"
+
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRenderCodePreview(t *testing.T) {
+	markup.Init(&markup.ProcessorHelper{
+		RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+			return "<div>code preview</div>", nil
+		},
+	})
+	test := func(input, expected string) {
+		buffer, err := markup.RenderString(&markup.RenderContext{
+			Ctx:  git.DefaultContext,
+			Type: "markdown",
+		}, input)
+		assert.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+	}
+	test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
+	test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
+}
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 0f0bf55740..005fcc278b 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"html/template"
 	"io"
 	"net/url"
 	"path/filepath"
@@ -33,6 +34,8 @@ type ProcessorHelper struct {
 	IsUsernameMentionable func(ctx context.Context, username string) bool
 
 	ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
+
+	RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
 }
 
 var DefaultProcessorHelper ProcessorHelper
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 79a2ba0dfb..77fbdf4520 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -60,6 +60,21 @@ func createDefaultPolicy() *bluemonday.Policy {
 	// For JS code copy and Mermaid loading state
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
 
+	// For code preview
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
+	policy.AllowAttrs("data-line-number").OnElements("span")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("code")
+
+	// For code preview (unicode escape)
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
+	policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
+
 	// For color preview
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
 
diff --git a/modules/translation/mock.go b/modules/translation/mock.go
index 18fbc1044a..f457271ea5 100644
--- a/modules/translation/mock.go
+++ b/modules/translation/mock.go
@@ -6,6 +6,7 @@ package translation
 import (
 	"fmt"
 	"html/template"
+	"strings"
 )
 
 // MockLocale provides a mocked locale without any translations
@@ -19,18 +20,25 @@ func (l MockLocale) Language() string {
 	return "en"
 }
 
-func (l MockLocale) TrString(s string, _ ...any) string {
-	return s
+func (l MockLocale) TrString(s string, args ...any) string {
+	return sprintAny(s, args...)
 }
 
-func (l MockLocale) Tr(s string, a ...any) template.HTML {
-	return template.HTML(s)
+func (l MockLocale) Tr(s string, args ...any) template.HTML {
+	return template.HTML(sprintAny(s, args...))
 }
 
 func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
-	return template.HTML(key1)
+	return template.HTML(sprintAny(key1, args...))
 }
 
 func (l MockLocale) PrettyNumber(v any) string {
 	return fmt.Sprint(v)
 }
+
+func sprintAny(s string, args ...any) string {
+	if len(args) == 0 {
+		return s
+	}
+	return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...)
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 39b9855186..0a3d12d7a4 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1233,6 +1233,8 @@ file_view_rendered = View Rendered
 file_view_raw = View Raw
 file_permalink = Permalink
 file_too_large = The file is too large to be shown.
+code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s
+code_preview_line_in = Line %[1]d in %[2]s
 invisible_runes_header = `This file contains invisible Unicode characters`
 invisible_runes_description = `This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.`
 ambiguous_runes_header = `This file contains ambiguous Unicode characters`
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 9d65427b8f..46f0208453 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -81,7 +81,7 @@ func Search(ctx *context.Context) {
 				// UpdatedUnix: not supported yet
 				// Language:    not supported yet
 				// Color:       not supported yet
-				Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, strings.Join(r.LineCodes, "\n")),
+				Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
 			})
 		}
 	}
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 52e216e6a0..8b5207f9d9 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -145,7 +145,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
 	})
 	NewWikiPost(ctx)
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-	assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
+	assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
 	assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
 }
 
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index d3e6de7efe..3064c56590 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -63,6 +63,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
 	base.Locale = &translation.MockLocale{}
 
 	ctx := context.NewWebContext(base, opt.Render, nil)
+	ctx.AppendContextValue(context.WebContextKey, ctx)
 	ctx.PageData = map[string]any{}
 	ctx.Data["PageStartTime"] = time.Now()
 	chiCtx := chi.NewRouteContext()
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
index 89fe3e7e34..5553ebc058 100644
--- a/services/markup/main_test.go
+++ b/services/markup/main_test.go
@@ -11,6 +11,6 @@ import (
 
 func TestMain(m *testing.M) {
 	unittest.MainTest(m, &unittest.TestOptions{
-		FixtureFiles: []string{"user.yml"},
+		FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
 	})
 }
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index a4378678a0..68487fb8db 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -14,6 +14,8 @@ import (
 func ProcessorHelper() *markup.ProcessorHelper {
 	return &markup.ProcessorHelper{
 		ElementDir: "auto", // set dir="auto" for necessary (eg: <p>, <h?>, etc) tags
+
+		RenderRepoFileCodePreview: renderRepoFileCodePreview,
 		IsUsernameMentionable: func(ctx context.Context, username string) bool {
 			mentionedUser, err := user.GetUserByName(ctx, username)
 			if err != nil {
diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/processorhelper_codepreview.go
new file mode 100644
index 0000000000..ef95046128
--- /dev/null
+++ b/services/markup/processorhelper_codepreview.go
@@ -0,0 +1,117 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"html/template"
+	"strings"
+
+	"code.gitea.io/gitea/models/perm/access"
+	"code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/modules/charset"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/indexer/code"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/setting"
+	gitea_context "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/repository/files"
+)
+
+func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+	opts.LineStop = max(opts.LineStop, opts.LineStart)
+	lineCount := opts.LineStop - opts.LineStart + 1
+	if lineCount <= 0 || lineCount > 140 /* GitHub at most show 140 lines */ {
+		lineCount = 10
+		opts.LineStop = opts.LineStart + lineCount
+	}
+
+	dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
+	if err != nil {
+		return "", err
+	}
+
+	webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
+	if !ok {
+		return "", fmt.Errorf("context is not a web context")
+	}
+	doer := webCtx.Doer
+
+	perms, err := access.GetUserRepoPermission(ctx, dbRepo, doer)
+	if err != nil {
+		return "", err
+	}
+	if !perms.CanRead(unit.TypeCode) {
+		return "", fmt.Errorf("no permission")
+	}
+
+	gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
+	if err != nil {
+		return "", err
+	}
+	defer gitRepo.Close()
+
+	commit, err := gitRepo.GetCommit(opts.CommitID)
+	if err != nil {
+		return "", err
+	}
+
+	language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath)
+	blob, err := commit.GetBlobByPath(opts.FilePath)
+	if err != nil {
+		return "", err
+	}
+
+	if blob.Size() > setting.UI.MaxDisplayFileSize {
+		return "", fmt.Errorf("file is too large")
+	}
+
+	dataRc, err := blob.DataAsync()
+	if err != nil {
+		return "", err
+	}
+	defer dataRc.Close()
+
+	reader := bufio.NewReader(dataRc)
+	for i := 1; i < opts.LineStart; i++ {
+		if _, err = reader.ReadBytes('\n'); err != nil {
+			return "", err
+		}
+	}
+
+	lineNums := make([]int, 0, lineCount)
+	lineCodes := make([]string, 0, lineCount)
+	for i := opts.LineStart; i <= opts.LineStop; i++ {
+		if line, err := reader.ReadString('\n'); err != nil && line == "" {
+			break
+		} else {
+			lineNums = append(lineNums, i)
+			lineCodes = append(lineCodes, line)
+		}
+	}
+	realLineStop := max(opts.LineStart, opts.LineStart+len(lineNums)-1)
+	highlightLines := code.HighlightSearchResultCode(opts.FilePath, language, lineNums, strings.Join(lineCodes, ""))
+
+	escapeStatus := &charset.EscapeStatus{}
+	lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
+	for i, hl := range highlightLines {
+		lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.RuneNBSP)
+		escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
+	}
+
+	return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{
+		"FullURL":          opts.FullURL,
+		"FilePath":         opts.FilePath,
+		"LineStart":        opts.LineStart,
+		"LineStop":         realLineStop,
+		"RepoLink":         dbRepo.Link(),
+		"CommitID":         opts.CommitID,
+		"HighlightLines":   highlightLines,
+		"EscapeStatus":     escapeStatus,
+		"LineEscapeStatus": lineEscapeStatus,
+	})
+}
diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/processorhelper_codepreview_test.go
new file mode 100644
index 0000000000..01db792925
--- /dev/null
+++ b/services/markup/processorhelper_codepreview_test.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/contexttest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProcessorHelperCodePreview(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	htm, err := renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+		FullURL:   "http://full",
+		OwnerName: "user2",
+		RepoName:  "repo1",
+		CommitID:  "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+		FilePath:  "/README.md",
+		LineStart: 1,
+		LineStop:  2,
+	})
+	assert.NoError(t, err)
+	assert.Equal(t, `<div class="code-preview-container file-content">
+	<div class="code-preview-header">
+		<a href="http://full" class="muted" rel="nofollow">/README.md</a>
+		repo.code_preview_line_from_to:1,2,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
+	</div>
+	<table class="file-view">
+		<tbody><tr>
+				<td class="lines-num"><span data-line-number="1"></span></td>
+				<td class="lines-code chroma"><code class="code-inner"><span class="gh"># repo1</code></td>
+			</tr><tr>
+				<td class="lines-num"><span data-line-number="2"></span></td>
+				<td class="lines-code chroma"><code class="code-inner"></span><span class="gh"></span></code></td>
+			</tr></tbody>
+	</table>
+</div>
+`, string(htm))
+
+	ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	htm, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+		FullURL:   "http://full",
+		OwnerName: "user2",
+		RepoName:  "repo1",
+		CommitID:  "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+		FilePath:  "/README.md",
+		LineStart: 1,
+	})
+	assert.NoError(t, err)
+	assert.Equal(t, `<div class="code-preview-container file-content">
+	<div class="code-preview-header">
+		<a href="http://full" class="muted" rel="nofollow">/README.md</a>
+		repo.code_preview_line_in:1,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
+	</div>
+	<table class="file-view">
+		<tbody><tr>
+				<td class="lines-num"><span data-line-number="1"></span></td>
+				<td class="lines-code chroma"><code class="code-inner"><span class="gh"># repo1</code></td>
+			</tr></tbody>
+	</table>
+</div>
+`, string(htm))
+
+	ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	_, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+		FullURL:   "http://full",
+		OwnerName: "user15",
+		RepoName:  "big_test_private_1",
+		CommitID:  "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+		FilePath:  "/README.md",
+		LineStart: 1,
+		LineStop:  10,
+	})
+	assert.ErrorContains(t, err, "no permission")
+}
diff --git a/templates/base/markup_codepreview.tmpl b/templates/base/markup_codepreview.tmpl
new file mode 100644
index 0000000000..c65ab28406
--- /dev/null
+++ b/templates/base/markup_codepreview.tmpl
@@ -0,0 +1,25 @@
+<div class="code-preview-container file-content">
+	<div class="code-preview-header">
+		<a href="{{.FullURL}}" class="muted" rel="nofollow">{{.FilePath}}</a>
+		{{$link := HTMLFormat `<a href="%s/src/commit/%s" rel="nofollow">%s</a>` .RepoLink .CommitID (.CommitID | ShortSha) -}}
+		{{- if eq .LineStart .LineStop -}}
+			{{ctx.Locale.Tr "repo.code_preview_line_in" .LineStart $link}}
+		{{- else -}}
+			{{ctx.Locale.Tr "repo.code_preview_line_from_to" .LineStart .LineStop $link}}
+		{{- end}}
+	</div>
+	<table class="file-view">
+		<tbody>
+			{{- range $idx, $line := .HighlightLines -}}
+			<tr>
+				<td class="lines-num"><span data-line-number="{{$line.Num}}"></span></td>
+				{{- if $.EscapeStatus.Escaped -}}
+					{{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}}
+					<td class="lines-escape">{{if $lineEscapeStatus.Escaped}}<a href="#" class="toggle-escape-button btn interact-bg" title="{{if $lineEscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if $lineEscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></a>{{end}}</td>
+				{{- end}}
+				<td class="lines-code chroma"><code class="code-inner">{{$line.FormattedContent}}</code></td>
+			</tr>
+			{{- end -}}
+		</tbody>
+	</table>
+</div>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 96c90ee692..05ddba3223 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1186,10 +1186,13 @@ overflow-menu .ui.label {
   content: attr(data-line-number);
   line-height: 20px !important;
   padding: 0 10px;
-  cursor: pointer;
   display: block;
 }
 
+.code-view .lines-num span::after {
+  cursor: pointer;
+}
+
 .lines-type-marker {
   vertical-align: top;
 }
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 40b1d3c881..7be8065dc7 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -41,6 +41,7 @@
 
 @import "./markup/content.css";
 @import "./markup/codecopy.css";
+@import "./markup/codepreview.css";
 @import "./markup/asciicast.css";
 
 @import "./chroma/base.css";
diff --git a/web_src/css/markup/codepreview.css b/web_src/css/markup/codepreview.css
new file mode 100644
index 0000000000..9219544993
--- /dev/null
+++ b/web_src/css/markup/codepreview.css
@@ -0,0 +1,36 @@
+.markup .code-preview-container {
+  border: 1px solid var(--color-secondary);
+  border-radius: var(--border-radius);
+  margin: 0.25em 0;
+}
+
+.markup .code-preview-container .code-preview-header {
+  border-bottom: 1px solid var(--color-secondary);
+  padding: 0.5em;
+  font-size: 12px;
+}
+
+.markup .code-preview-container table {
+  width: 100%;
+  max-height: 100px;
+  overflow-y: auto;
+  margin: 0; /* override ".markup table {margin}" */
+}
+
+/* workaround to hide empty p before container - more details are in "html_codepreview.go" */
+.markup p:empty:has(+ .code-preview-container) {
+  display: none;
+}
+
+/* override the polluted styles from the content.css: ".markup table ..." */
+.markup .code-preview-container table tr {
+  border: 0 !important;
+}
+.markup .code-preview-container table th,
+.markup .code-preview-container table td {
+  border: 0 !important;
+  padding: 0 0 0 5px !important;
+}
+.markup .code-preview-container table tr:nth-child(2n) {
+  background: none !important;
+}
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index 5eeef078a5..376d3030c7 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -382,7 +382,7 @@
   text-align: center;
 }
 
-.markup span.align-center span img
+.markup span.align-center span img,
 .markup span.align-center span video {
   margin: 0 auto;
   text-align: center;
@@ -432,7 +432,7 @@
   text-align: right;
 }
 
-.markup code,
+.markup code:not(.code-inner),
 .markup tt {
   padding: 0.2em 0.4em;
   margin: 0;

From e006451ab1509f8d6d43c5974387c05b26517392 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Tiago?=
 <114936010+jmlt2002@users.noreply.github.com>
Date: Tue, 2 Apr 2024 19:15:40 +0100
Subject: [PATCH 615/679] Fixes #27605: inline math blocks can't be
 preceeded/followed by alphanumerical characters (#30175)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Inline math blocks couldn't be preceeded or succeeded by
alphanumerical characters due to changes introduced in PR #21171.
Removed the condition that caused this (precedingCharacter condition)
and added a new exit condition of the for-loop that checks if a specific
'$' was escaped using '\' so that the math expression can be rendered as
intended.
- Additionally this PR fixes another bug where math blocks of the type
'$xyz$abc$' where the dollar sign was not escaped by the user, generated
an error (shown in the screenshots below)
- Altered the tests to accomodate for the changes

Former behaviour (from try.gitea.io):

![image](https://github.com/go-gitea/gitea/assets/114936010/8f0cbb21-321d-451c-b871-c67a8e1e9235)

Fixed behaviour (from my local build):

![image](https://github.com/go-gitea/gitea/assets/114936010/5c22687c-6f11-4407-b5e7-c14b838bc20d)

(Edit) Source code for the README.md file:
```
$x$ -$x$ $x$-

a$xa$ $xa$a 1$xb$ $xb$1

$a a$b b$

a$b $a a$b b$

$a a\$b b$
```

---------

Signed-off-by: João Tiago <joao.leal.tintas@tecnico.ulisboa.pt>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 modules/markup/markdown/markdown_test.go      | 20 +++++++++++++++++--
 modules/markup/markdown/math/inline_parser.go | 18 ++++++++++++-----
 2 files changed, 31 insertions(+), 7 deletions(-)

diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index c664758a27..a9c9024982 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -511,9 +511,17 @@ func TestMathBlock(t *testing.T) {
 			`\(a\) \(b\)`,
 			`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl,
 		},
+		{
+			`$a$.`,
+			`<p><code class="language-math is-loading">a</code>.</p>` + nl,
+		},
+		{
+			`.$a$`,
+			`<p>.$a$</p>` + nl,
+		},
 		{
 			`$a a$b b$`,
-			`<p><code class="language-math is-loading">a a$b b</code></p>` + nl,
+			`<p>$a a$b b$</p>` + nl,
 		},
 		{
 			`a a$b b`,
@@ -521,7 +529,15 @@ func TestMathBlock(t *testing.T) {
 		},
 		{
 			`a$b $a a$b b$`,
-			`<p>a$b <code class="language-math is-loading">a a$b b</code></p>` + nl,
+			`<p>a$b $a a$b b$</p>` + nl,
+		},
+		{
+			"a$x$",
+			`<p>a$x$</p>` + nl,
+		},
+		{
+			"$x$a",
+			`<p>$x$a</p>` + nl,
 		},
 		{
 			"$$a$$",
diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go
index 0ac25c2b2a..862234e69b 100644
--- a/modules/markup/markdown/math/inline_parser.go
+++ b/modules/markup/markdown/math/inline_parser.go
@@ -41,9 +41,12 @@ func (parser *inlineParser) Trigger() []byte {
 	return parser.start[0:1]
 }
 
+func isPunctuation(b byte) bool {
+	return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
+}
+
 func isAlphanumeric(b byte) bool {
-	// Github only cares about 0-9A-Za-z
-	return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
+	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
 }
 
 // Parse parses the current line and returns a result of parsing.
@@ -56,7 +59,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
 	}
 
 	precedingCharacter := block.PrecendingCharacter()
-	if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) {
+	if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
 		// need to exclude things like `a$` from being considered a start
 		return nil
 	}
@@ -75,14 +78,19 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
 		ender += pos
 
 		// Now we want to check the character at the end of our parser section
-		// that is ender + len(parser.end)
+		// that is ender + len(parser.end) and check if char before ender is '\'
 		pos = ender + len(parser.end)
 		if len(line) <= pos {
 			break
 		}
-		if !isAlphanumeric(line[pos]) {
+		suceedingCharacter := line[pos]
+		if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') {
+			return nil
+		}
+		if line[ender-1] != '\\' {
 			break
 		}
+
 		// move the pointer onwards
 		ender += len(parser.end)
 	}

From 6f4e2e79ffd1a244e9c266db19840a5bfda09119 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 3 Apr 2024 03:44:15 +0200
Subject: [PATCH 616/679] Show 12 lines in markup code preview (#30255)

Show up to 12 lines instead of previous 5.
---
 web_src/css/markup/codepreview.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/css/markup/codepreview.css b/web_src/css/markup/codepreview.css
index 9219544993..c9d19f5cc8 100644
--- a/web_src/css/markup/codepreview.css
+++ b/web_src/css/markup/codepreview.css
@@ -12,7 +12,7 @@
 
 .markup .code-preview-container table {
   width: 100%;
-  max-height: 100px;
+  max-height: 240px; /* 12 lines at 20px per line */
   overflow-y: auto;
   margin: 0; /* override ".markup table {margin}" */
 }

From b28d3a4218b1338ce6f683d11993081b722bae0a Mon Sep 17 00:00:00 2001
From: scribblemaniac <scribblemaniac@users.noreply.github.com>
Date: Tue, 2 Apr 2024 19:47:13 -0600
Subject: [PATCH 617/679] Add -u git to docs when using docker exec with root
 installation (#29314)

This fixes a minor issue in the documentation for SSH Container
Passthrough for non-rootless installs. The non-rootless Dockerfile and
docker-compose do not set `USER`/`user` instructions so `docker exec`
will run as root by default. While running as root, gitea commands will
refuse to execute, breaking these approaches. For containers built with
the rootless instructions, `docker exec` will run as git by default so
this is not necessary in that case.

This issue was already discussed in #19065, but it does not appear this
part of the issue was ever added to the documentation.
---
 docs/content/installation/with-docker.en-us.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/content/installation/with-docker.en-us.md b/docs/content/installation/with-docker.en-us.md
index e67f5bccb2..e8a80f7c96 100644
--- a/docs/content/installation/with-docker.en-us.md
+++ b/docs/content/installation/with-docker.en-us.md
@@ -545,7 +545,7 @@ In this option, the idea is that the host SSH uses an `AuthorizedKeysCommand` in
   ```bash
   cat <<"EOF" | sudo tee /home/git/docker-shell
   #!/bin/sh
-  /usr/bin/docker exec -i --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
+  /usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
   EOF
   sudo chmod +x /home/git/docker-shell
   sudo usermod -s /home/git/docker-shell git
@@ -560,7 +560,7 @@ Add the following block to `/etc/ssh/sshd_config`, on the host:
 ```bash
 Match User git
   AuthorizedKeysCommandUser git
-  AuthorizedKeysCommand /usr/bin/docker exec -i gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
+  AuthorizedKeysCommand /usr/bin/docker exec -i -u git gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
 ```
 
 (From 1.16.0 you will not need to set the `-c /data/gitea/conf/app.ini` option.)

From 654cfd1dfbd3f3f1d94addee50b6fe2b018a49c3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 3 Apr 2024 10:16:46 +0800
Subject: [PATCH 618/679] Refactor "dump" sub-command (#30240)

Major changes:

* Move some functions like "addReader" / "isSubDir" /
"addRecursiveExclude" to a separate package, and add tests
* Clarify the filename&dump type logic and add tests
* Clarify the logger behavior and remove FIXME comments

Co-authored-by: Giteabot <teabot@gitea.io>
---
 cmd/dump.go                   | 296 ++++++++--------------------------
 modules/dump/dumper.go        | 174 ++++++++++++++++++++
 modules/dump/dumper_test.go   | 113 +++++++++++++
 modules/setting/log.go        |   9 ++
 modules/timeutil/timestamp.go |   3 +-
 modules/util/util.go          |   8 +
 6 files changed, 374 insertions(+), 229 deletions(-)
 create mode 100644 modules/dump/dumper.go
 create mode 100644 modules/dump/dumper_test.go

diff --git a/cmd/dump.go b/cmd/dump.go
index 69ecdcec12..da0a51d9ce 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -6,14 +6,13 @@ package cmd
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"path"
 	"path/filepath"
 	"strings"
-	"time"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/dump"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -25,89 +24,17 @@ import (
 	"github.com/urfave/cli/v2"
 )
 
-func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
-	if verbose {
-		log.Info("Adding file %s", customName)
-	}
-
-	return w.Write(archiver.File{
-		FileInfo: archiver.FileInfo{
-			FileInfo:   info,
-			CustomName: customName,
-		},
-		ReadCloser: r,
-	})
-}
-
-func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
-	file, err := os.Open(absPath)
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-	fileInfo, err := file.Stat()
-	if err != nil {
-		return err
-	}
-
-	return addReader(w, file, fileInfo, filePath, verbose)
-}
-
-func isSubdir(upper, lower string) (bool, error) {
-	if relPath, err := filepath.Rel(upper, lower); err != nil {
-		return false, err
-	} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
-		return true, nil
-	}
-	return false, nil
-}
-
-type outputType struct {
-	Enum     []string
-	Default  string
-	selected string
-}
-
-func (o outputType) Join() string {
-	return strings.Join(o.Enum, ", ")
-}
-
-func (o *outputType) Set(value string) error {
-	for _, enum := range o.Enum {
-		if enum == value {
-			o.selected = value
-			return nil
-		}
-	}
-
-	return fmt.Errorf("allowed values are %s", o.Join())
-}
-
-func (o outputType) String() string {
-	if o.selected == "" {
-		return o.Default
-	}
-	return o.selected
-}
-
-var outputTypeEnum = &outputType{
-	Enum:    []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
-	Default: "zip",
-}
-
 // CmdDump represents the available dump sub-command.
 var CmdDump = &cli.Command{
-	Name:  "dump",
-	Usage: "Dump Gitea files and database",
-	Description: `Dump compresses all related files and database into zip file.
-It can be used for backup and capture Gitea server image to send to maintainer`,
-	Action: runDump,
+	Name:        "dump",
+	Usage:       "Dump Gitea files and database",
+	Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
+	Action:      runDump,
 	Flags: []cli.Flag{
 		&cli.StringFlag{
 			Name:    "file",
 			Aliases: []string{"f"},
-			Value:   fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
-			Usage:   "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
+			Usage:   `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
 		},
 		&cli.BoolFlag{
 			Name:    "verbose",
@@ -160,64 +87,52 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
 			Name:  "skip-index",
 			Usage: "Skip bleve index data",
 		},
-		&cli.GenericFlag{
+		&cli.StringFlag{
 			Name:  "type",
-			Value: outputTypeEnum,
-			Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
+			Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
 		},
 	},
 }
 
 func fatal(format string, args ...any) {
-	fmt.Fprintf(os.Stderr, format+"\n", args...)
 	log.Fatal(format, args...)
 }
 
 func runDump(ctx *cli.Context) error {
-	var file *os.File
-	fileName := ctx.String("file")
-	outType := ctx.String("type")
-	if fileName == "-" {
-		file = os.Stdout
-		setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
-	} else {
-		for _, suffix := range outputTypeEnum.Enum {
-			if strings.HasSuffix(fileName, "."+suffix) {
-				fileName = strings.TrimSuffix(fileName, "."+suffix)
-				break
-			}
-		}
-		fileName += "." + outType
-	}
 	setting.MustInstalled()
 
-	// make sure we are logging to the console no matter what the configuration tells us do to
-	// FIXME: don't use CfgProvider directly
-	if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil {
-		fatal("Setting logging mode to console failed: %v", err)
-	}
-	if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil {
-		fatal("Setting console logger to stderr failed: %v", err)
-	}
-
-	// Set loglevel to Warn if quiet-mode is requested
-	if ctx.Bool("quiet") {
-		if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil {
-			fatal("Setting console log-level failed: %v", err)
-		}
-	}
-
-	if !setting.InstallLock {
-		log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
-		return fmt.Errorf("gitea is not initialized")
-	}
-	setting.LoadSettings() // cannot access session settings otherwise
-
+	quite := ctx.Bool("quiet")
 	verbose := ctx.Bool("verbose")
-	if verbose && ctx.Bool("quiet") {
-		return fmt.Errorf("--quiet and --verbose cannot both be set")
+	if verbose && quite {
+		fatal("Option --quiet and --verbose cannot both be set")
 	}
 
+	// outFileName is either "-" or a file name (will be made absolute)
+	outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
+	if outType == "" {
+		fatal("Invalid output type")
+	}
+
+	outFile := os.Stdout
+	if outFileName != "-" {
+		var err error
+		if outFileName, err = filepath.Abs(outFileName); err != nil {
+			fatal("Unable to get absolute path of dump file: %v", err)
+		}
+		if exist, _ := util.IsExist(outFileName); exist {
+			fatal("Dump file %q exists", outFileName)
+		}
+		if outFile, err = os.Create(outFileName); err != nil {
+			fatal("Unable to create dump file %q: %v", outFileName, err)
+		}
+		defer outFile.Close()
+	}
+
+	setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
+
+	setting.DisableLoggerInit()
+	setting.LoadSettings() // cannot access session settings otherwise
+
 	stdCtx, cancel := installSignals()
 	defer cancel()
 
@@ -226,44 +141,32 @@ func runDump(ctx *cli.Context) error {
 		return err
 	}
 
-	if err := storage.Init(); err != nil {
+	if err = storage.Init(); err != nil {
 		return err
 	}
 
-	if file == nil {
-		file, err = os.Create(fileName)
-		if err != nil {
-			fatal("Unable to open %s: %v", fileName, err)
-		}
-	}
-	defer file.Close()
-
-	absFileName, err := filepath.Abs(fileName)
-	if err != nil {
-		return err
-	}
-
-	var iface any
-	if fileName == "-" {
-		iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
-	} else {
-		iface, err = archiver.ByExtension(fileName)
-	}
+	archiverGeneric, err := archiver.ByExtension("." + outType)
 	if err != nil {
 		fatal("Unable to get archiver for extension: %v", err)
 	}
 
-	w, _ := iface.(archiver.Writer)
-	if err := w.Create(file); err != nil {
+	archiverWriter := archiverGeneric.(archiver.Writer)
+	if err := archiverWriter.Create(outFile); err != nil {
 		fatal("Creating archiver.Writer failed: %v", err)
 	}
-	defer w.Close()
+	defer archiverWriter.Close()
+
+	dumper := &dump.Dumper{
+		Writer:  archiverWriter,
+		Verbose: verbose,
+	}
+	dumper.GlobalExcludeAbsPath(outFileName)
 
 	if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
 		log.Info("Skip dumping local repositories")
 	} else {
 		log.Info("Dumping local repositories... %s", setting.RepoRootPath)
-		if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
+		if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
 			fatal("Failed to include repositories: %v", err)
 		}
 
@@ -276,8 +179,7 @@ func runDump(ctx *cli.Context) error {
 			if err != nil {
 				return err
 			}
-
-			return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
+			return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
 		}); err != nil {
 			fatal("Failed to dump LFS objects: %v", err)
 		}
@@ -310,15 +212,13 @@ func runDump(ctx *cli.Context) error {
 		fatal("Failed to dump database: %v", err)
 	}
 
-	if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil {
+	if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
 		fatal("Failed to include gitea-db.sql: %v", err)
 	}
 
-	if len(setting.CustomConf) > 0 {
-		log.Info("Adding custom configuration file from %s", setting.CustomConf)
-		if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
-			fatal("Failed to include specified app.ini: %v", err)
-		}
+	log.Info("Adding custom configuration file from %s", setting.CustomConf)
+	if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
+		fatal("Failed to include specified app.ini: %v", err)
 	}
 
 	if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
@@ -326,8 +226,8 @@ func runDump(ctx *cli.Context) error {
 	} else {
 		customDir, err := os.Stat(setting.CustomPath)
 		if err == nil && customDir.IsDir() {
-			if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
-				if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
+			if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
+				if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
 					fatal("Failed to include custom: %v", err)
 				}
 			} else {
@@ -364,8 +264,7 @@ func runDump(ctx *cli.Context) error {
 		excludes = append(excludes, setting.Attachment.Storage.Path)
 		excludes = append(excludes, setting.Packages.Storage.Path)
 		excludes = append(excludes, setting.Log.RootPath)
-		excludes = append(excludes, absFileName)
-		if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
+		if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
 			fatal("Failed to include data directory: %v", err)
 		}
 	}
@@ -377,8 +276,7 @@ func runDump(ctx *cli.Context) error {
 		if err != nil {
 			return err
 		}
-
-		return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
+		return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
 	}); err != nil {
 		fatal("Failed to dump attachments: %v", err)
 	}
@@ -392,8 +290,7 @@ func runDump(ctx *cli.Context) error {
 		if err != nil {
 			return err
 		}
-
-		return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
+		return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
 	}); err != nil {
 		fatal("Failed to dump packages: %v", err)
 	}
@@ -409,80 +306,23 @@ func runDump(ctx *cli.Context) error {
 			log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
 		}
 		if isExist {
-			if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
+			if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
 				fatal("Failed to include log: %v", err)
 			}
 		}
 	}
 
-	if fileName != "-" {
-		if err = w.Close(); err != nil {
-			_ = util.Remove(fileName)
-			fatal("Failed to save %s: %v", fileName, err)
+	if outFileName == "-" {
+		log.Info("Finish dumping to stdout")
+	} else {
+		if err = archiverWriter.Close(); err != nil {
+			_ = os.Remove(outFileName)
+			fatal("Failed to save %q: %v", outFileName, err)
 		}
-
-		if err := os.Chmod(fileName, 0o600); err != nil {
+		if err = os.Chmod(outFileName, 0o600); err != nil {
 			log.Info("Can't change file access permissions mask to 0600: %v", err)
 		}
-	}
-
-	if fileName != "-" {
-		log.Info("Finish dumping in file %s", fileName)
-	} else {
-		log.Info("Finish dumping to stdout")
-	}
-
-	return nil
-}
-
-// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
-func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
-	absPath, err := filepath.Abs(absPath)
-	if err != nil {
-		return err
-	}
-	dir, err := os.Open(absPath)
-	if err != nil {
-		return err
-	}
-	defer dir.Close()
-
-	files, err := dir.Readdir(0)
-	if err != nil {
-		return err
-	}
-	for _, file := range files {
-		currentAbsPath := filepath.Join(absPath, file.Name())
-		currentInsidePath := path.Join(insidePath, file.Name())
-		if file.IsDir() {
-			if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
-				if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
-					return err
-				}
-				if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
-					return err
-				}
-			}
-		} else {
-			// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
-			shouldAdd := file.Mode().IsRegular()
-			if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
-				target, err := filepath.EvalSymlinks(currentAbsPath)
-				if err != nil {
-					return err
-				}
-				targetStat, err := os.Stat(target)
-				if err != nil {
-					return err
-				}
-				shouldAdd = targetStat.Mode().IsRegular()
-			}
-			if shouldAdd {
-				if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
-					return err
-				}
-			}
-		}
+		log.Info("Finish dumping in file %s", outFileName)
 	}
 	return nil
 }
diff --git a/modules/dump/dumper.go b/modules/dump/dumper.go
new file mode 100644
index 0000000000..47730851fb
--- /dev/null
+++ b/modules/dump/dumper.go
@@ -0,0 +1,174 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package dump
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path"
+	"path/filepath"
+	"slices"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"github.com/mholt/archiver/v3"
+)
+
+var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
+
+// PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
+func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
+	if argFile == "" && argType == "" {
+		outType = SupportedOutputTypes[0]
+		outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
+	} else if argFile == "" {
+		outType = argType
+		outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
+	} else if argType == "" {
+		if filepath.Ext(outFileName) == "" {
+			outType = SupportedOutputTypes[0]
+			outFileName = argFile
+		} else {
+			for _, t := range SupportedOutputTypes {
+				if strings.HasSuffix(argFile, "."+t) {
+					outFileName = argFile
+					outType = t
+				}
+			}
+		}
+	} else {
+		outFileName, outType = argFile, argType
+	}
+	if !slices.Contains(SupportedOutputTypes, outType) {
+		return "", ""
+	}
+	return outFileName, outType
+}
+
+func IsSubdir(upper, lower string) (bool, error) {
+	if relPath, err := filepath.Rel(upper, lower); err != nil {
+		return false, err
+	} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
+		return true, nil
+	}
+	return false, nil
+}
+
+type Dumper struct {
+	Writer  archiver.Writer
+	Verbose bool
+
+	globalExcludeAbsPaths []string
+}
+
+func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error {
+	if dumper.Verbose {
+		log.Info("Adding file %s", customName)
+	}
+
+	return dumper.Writer.Write(archiver.File{
+		FileInfo: archiver.FileInfo{
+			FileInfo:   info,
+			CustomName: customName,
+		},
+		ReadCloser: r,
+	})
+}
+
+func (dumper *Dumper) AddFile(filePath, absPath string) error {
+	file, err := os.Open(absPath)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	fileInfo, err := file.Stat()
+	if err != nil {
+		return err
+	}
+	return dumper.AddReader(file, fileInfo, filePath)
+}
+
+func (dumper *Dumper) normalizeFilePath(absPath string) string {
+	absPath = filepath.Clean(absPath)
+	if setting.IsWindows {
+		absPath = strings.ToLower(absPath)
+	}
+	return absPath
+}
+
+func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
+	for _, absPath := range absPaths {
+		dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
+	}
+}
+
+func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
+	norm := dumper.normalizeFilePath(absPath)
+	return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
+}
+
+func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
+	excludes = slices.Clone(excludes)
+	for i := range excludes {
+		excludes[i] = dumper.normalizeFilePath(excludes[i])
+	}
+	return dumper.addFileOrDir(insidePath, absPath, excludes)
+}
+
+func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
+	absPath, err := filepath.Abs(absPath)
+	if err != nil {
+		return err
+	}
+	dir, err := os.Open(absPath)
+	if err != nil {
+		return err
+	}
+	defer dir.Close()
+
+	files, err := dir.Readdir(0)
+	if err != nil {
+		return err
+	}
+	for _, file := range files {
+		currentAbsPath := filepath.Join(absPath, file.Name())
+		if dumper.shouldExclude(currentAbsPath, excludes) {
+			continue
+		}
+
+		currentInsidePath := path.Join(insidePath, file.Name())
+		if file.IsDir() {
+			if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
+				return err
+			}
+			if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
+				return err
+			}
+		} else {
+			// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
+			shouldAdd := file.Mode().IsRegular()
+			if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
+				target, err := filepath.EvalSymlinks(currentAbsPath)
+				if err != nil {
+					return err
+				}
+				targetStat, err := os.Stat(target)
+				if err != nil {
+					return err
+				}
+				shouldAdd = targetStat.Mode().IsRegular()
+			}
+			if shouldAdd {
+				if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
+					return err
+				}
+			}
+		}
+	}
+	return nil
+}
diff --git a/modules/dump/dumper_test.go b/modules/dump/dumper_test.go
new file mode 100644
index 0000000000..b444fa2de5
--- /dev/null
+++ b/modules/dump/dumper_test.go
@@ -0,0 +1,113 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package dump
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sort"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"github.com/mholt/archiver/v3"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPrepareFileNameAndType(t *testing.T) {
+	defer timeutil.MockSet(time.Unix(1234, 0))()
+	test := func(argFile, argType, expFile, expType string) {
+		outFile, outType := PrepareFileNameAndType(argFile, argType)
+		assert.Equal(t,
+			fmt.Sprintf("outFile=%s, outType=%s", expFile, expType),
+			fmt.Sprintf("outFile=%s, outType=%s", outFile, outType),
+			fmt.Sprintf("argFile=%s, argType=%s", argFile, argType),
+		)
+	}
+
+	test("", "", "gitea-dump-1234.zip", "zip")
+	test("", "tar.gz", "gitea-dump-1234.tar.gz", "tar.gz")
+	test("", "no-such", "", "")
+
+	test("-", "", "-", "zip")
+	test("-", "tar.gz", "-", "tar.gz")
+	test("-", "no-such", "", "")
+
+	test("a", "", "a", "zip")
+	test("a", "tar.gz", "a", "tar.gz")
+	test("a", "no-such", "", "")
+
+	test("a.zip", "", "a.zip", "zip")
+	test("a.zip", "tar.gz", "a.zip", "tar.gz")
+	test("a.zip", "no-such", "", "")
+
+	test("a.tar.gz", "", "a.tar.gz", "zip")
+	test("a.tar.gz", "tar.gz", "a.tar.gz", "tar.gz")
+	test("a.tar.gz", "no-such", "", "")
+}
+
+func TestIsSubDir(t *testing.T) {
+	tmpDir := t.TempDir()
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
+
+	isSub, err := IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include"))
+	assert.NoError(t, err)
+	assert.True(t, isSub)
+
+	isSub, err = IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include/sub"))
+	assert.NoError(t, err)
+	assert.True(t, isSub)
+
+	isSub, err = IsSubdir(filepath.Join(tmpDir, "include/sub"), filepath.Join(tmpDir, "include"))
+	assert.NoError(t, err)
+	assert.False(t, isSub)
+}
+
+type testWriter struct {
+	added []string
+}
+
+func (t *testWriter) Create(out io.Writer) error {
+	return nil
+}
+
+func (t *testWriter) Write(f archiver.File) error {
+	t.added = append(t.added, f.Name())
+	return nil
+}
+
+func (t *testWriter) Close() error {
+	return nil
+}
+
+func TestDumper(t *testing.T) {
+	sortStrings := func(s []string) []string {
+		sort.Strings(s)
+		return s
+	}
+	tmpDir := t.TempDir()
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude1"), 0o755)
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude2"), 0o755)
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/a"), nil, 0o644)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/sub/b"), nil, 0o644)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude1/a-1"), nil, 0o644)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude2/a-2"), nil, 0o644)
+
+	tw := &testWriter{}
+	d := &Dumper{Writer: tw}
+	d.GlobalExcludeAbsPath(filepath.Join(tmpDir, "include/exclude1"))
+	err := d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), []string{filepath.Join(tmpDir, "include/exclude2")})
+	assert.NoError(t, err)
+	assert.EqualValues(t, sortStrings([]string{"include/a", "include/sub", "include/sub/b"}), sortStrings(tw.added))
+
+	tw = &testWriter{}
+	d = &Dumper{Writer: tw}
+	err = d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), nil)
+	assert.NoError(t, err)
+	assert.EqualValues(t, sortStrings([]string{"include/exclude2", "include/exclude2/a-2", "include/a", "include/sub", "include/sub/b", "include/exclude1", "include/exclude1/a-1"}), sortStrings(tw.added))
+}
diff --git a/modules/setting/log.go b/modules/setting/log.go
index e404074b72..50c5779994 100644
--- a/modules/setting/log.go
+++ b/modules/setting/log.go
@@ -185,8 +185,13 @@ func InitLoggersForTest() {
 	initAllLoggers()
 }
 
+var initLoggerDisabled bool
+
 // initAllLoggers creates all the log services
 func initAllLoggers() {
+	if initLoggerDisabled {
+		return
+	}
 	initManagedLoggers(log.GetManager(), CfgProvider)
 
 	golog.SetFlags(0)
@@ -194,6 +199,10 @@ func initAllLoggers() {
 	golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
 }
 
+func DisableLoggerInit() {
+	initLoggerDisabled = true
+}
+
 func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
 	loadLogGlobalFrom(cfg)
 	prepareLoggerConfig(cfg)
diff --git a/modules/timeutil/timestamp.go b/modules/timeutil/timestamp.go
index 27a80b6682..e77652b24f 100644
--- a/modules/timeutil/timestamp.go
+++ b/modules/timeutil/timestamp.go
@@ -21,8 +21,9 @@ var (
 )
 
 // MockSet sets the time to a mocked time.Time
-func MockSet(now time.Time) {
+func MockSet(now time.Time) func() {
 	mockNow = now
+	return MockUnset
 }
 
 // MockUnset will unset the mocked time.Time
diff --git a/modules/util/util.go b/modules/util/util.go
index b6e730eb54..3921002e2a 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -213,6 +213,14 @@ func ToPointer[T any](val T) *T {
 	return &val
 }
 
+// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
+func Iif[T comparable](condition bool, trueVal, falseVal T) T {
+	if condition {
+		return trueVal
+	}
+	return falseVal
+}
+
 // IfZero returns "def" if "v" is a zero value, otherwise "v"
 func IfZero[T comparable](v, def T) T {
 	var zero T

From 1195be41a13d2198ab644c8558549edd74485510 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 3 Apr 2024 11:15:06 +0200
Subject: [PATCH 619/679] Replace coloris with vanilla-colorful (#30201)

Found [a better color
picker](https://github.com/web-padawan/vanilla-colorful) that [does not
rely](https://github.com/mdbassit/Coloris/issues/139) on
`querySelectorAll` or a global shared instance, and is also around a
third of the size of the previous one.

The popover is handled by tippy.js for which I introduced a new "bare"
theme and it uses a new sibling-based mechanism which should prove
useful later to create tippy popovers via HTML only.

<img width="846" alt="Screenshot 2024-03-31 at 04 03 38"
src="https://github.com/go-gitea/gitea/assets/115237/7639b911-a2d7-4f5c-bffd-a9d84561e747">
---
 package-lock.json                    |  12 +--
 package.json                         |   2 +-
 web_src/css/features/colorpicker.css | 141 +++------------------------
 web_src/css/modules/tippy.css        |  11 +++
 web_src/js/features/colorpicker.js   |  85 +++++++++++-----
 web_src/js/modules/tippy.js          |   7 +-
 6 files changed, 94 insertions(+), 164 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 21de79387f..a5f7a09ed0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,6 @@
         "@github/relative-time-element": "4.4.0",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-        "@melloware/coloris": "0.23.0",
         "@primer/octicons": "19.9.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
@@ -54,6 +53,7 @@
         "toastify-js": "1.12.0",
         "tributejs": "5.1.3",
         "uint8-to-base64": "0.2.0",
+        "vanilla-colorful": "0.7.2",
         "vue": "3.4.21",
         "vue-bar-graph": "2.0.0",
         "vue-chartjs": "5.3.0",
@@ -1290,11 +1290,6 @@
         "@mcaptcha/core-glue": "^0.1.0-alpha-5"
       }
     },
-    "node_modules/@melloware/coloris": {
-      "version": "0.23.0",
-      "resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
-      "integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
-    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -11853,6 +11848,11 @@
         "builtins": "^1.0.3"
       }
     },
+    "node_modules/vanilla-colorful": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz",
+      "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
+    },
     "node_modules/vite": {
       "version": "5.2.6",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
diff --git a/package.json b/package.json
index beea0e5d86..004ac9e2bf 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,6 @@
     "@github/relative-time-element": "4.4.0",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-    "@melloware/coloris": "0.23.0",
     "@primer/octicons": "19.9.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
@@ -53,6 +52,7 @@
     "toastify-js": "1.12.0",
     "tributejs": "5.1.3",
     "uint8-to-base64": "0.2.0",
+    "vanilla-colorful": "0.7.2",
     "vue": "3.4.21",
     "vue-bar-graph": "2.0.0",
     "vue-chartjs": "5.3.0",
diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css
index 0c651cfeb3..b7436783df 100644
--- a/web_src/css/features/colorpicker.css
+++ b/web_src/css/features/colorpicker.css
@@ -1,10 +1,6 @@
-/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
-   opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
-   based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
-
 .js-color-picker-input {
   display: flex;
-  flex-wrap: wrap;
+  position: relative;
 }
 
 .js-color-picker-input input {
@@ -13,152 +9,39 @@
   padding-left: 32px !important;
 }
 
-.clr-picker {
-  display: none;
-  flex-wrap: wrap;
-  position: absolute;
-  width: 200px;
-  z-index: 1002; /* above .ui.modal which has 1001 */
-  border-radius: var(--border-radius);
-  background-color: var(--color-menu);
-  justify-content: flex-end;
-  direction: ltr;
-  box-shadow: 0 5px 20px var(--color-shadow);
-  user-select: none;
-}
-
-.clr-picker.clr-open {
-  display: flex;
-}
-
-.clr-gradient {
-  position: relative;
-  width: 100%;
-  height: 100px;
-  border-radius: 3px 3px 0 0;
-  background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
-  cursor: pointer;
-}
-
-.clr-marker {
-  position: absolute;
-  width: 12px;
-  height: 12px;
-  margin: -6px 0 0 -6px;
-  border: 1px solid var(--color-white);
-  border-radius: 50%;
-  background-color: currentcolor;
-  cursor: pointer;
-}
-
-.clr-picker input[type="range"]::-webkit-slider-runnable-track {
-  width: 100%;
-  height: 16px;
-}
-
-.clr-picker input[type="range"]::-webkit-slider-thumb {
-  width: 16px;
-  height: 16px;
-  -webkit-appearance: none;
-}
-
-.clr-picker input[type="range"]::-moz-range-track {
-  width: 100%;
-  height: 16px;
-  border: 0;
-}
-
-.clr-picker input[type="range"]::-moz-range-thumb {
-  width: 16px;
-  height: 16px;
-  border: 0;
-}
-
-.clr-hue {
-  background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
-  position: relative;
-  width: calc(100% - 40px);
-  height: 10px;
-  margin: 10px 20px;
-  border-radius: 4px;
-}
-
-.clr-hue input[type="range"] {
-  position: absolute;
-  width: calc(100% + 32px);
-  margin: 0;
-  background-color: transparent;
-  opacity: 0;
-  cursor: pointer;
-  appearance: none;
-}
-
-.clr-hue div {
-  position: absolute;
-  width: 16px;
-  height: 16px;
-  left: 0;
-  top: 50%;
-  transform: translate(-50%, -50%);
-  border: 2px solid var(--color-white);
-  border-radius: 50%;
-  background-color: currentcolor;
-  box-shadow: 0 0 1px var(--color-shadow);
-  pointer-events: none;
-}
-
-.clr-field {
-  flex: 1;
-  position: relative;
-  color: transparent;
-}
-
-.clr-field button {
+.js-color-picker-input .preview-square {
   position: absolute;
   aspect-ratio: 1;
   height: 16px;
   left: 10px;
   top: 50%;
   transform: translateY(-50%);
-  margin: 0;
-  padding: 0;
-  border: 0;
-  color: inherit;
-  pointer-events: none;
   border-radius: 2px;
   background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
   background-position: 0 0, 4px 4px;
   background-size: 8px 8px;
 }
 
-.clr-field button::after {
+.js-color-picker-input .preview-square::after {
   content: "";
-  display: block;
   position: absolute;
   width: 100%;
   height: 100%;
-  left: 0;
-  top: 0;
   border-radius: inherit;
   background-color: currentcolor;
 }
 
-.clr-marker:focus {
-  outline: none;
+hex-color-picker {
+  width: 180px;
+  height: 120px;
 }
 
-.clr-keyboard-nav .clr-marker:focus,
-.clr-keyboard-nav .clr-hue input:focus + div,
-.clr-keyboard-nav .clr-alpha input:focus + div {
-  outline: none;
-  box-shadow: 0 0 2px 2px var(--color-white);
+hex-color-picker::part(hue-pointer),
+hex-color-picker::part(saturation-pointer) {
+  width: 22px;
+  height: 22px;
 }
 
-.clr-picker .clr-preview,
-.clr-picker .clr-clear,
-.clr-picker .clr-swatches,
-.clr-picker .clr-format,
-.clr-picker .clr-alpha,
-.clr-picker .clr-color {
-  display: none;
+hex-color-picker::part(hue) {
+  flex-basis: 16px;
 }
diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css
index 76d36b4293..6ac7c37d93 100644
--- a/web_src/css/modules/tippy.css
+++ b/web_src/css/modules/tippy.css
@@ -29,6 +29,17 @@
   z-index: 1;
 }
 
+/* bare theme, no styling at all, except box-shadow */
+.tippy-box[data-theme="bare"] {
+  border: none;
+  box-shadow: 0 6px 18px var(--color-shadow);
+}
+
+.tippy-box[data-theme="bare"] .tippy-content {
+  padding: 0;
+  background: transparent;
+}
+
 /* tooltip theme for text tooltips */
 
 .tippy-box[data-theme="tooltip"] {
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
index f342598e66..6d00d908c9 100644
--- a/web_src/js/features/colorpicker.js
+++ b/web_src/js/features/colorpicker.js
@@ -1,31 +1,66 @@
-export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
-  const inputEls = document.querySelectorAll(selector);
-  if (!inputEls.length) return;
+import {createTippy} from '../modules/tippy.js';
 
-  const [{coloris, init}] = await Promise.all([
-    import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
+export async function initColorPickers() {
+  const els = document.getElementsByClassName('js-color-picker-input');
+  if (!els.length) return;
+
+  await Promise.all([
+    import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
     import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
   ]);
 
-  init();
-  coloris({
-    el: selector,
-    alpha: false,
-    focusInput: true,
-    selectInput: false,
-    ...opts,
-  });
-
-  for (const inputEl of inputEls) {
-    const parent = inputEl.closest('.js-color-picker-input');
-    // prevent tabbing on the color preview `button` inside the input
-    parent.querySelector('button').tabIndex = -1;
-    // init precolors
-    for (const el of parent.querySelectorAll('.precolors .color')) {
-      el.addEventListener('click', (e) => {
-        inputEl.value = e.target.getAttribute('data-color-hex');
-        inputEl.dispatchEvent(new Event('input', {bubbles: true}));
-      });
-    }
+  for (const el of els) {
+    initPicker(el);
+  }
+}
+
+function updateSquare(el, newValue) {
+  el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
+}
+
+function updatePicker(el, newValue) {
+  el.setAttribute('color', newValue);
+}
+
+function initPicker(el) {
+  const input = el.querySelector('input');
+
+  const square = document.createElement('div');
+  square.classList.add('preview-square');
+  updateSquare(square, input.value);
+  el.append(square);
+
+  const picker = document.createElement('hex-color-picker');
+  picker.addEventListener('color-changed', (e) => {
+    input.value = e.detail.value;
+    input.focus();
+    updateSquare(square, e.detail.value);
+  });
+
+  input.addEventListener('input', (e) => {
+    updateSquare(square, e.target.value);
+    updatePicker(picker, e.target.value);
+  });
+
+  createTippy(input, {
+    trigger: 'focus click',
+    theme: 'bare',
+    hideOnClick: true,
+    content: picker,
+    placement: 'bottom-start',
+    interactive: true,
+    onShow() {
+      updatePicker(picker, input.value);
+    },
+  });
+
+  // init precolors
+  for (const colorEl of el.querySelectorAll('.precolors .color')) {
+    colorEl.addEventListener('click', (e) => {
+      const newValue = e.target.getAttribute('data-color-hex');
+      input.value = newValue;
+      input.dispatchEvent(new Event('input', {bubbles: true}));
+      updateSquare(square, newValue);
+    });
   }
 }
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index 220c9e5512..83b28e5745 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -3,11 +3,12 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
 import {formatDatetime} from '../utils/time.js';
 
 const visibleInstances = new Set();
+const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
 
 export function createTippy(target, opts = {}) {
   // the callback functions should be destructured from opts,
   // because we should use our own wrapper functions to handle them, do not let the user override them
-  const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
+  const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
 
   const instance = tippy(target, {
     appendTo: document.body,
@@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
       visibleInstances.add(instance);
       return onShow?.(instance);
     },
-    arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`,
+    arrow: arrow || (theme === 'bare' ? false : arrowSvg),
     role: role || 'menu', // HTML role attribute
-    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
+    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
     plugins: [followCursor],
     ...other,
   });

From 0ceecfc11ab4851863a5a6bc5e398d2baf7e86f6 Mon Sep 17 00:00:00 2001
From: guangwu <guoguangwu@magic-shield.com>
Date: Wed, 3 Apr 2024 22:58:13 +0800
Subject: [PATCH 620/679] fix: close file in the Upload func (#30262)

---
 modules/lfs/filesystem_client.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/lfs/filesystem_client.go b/modules/lfs/filesystem_client.go
index 3503a9effc..71bef5c899 100644
--- a/modules/lfs/filesystem_client.go
+++ b/modules/lfs/filesystem_client.go
@@ -44,7 +44,7 @@ func (c *FilesystemClient) Download(ctx context.Context, objects []Pointer, call
 		if err != nil {
 			return err
 		}
-
+		defer f.Close()
 		if err := callback(p, f, nil); err != nil {
 			return err
 		}
@@ -75,7 +75,7 @@ func (c *FilesystemClient) Upload(ctx context.Context, objects []Pointer, callba
 			if err != nil {
 				return err
 			}
-
+			defer f.Close()
 			_, err = io.Copy(f, content)
 
 			return err

From 609a627a44dbcb7b630ff51ce9f4b9f448b48ca8 Mon Sep 17 00:00:00 2001
From: Yakov <git@yakov.cloud>
Date: Wed, 3 Apr 2024 09:01:50 -0700
Subject: [PATCH 621/679] Add `[other].SHOW_FOOTER_POWERED_BY` setting to hide
 `Powered by` (#30253)

This allows you to hide the "Powered by" text in footer via
`SHOW_FOOTER_POWERED_BY` flag in configuration.

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 custom/conf/app.example.ini                             | 2 ++
 docs/content/administration/config-cheat-sheet.en-us.md | 1 +
 docs/content/administration/config-cheat-sheet.zh-cn.md | 1 +
 modules/setting/other.go                                | 2 ++
 modules/templates/helper.go                             | 3 +++
 templates/base/footer_content.tmpl                      | 4 +++-
 6 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 1584b10301..918252044b 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2315,6 +2315,8 @@ LEVEL = Info
 ;SHOW_FOOTER_VERSION = true
 ;; Show template execution time in the footer
 ;SHOW_FOOTER_TEMPLATE_LOAD_TIME = true
+;; Show the "powered by" text in the footer
+;SHOW_FOOTER_POWERED_BY = true
 ;; Generate sitemap. Defaults to `true`.
 ;ENABLE_SITEMAP = true
 ;; Enable/Disable RSS/Atom feed
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index f2209d269a..9de7511964 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -1429,5 +1429,6 @@ Like `uses: https://gitea.com/actions/checkout@v4` or `uses: http://your-git-ser
 
 - `SHOW_FOOTER_VERSION`: **true**: Show Gitea and Go version information in the footer.
 - `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: Show time of template execution in the footer.
+- `SHOW_FOOTER_POWERED_BY`: **true**: Show the "powered by" text in the footer.
 - `ENABLE_SITEMAP`: **true**: Generate sitemap.
 - `ENABLE_FEED`: **true**: Enable/Disable RSS/Atom feed.
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 41c8844ae5..759f39b576 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -1353,5 +1353,6 @@ PROXY_HOSTS = *.github.com
 
 - `SHOW_FOOTER_VERSION`: **true**: 在页面底部显示Gitea的版本。
 - `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: 在页脚显示模板执行的时间。
+- `SHOW_FOOTER_POWERED_BY`: **true**: 在页脚显示“由...提供动力”的文本。
 - `ENABLE_SITEMAP`: **true**: 生成sitemap.
 - `ENABLE_FEED`: **true**: 是否启用RSS/Atom
diff --git a/modules/setting/other.go b/modules/setting/other.go
index 706cb1e3d9..4ba494765b 100644
--- a/modules/setting/other.go
+++ b/modules/setting/other.go
@@ -8,6 +8,7 @@ import "code.gitea.io/gitea/modules/log"
 type OtherConfig struct {
 	ShowFooterVersion          bool
 	ShowFooterTemplateLoadTime bool
+	ShowFooterPoweredBy        bool
 	EnableFeed                 bool
 	EnableSitemap              bool
 }
@@ -15,6 +16,7 @@ type OtherConfig struct {
 var Other = OtherConfig{
 	ShowFooterVersion:          true,
 	ShowFooterTemplateLoadTime: true,
+	ShowFooterPoweredBy:        true,
 	EnableSitemap:              true,
 	EnableFeed:                 true,
 }
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 2452064749..9e770a2606 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -106,6 +106,9 @@ func NewFuncMap() template.FuncMap {
 		"ShowFooterTemplateLoadTime": func() bool {
 			return setting.Other.ShowFooterTemplateLoadTime
 		},
+		"ShowFooterPoweredBy": func() bool {
+			return setting.Other.ShowFooterPoweredBy
+		},
 		"AllowedReactions": func() []string {
 			return setting.UI.Reactions
 		},
diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl
index f0a7865602..8d0d8e669c 100644
--- a/templates/base/footer_content.tmpl
+++ b/templates/base/footer_content.tmpl
@@ -1,6 +1,8 @@
 <footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
 	<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
-		<a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">{{ctx.Locale.Tr "powered_by" "Gitea"}}</a>
+		{{if ShowFooterPoweredBy}}
+			<a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">{{ctx.Locale.Tr "powered_by" "Gitea"}}</a>
+		{{end}}
 		{{if (or .ShowFooterVersion .PageIsAdmin)}}
 			{{ctx.Locale.Tr "version"}}:
 			{{if .IsAdmin}}

From 39e64e094f5b62401c3652983d8058df85ef744d Mon Sep 17 00:00:00 2001
From: Knud Hollander <26556793+KnudH@users.noreply.github.com>
Date: Thu, 4 Apr 2024 01:16:02 +0200
Subject: [PATCH 622/679] update mailer example config, remove deprecated HOST
 (#30267)

---
 docs/content/installation/with-docker.en-us.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/docs/content/installation/with-docker.en-us.md b/docs/content/installation/with-docker.en-us.md
index e8a80f7c96..a16d4a8d60 100644
--- a/docs/content/installation/with-docker.en-us.md
+++ b/docs/content/installation/with-docker.en-us.md
@@ -304,7 +304,8 @@ services:
       - GITEA__mailer__ENABLED=true
       - GITEA__mailer__FROM=${GITEA__mailer__FROM:?GITEA__mailer__FROM not set}
       - GITEA__mailer__PROTOCOL=smtps
-      - GITEA__mailer__HOST=${GITEA__mailer__HOST:?GITEA__mailer__HOST not set}
+      - GITEA__mailer__SMTP_ADDR=${GITEA__mailer__SMTP_ADDR:?GITEA__mailer__SMTP_ADDR not set}
+      - GITEA__mailer__SMTP_PORT=${GITEA__mailer__SMTP_PORT:?GITEA__mailer__SMTP_PORT not set}
       - GITEA__mailer__USER=${GITEA__mailer__USER:-apikey}
       - GITEA__mailer__PASSWD="""${GITEA__mailer__PASSWD:?GITEA__mailer__PASSWD not set}"""
 ```

From 663acd0b4620a7a4e83f2d0699749af95126b0f5 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Thu, 4 Apr 2024 00:24:47 +0000
Subject: [PATCH 623/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_pt-PT.ini | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 09b9d4e3ce..59b3d3df67 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1233,6 +1233,8 @@ file_view_rendered=Ver resultado processado
 file_view_raw=Ver em bruto
 file_permalink=Ligação permanente
 file_too_large=O ficheiro é demasiado grande para ser apresentado.
+code_preview_line_from_to=Linhas %[1]d até %[2]d em %[3]s
+code_preview_line_in=Linha %[1]d em %[2]s
 invisible_runes_header=`Este ficheiro contém caracteres Unicode invisíveis`
 invisible_runes_description=`Este ficheiro contém caracteres Unicode indistinguíveis para humanos mas que podem ser processados de forma diferente por um computador. Se acha que é intencional, pode ignorar este aviso com segurança. Use o botão Revelar para os mostrar.`
 ambiguous_runes_header=`Este ficheiro contém caracteres Unicode ambíguos`

From 83c5072077f34e6e108b12559b5aef2ae2de5a22 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Fri, 5 Apr 2024 00:24:29 +0000
Subject: [PATCH 624/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_ja-JP.ini | 64 +++++++++++++++++++++++++++++++--
 options/locale/locale_zh-CN.ini | 54 ++++++++++++++++++++++++++++
 2 files changed, 116 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index eddad35073..7e725b4647 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -113,6 +113,7 @@ loading=読み込み中…
 error=エラー
 error404=アクセスしようとしたページは<strong>存在しない</strong>か、閲覧が<strong>許可されていません</strong>。
 go_back=戻る
+invalid_data=無効なデータ: %v
 
 never=無し
 unknown=不明
@@ -143,13 +144,41 @@ name=名称
 value=値
 
 filter=フィルター
+filter.clear=フィルターをクリア
 filter.is_archived=アーカイブ
+filter.not_archived=非アーカイブ
+filter.is_fork=フォーク
+filter.not_fork=非フォーク
+filter.is_mirror=ミラー
+filter.not_mirror=非ミラー
 filter.is_template=テンプレート
+filter.not_template=非テンプレート
 filter.public=公開
 filter.private=プライベート
 
+no_results_found=見つかりません。
 
 [search]
+search=検索…
+type_tooltip=検索タイプ
+fuzzy=あいまい
+fuzzy_tooltip=検索ワードに近い結果も含めます
+match=一致
+match_tooltip=検索ワードと完全に一致する結果のみ含めます
+repo_kind=リポジトリを検索...
+user_kind=ユーザーを検索...
+org_kind=組織を検索...
+team_kind=チームを検索…
+code_kind=コードを検索...
+code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
+code_search_by_git_grep=現在のコード検索結果は "git grep" で提供されています。 サイト管理者がリポジトリインデクサーを有効にすると、より良い結果が得られるかもしれません。
+package_kind=パッケージを検索...
+project_kind=プロジェクトを検索...
+branch_kind=ブランチを検索...
+commit_kind=コミットを検索...
+runner_kind=ランナーを検索...
+no_results=一致する結果が見つかりませんでした
+keyword_search_unavailable=現在キーワード検索は利用できません。 サイト管理者にお問い合わせください。
 
 [aria]
 navbar=ナビゲーションバー
@@ -256,6 +285,7 @@ email_title=メール設定
 smtp_addr=SMTPホスト
 smtp_port=SMTPポート
 smtp_from=メール送信者
+smtp_from_invalid=「メール送信者」のアドレスが無効です
 smtp_from_helper=Giteaが使用するメールアドレス。 メールアドレスのみ、または、 "名前" <email@example.com> の形式で入力してください。
 mailer_user=SMTPユーザー名
 mailer_password=SMTPパスワード
@@ -315,6 +345,7 @@ env_config_keys=環境設定
 env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます:
 
 [home]
+nav_menu=ナビゲーションメニュー
 uname_holder=ユーザー名またはメールアドレス
 password_holder=パスワード
 switch_dashboard_context=ダッシュボードのコンテキスト切替
@@ -618,6 +649,23 @@ block.block.org=組織向けにユーザーをブロック
 block.block.failure=ユーザーのブロックに失敗しました: %s
 block.unblock=ブロックを解除
 block.unblock.failure=ユーザーのブロック解除に失敗しました: %s
+block.blocked=あなたはこのユーザーをブロックしています。
+block.title=ユーザーをブロックする
+block.info=ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。 ユーザーのブロックについてはよく確認してください。
+block.info_1=ユーザーをブロックすることで、あなたのアカウントとリポジトリに対する以下の行為を防ぎます:
+block.info_2=あなたのアカウントのフォロー
+block.info_3=あなたのユーザー名で@メンションして通知を送ること
+block.info_4=そのユーザーのリポジトリに、あなたを共同作業者として招待すること
+block.info_5=リポジトリへの、スター、フォーク、ウォッチ
+block.info_6=イシューやプルリクエストの作成、コメント投稿
+block.info_7=イシューやプルリクエストでの、あなたのコメントに対するリアクションの送信
+block.user_to_block=ブロックするユーザー
+block.note=メモ
+block.note.title=メモ(任意):
+block.note.info=メモはブロックされるユーザーには表示されません。
+block.note.edit=メモを編集
+block.list=ブロックしたユーザー
+block.list.none=ブロックしているユーザーはいません。
 
 [settings]
 profile=プロフィール
@@ -956,6 +1004,7 @@ fork_branch=フォークにクローンされるブランチ
 all_branches=すべてのブランチ
 fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
 use_template=このテンプレートを使用
+open_with_editor=%s で開く
 download_zip=ZIPファイルをダウンロード
 download_tar=TAR.GZファイルをダウンロード
 download_bundle=バンドルをダウンロード
@@ -1008,6 +1057,7 @@ watchers=ウォッチャー
 stargazers=スターゲイザー
 stars_remove_warning=これを指定すると、このリポジトリのスターはすべて削除されます。
 forks=フォーク
+stars=スター
 reactions_more=さらに %d 件
 unit_disabled=サイト管理者がこのリポジトリセクションを無効にしています。
 language_other=その他
@@ -1039,7 +1089,7 @@ transfer.no_permission_to_reject=この移転を拒否する権限がありま
 desc.private=プライベート
 desc.public=公開
 desc.template=テンプレート
-desc.internal=組織内
+desc.internal=内部
 desc.archived=アーカイブ
 desc.sha256=SHA256
 
@@ -1257,6 +1307,8 @@ editor.file_editing_no_longer_exists=編集中のファイル "%s" が、もう
 editor.file_deleting_no_longer_exists=削除しようとしたファイル "%s" が、すでにリポジトリ内にありません。
 editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 <a target="_blank" rel="noopener noreferrer" href="%s">ここをクリック</a>して何が変更されたか確認するか、<strong>もう一度"変更をコミット"をクリック</strong>して上書きします。
 editor.file_already_exists=ファイル "%s" は、このリポジトリに既に存在します。
+editor.commit_id_not_matching=コミットIDが編集を開始したときのIDと一致しません。 パッチ用のブランチにコミットしたあとマージしてください。
+editor.push_out_of_date=このプッシュは最新ではないようです。
 editor.commit_empty_file_header=空ファイルのコミット
 editor.commit_empty_file_text=コミットしようとしているファイルは空です。 続行しますか?
 editor.no_changes_to_show=表示する変更箇所はありません。
@@ -1281,6 +1333,7 @@ commits.commits=コミット
 commits.no_commits=共通のコミットはありません。 "%s" と "%s" の履歴はすべて異なっています。
 commits.nothing_to_compare=二つのブランチは同じ内容です。
 commits.search.tooltip=`キーワード "author:"、"committer:"、"after:"、"before:" を付けて指定できます。 例 "revert author:Alice before:2019-01-13"`
+commits.search_branch=このブランチ
 commits.search_all=すべてのブランチ
 commits.author=作成者
 commits.message=メッセージ
@@ -1339,6 +1392,7 @@ projects.column.new=新しい列
 projects.column.set_default=デフォルトに設定
 projects.column.set_default_desc=この列を未分類のイシューやプルリクエストが入るデフォルトの列にします
 projects.column.delete=列を削除
+projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューがデフォルトの列に移動します。 続行しますか?
 projects.column.color=カラー
 projects.open=オープン
 projects.close=クローズ
@@ -1738,7 +1792,7 @@ pulls.is_checking=マージのコンフリクトを確認中です。 少し待
 pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
 pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
 pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
-pulls.required_status_check_missing=必要なステータスチェックが見つかりません。
+pulls.required_status_check_missing=必要なチェックがいくつか抜けています。
 pulls.required_status_check_administrator=管理者であるため、このプルリクエストをマージすることは可能です。
 pulls.blocked_by_approvals=このプルリクエストはまだ承認数が足りません。 %[1]d/%[2]dの承認を得ています。
 pulls.blocked_by_rejection=このプルリクエストは公式レビューアにより変更要請されています。
@@ -1907,6 +1961,7 @@ wiki.original_git_entry_tooltip=フレンドリーリンクを使用する代わ
 activity=アクティビティ
 activity.navbar.pulse=Pulse
 activity.navbar.contributors=貢献者
+activity.navbar.recent_commits=最近のコミット
 activity.period.filter_label=期間:
 activity.period.daily=1日
 activity.period.halfweekly=3日
@@ -2571,6 +2626,7 @@ component_loading_failed=%sを読み込めませんでした
 component_loading_info=少し時間がかかるかもしれません…
 component_failed_to_load=予期しないエラーが発生しました。
 contributors.what=実績
+recent_commits.what=最近のコミット
 
 [org]
 org_name_holder=組織名
@@ -2684,6 +2740,7 @@ teams.add_nonexistent_repo=追加しようとしているリポジトリは存
 teams.add_duplicate_users=ユーザーは既にチームのメンバーです。
 teams.repos.none=このチームがアクセスできるリポジトリはありません。
 teams.members.none=このチームにはメンバーがいません。
+teams.members.blocked_user=組織によってブロックされているため、ユーザーを追加できません。
 teams.specific_repositories=指定したリポジトリ
 teams.specific_repositories_helper=メンバーは、明示的にチームへ追加したリポジトリにのみアクセスできます。 これを選択しても、すでに<i>すべてのリポジトリ</i>で追加されたリポジトリは自動的に除去<strong>されません</strong>。
 teams.all_repositories=すべてのリポジトリ
@@ -3009,6 +3066,7 @@ auths.tip.nextcloud=新しいOAuthコンシューマーを、インスタンス
 auths.tip.dropbox=新しいアプリケーションを https://www.dropbox.com/developers/apps から登録してください。
 auths.tip.facebook=新しいアプリケーションを https://developers.facebook.com/apps で登録し、"Facebook Login"を追加してください。
 auths.tip.github=新しいOAuthアプリケーションを https://github.com/settings/applications/new から登録してください。
+auths.tip.gitlab_new=新しいアプリケーションを https://gitlab.com/-/profile/applications から登録してください。
 auths.tip.google_plus=OAuth2クライアント資格情報を、Google APIコンソール https://console.developers.google.com/ から取得してください。
 auths.tip.openid_connect=OpenID Connect DiscoveryのURL (<server>/.well-known/openid-configuration) をエンドポイントとして指定してください
 auths.tip.twitter=https://dev.twitter.com/apps へアクセスしてアプリケーションを作成し、“Allow this application to be used to Sign in with Twitter”オプションを有効にしてください。
@@ -3144,6 +3202,7 @@ config.picture_config=画像とアバターの設定
 config.picture_service=画像サービス
 config.disable_gravatar=Gravatarが無効
 config.enable_federated_avatar=フェデレーテッド・アバター有効
+config.open_with_editor_app_help=クローンメニューの「~で開く」に表示するエディタ。 空白のままにするとデフォルトが使用されます。 展開するとデフォルトを確認できます。
 
 config.git_config=Git設定
 config.git_disable_diff_highlight=Diffのシンタックスハイライトが無効
@@ -3542,6 +3601,7 @@ runs.scheduled=スケジュール済み
 runs.pushed_by=pushed by
 runs.invalid_workflow_helper=ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s
 runs.no_matching_online_runner_helper=ラベルに一致するオンラインのランナーが見つかりません: %s
+runs.no_job_without_needs=ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。
 runs.actor=アクター
 runs.status=ステータス
 runs.actors_no_select=すべてのアクター
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 01058d48d2..3e907eabfd 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -25,6 +25,7 @@ enable_javascript=此网站需要 JavaScript。
 toc=目录
 licenses=许可证
 return_to_gitea=返回 Gitea
+more_items=更多选项
 
 username=用户名
 email=电子邮件地址
@@ -113,6 +114,7 @@ loading=正在加载...
 error=错误
 error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。
 go_back=返回
+invalid_data=无效数据: %v
 
 never=从不
 unknown=未知
@@ -143,13 +145,36 @@ name=名称
 value=值
 
 filter=过滤
+filter.clear=清除筛选器
 filter.is_archived=已归档
+filter.not_archived=非存档
+filter.is_fork=派生
+filter.not_fork=非派生
+filter.is_mirror=镜像
+filter.not_mirror=非镜像
 filter.is_template=模板
+filter.not_template=非模板
 filter.public=公开
 filter.private=私有库
 
+no_results_found=未找到结果
 
 [search]
+search=搜索...
+type_tooltip=搜索类型
+fuzzy=模糊
+match=匹配
+repo_kind=搜索仓库...
+user_kind=搜索用户...
+org_kind=搜索组织...
+team_kind=搜索团队...
+code_kind=搜索代码...
+package_kind=搜索软件包...
+project_kind=搜索项目...
+branch_kind=搜索分支...
+commit_kind=搜索提交记录...
+runner_kind=搜索runners...
+no_results=未找到匹配结果
 
 [aria]
 navbar=导航栏
@@ -315,6 +340,7 @@ env_config_keys=环境配置
 env_config_keys_prompt=以下环境变量也将应用于您的配置文件:
 
 [home]
+nav_menu=导航菜单
 uname_holder=用户名或邮箱
 password_holder=密码
 switch_dashboard_context=切换控制面板用户
@@ -612,6 +638,13 @@ form.name_reserved=用户名 "%s" 被保留。
 form.name_pattern_not_allowed=用户名中不允许使用 "%s" 格式。
 form.name_chars_not_allowed=用户名 "%s" 包含无效字符。
 
+block.block=屏蔽
+block.block.user=屏蔽用户
+block.block.org=屏蔽用户访问组织
+block.block.failure=屏蔽用户失败: %s
+block.unblock=取消屏蔽
+block.title=屏蔽一个用户
+block.info_2=关注你的账号
 
 [settings]
 profile=个人信息
@@ -1275,6 +1308,7 @@ commits.commits=次代码提交
 commits.no_commits=没有共同的提交。%s 和 %s 的历史完全不同。
 commits.nothing_to_compare=这些分支是相同的。
 commits.search.tooltip=`您可以在关键词前加上前缀,如"author:", "committer:", "after:", 或"before:", 例如 "retrin author:Alice before:2019-01-13"`
+commits.search_branch=此分支
 commits.search_all=所有分支
 commits.author=作者
 commits.message=备注
@@ -1333,6 +1367,7 @@ projects.column.new=创建列
 projects.column.set_default=设为默认
 projects.column.set_default_desc=设置此列为未分类问题和合并请求的默认值
 projects.column.delete=删除列
+projects.column.deletion_desc=删除项目列会将所有相关问题移到“未分类”。是否继续?
 projects.column.color=彩色
 projects.open=开启
 projects.close=关闭
@@ -1898,7 +1933,9 @@ wiki.page_name_desc=输入此 Wiki 页面的名称。特殊名称有:'Home', '
 wiki.original_git_entry_tooltip=查看原始的 Git 文件而不是使用友好链接。
 
 activity=动态
+activity.navbar.code_frequency=代码频率
 activity.navbar.contributors=贡献者
+activity.navbar.recent_commits=最近的提交
 activity.period.filter_label=周期:
 activity.period.daily=1 天
 activity.period.halfweekly=3 天
@@ -2017,6 +2054,8 @@ settings.branches.add_new_rule=添加新规则
 settings.advanced_settings=高级设置
 settings.wiki_desc=启用仓库百科
 settings.use_internal_wiki=使用内置百科
+settings.default_wiki_branch_name=默认百科分支名称
+settings.failed_to_change_default_wiki_branch=更改百科默认分支失败。
 settings.use_external_wiki=使用外部百科
 settings.external_wiki_url=外部 Wiki 链接
 settings.external_wiki_url_error=外部百科链接无效
@@ -2047,6 +2086,9 @@ settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者
 settings.releases_desc=启用发布
 settings.packages_desc=启用仓库软件包注册中心
 settings.projects_desc=启用仓库项目
+settings.projects_mode_desc=项目模式 (要显示的项目类型)
+settings.projects_mode_repo=仅仓库项目
+settings.projects_mode_owner=仅限用户或组织项目
 settings.projects_mode_all=所有项目
 settings.actions_desc=启用 Actions
 settings.admin_settings=管理员设置
@@ -2073,6 +2115,7 @@ settings.convert_fork_succeed=此派生仓库已经转换为普通仓库。
 settings.transfer=转移仓库所有权
 settings.transfer.rejected=代码库转移被拒绝。
 settings.transfer.success=代码库转移成功。
+settings.transfer.blocked_user=无法传输仓库,因为您被新的所有者屏蔽。
 settings.transfer_abort=取消转移
 settings.transfer_abort_invalid=你不能取消不存在的代码库转移。
 settings.transfer_abort_success=成功取消了将代码库转让给 %s。
@@ -2118,6 +2161,7 @@ settings.add_collaborator_success=协作者添加成功!
 settings.add_collaborator_inactive_user=无法添加未激活的用户作为合作者。
 settings.add_collaborator_owner=不能将所有者添加为协作者。
 settings.add_collaborator_duplicate=合作者已经被添加到本仓库。
+settings.add_collaborator.blocked_user=此写作者被仓库所有者屏蔽,反之亦然。
 settings.delete_collaborator=删除
 settings.collaborator_deletion=删除协作者
 settings.collaborator_deletion_desc=删除协作者后他将无法再对此仓库的访问。继续?
@@ -2556,13 +2600,16 @@ find_file.no_matching=没有找到匹配的文件
 error.csv.too_large=无法渲染此文件,因为它太大了。
 error.csv.unexpected=无法渲染此文件,因为它包含了意外字符,其位于第 %d 行和第 %d 列。
 error.csv.invalid_field_count=无法渲染此文件,因为它在第 %d 行中的字段数有误。
+error.broken_git_hook=此仓库的 Git 钩子似乎已损坏。 请按照 <a target="_blank" rel="noreferrer" href="%s">文档</a> 来修复它们,然后推送一些提交来刷新状态。
 
 [graphs]
 component_loading=正在加载 %s...
 component_loading_failed=无法加载 %s
 component_loading_info=这可能需要一点…
 component_failed_to_load=意外的错误发生了。
+code_frequency.what=代码频率
 contributors.what=贡献
+recent_commits.what=最近的提交
 
 [org]
 org_name_holder=组织名称
@@ -2676,6 +2723,7 @@ teams.add_nonexistent_repo=您尝试添加的仓库不存在,请先创建它
 teams.add_duplicate_users=用户已经是团队成员。
 teams.repos.none=此团队无法访问任何仓库。
 teams.members.none=团队中没有成员。
+teams.members.blocked_user=不能添加用户因为他已经被该组织屏蔽。
 teams.specific_repositories=指定仓库
 teams.specific_repositories_helper=团队成员将只能访问添加到团队的仓库。 选择此项 <strong>将不会</strong> 自动删除已经添加的仓库。
 teams.all_repositories=所有仓库
@@ -2688,6 +2736,7 @@ teams.invite.by=邀请人 %s
 teams.invite.description=请点击下面的按钮加入团队。
 
 [admin]
+maintenance=维护
 dashboard=管理面板
 self_check=自我检查
 identity_access=身份及认证
@@ -2711,6 +2760,7 @@ settings=管理设置
 
 dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">博客</a> 了解详情。
 dashboard.statistic=摘要
+dashboard.maintenance_operations=运维
 dashboard.system_status=系统状态
 dashboard.operation_name=操作名称
 dashboard.operation_switch=开关
@@ -3001,6 +3051,7 @@ auths.tip.nextcloud=使用下面的菜单“设置(Settings) -> 安全(Sec
 auths.tip.dropbox=在 https://www.dropbox.com/developers/apps 上创建一个新的应用程序
 auths.tip.facebook=`在 https://developers.facebook.com/apps 注册一个新的应用,并添加产品"Facebook 登录"`
 auths.tip.github=在 https://github.com/settings/applications/new 注册一个 OAuth 应用程序
+auths.tip.gitlab_new=在 https://gitlab.com/-/profile/applications 注册一个新的应用
 auths.tip.google_plus=从谷歌 API 控制台 (https://console.developers.google.com/) 获得 OAuth2 客户端凭据
 auths.tip.openid_connect=使用 OpenID 连接发现 URL (<server>/.well-known/openid-configuration) 来指定终点
 auths.tip.twitter=访问 https://dev.twitter.com/apps,创建应用并确保启用了"允许此应用程序用于登录 Twitter"的选项。
@@ -3136,6 +3187,7 @@ config.picture_config=图片和头像配置
 config.picture_service=图片服务
 config.disable_gravatar=禁用 Gravatar 头像
 config.enable_federated_avatar=启用 Federated Avatars
+config.open_with_editor_app_help=用于克隆菜单的编辑器。如果为空将使用默认值。展开可以查看默认值。
 
 config.git_config=Git 配置
 config.git_disable_diff_highlight=禁用差异对比语法高亮
@@ -3215,6 +3267,7 @@ notices.op=操作
 notices.delete_success=系统通知已被删除。
 
 self_check.no_problem_found=尚未发现问题。
+self_check.startup_warnings=启动警告:
 self_check.database_collation_mismatch=期望数据库使用的校验方式:%s
 self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。
 self_check.database_inconsistent_collation_columns=数据库正在使用%s的排序规则,但是这些列使用了不匹配的排序规则。这可能会造成一些意外问题。
@@ -3534,6 +3587,7 @@ runs.scheduled=已计划的
 runs.pushed_by=推送者
 runs.invalid_workflow_helper=工作流配置文件无效。请检查您的配置文件: %s
 runs.no_matching_online_runner_helper=没有匹配标签的在线 runner: %s
+runs.no_job_without_needs=工作流必须包含至少一个没有依赖关系的作业。
 runs.actor=操作者
 runs.status=状态
 runs.actors_no_select=所有操作者

From 07bcfc171bcccfe78a86c7b4b3f9b729ba7d60b6 Mon Sep 17 00:00:00 2001
From: sebastian-sauer <sauer.sebastian@gmail.com>
Date: Fri, 5 Apr 2024 02:51:53 +0200
Subject: [PATCH 625/679] Commit-Dropdown: Show Author of commit if available
 (#30272)

As in commits page we show the author of the commit in the commits
dropdown and not the committer.

Commits Page:
![Screenshot from 2024-04-03
22-34-41](https://github.com/go-gitea/gitea/assets/1135157/1c7c5c19-6d0a-4176-8a87-7bca6a0c6dc8)

and the same contents in our dropdown:

![image](https://github.com/go-gitea/gitea/assets/1135157/aa094af2-c369-47ac-9c27-ca208d1d03f0)


fixes #29588
---
 services/pull/pull.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/services/pull/pull.go b/services/pull/pull.go
index c091b8608a..185a1895c9 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -989,12 +989,12 @@ func GetPullCommits(ctx *gitea_context.Context, issue *issues_model.Issue) ([]Co
 	for _, commit := range prInfo.Commits {
 		var committerOrAuthorName string
 		var commitTime time.Time
-		if commit.Committer != nil {
-			committerOrAuthorName = commit.Committer.Name
-			commitTime = commit.Committer.When
-		} else {
+		if commit.Author != nil {
 			committerOrAuthorName = commit.Author.Name
 			commitTime = commit.Author.When
+		} else {
+			committerOrAuthorName = commit.Committer.Name
+			commitTime = commit.Committer.When
 		}
 
 		commits = append(commits, CommitInfo{

From 95504045cceee9d60ff5d2eeb32cd30213b52322 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 5 Apr 2024 04:45:59 +0200
Subject: [PATCH 626/679] Upgrade `golang.org/x/net` to v0.24.0 (#30283)

Result of `go get -u golang.org/x/net; make tidy`.

This is related to the following vulncheck warning:
```
There are 2 vulnerabilities in modules that you require that are
neither imported nor called. You may not need to take any action.
See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck for details.

Vulnerability #1: GO-2024-2687
    HTTP/2 CONTINUATION flood in net/http
  More info: https://pkg.go.dev/vuln/GO-2024-2687
  Module: golang.org/x/net
    Found in: golang.org/x/net@v0.22.0
    Fixed in: golang.org/x/net@v0.23.0

Vulnerability #2: GO-2022-0470
    No access control in github.com/blevesearch/bleve and bleve/v2
  More info: https://pkg.go.dev/vuln/GO-2022-0470
  Module: github.com/blevesearch/bleve/v2
    Found in: github.com/blevesearch/bleve/v2@v2.3.10
    Fixed in: N/A
```
---
 go.mod |  6 +++---
 go.sum | 16 ++++++++--------
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/go.mod b/go.mod
index b76eb74876..27e1924806 100644
--- a/go.mod
+++ b/go.mod
@@ -105,11 +105,11 @@ require (
 	github.com/yuin/goldmark v1.7.0
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
 	github.com/yuin/goldmark-meta v1.1.0
-	golang.org/x/crypto v0.21.0
+	golang.org/x/crypto v0.22.0
 	golang.org/x/image v0.15.0
-	golang.org/x/net v0.22.0
+	golang.org/x/net v0.24.0
 	golang.org/x/oauth2 v0.18.0
-	golang.org/x/sys v0.18.0
+	golang.org/x/sys v0.19.0
 	golang.org/x/text v0.14.0
 	golang.org/x/tools v0.19.0
 	google.golang.org/grpc v1.62.1
diff --git a/go.sum b/go.sum
index d82110177c..55f24bf2e7 100644
--- a/go.sum
+++ b/go.sum
@@ -846,8 +846,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
 golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
 golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
 golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
@@ -881,8 +881,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
 golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
 golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -932,8 +932,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -943,8 +943,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
-golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

From 5dabc679aa0a33bc1b997335a216acfe97e70ea5 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 5 Apr 2024 05:35:37 +0200
Subject: [PATCH 627/679] Update JS dependencies and add new eslint rules
 (#30279)

- Run `make update-js`
- Added new eslint rules
- Tested webpack build and swagger ui
---
 .eslintrc.yaml    |   5 +-
 package-lock.json | 633 ++++++++++++++++++++++++++++------------------
 package.json      |  24 +-
 3 files changed, 396 insertions(+), 266 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 43edd14cec..5fd0a245f2 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -537,7 +537,7 @@ rules:
   no-underscore-dangle: [0]
   no-unexpected-multiline: [2]
   no-unmodified-loop-condition: [2]
-  no-unneeded-ternary: [0]
+  no-unneeded-ternary: [2]
   no-unreachable-loop: [2]
   no-unreachable: [2]
   no-unsafe-finally: [2]
@@ -716,12 +716,14 @@ rules:
   unicorn/import-style: [0]
   unicorn/new-for-builtins: [2]
   unicorn/no-abusive-eslint-disable: [0]
+  unicorn/no-anonymous-default-export: [0]
   unicorn/no-array-callback-reference: [0]
   unicorn/no-array-for-each: [2]
   unicorn/no-array-method-this-argument: [2]
   unicorn/no-array-push-push: [2]
   unicorn/no-array-reduce: [2]
   unicorn/no-await-expression-member: [0]
+  unicorn/no-await-in-promise-methods: [2]
   unicorn/no-console-spaces: [0]
   unicorn/no-document-cookie: [2]
   unicorn/no-empty-file: [2]
@@ -738,6 +740,7 @@ rules:
   unicorn/no-null: [0]
   unicorn/no-object-as-default-parameter: [0]
   unicorn/no-process-exit: [0]
+  unicorn/no-single-promise-in-promise-methods: [2]
   unicorn/no-static-only-class: [2]
   unicorn/no-thenable: [2]
   unicorn/no-this-assignment: [2]
diff --git a/package-lock.json b/package-lock.json
index a5f7a09ed0..35bf886fc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,7 +21,7 @@
         "chartjs-adapter-dayjs-4": "1.0.4",
         "chartjs-plugin-zoom": "2.0.1",
         "clippie": "4.0.7",
-        "css-loader": "6.10.0",
+        "css-loader": "7.0.0",
         "dayjs": "1.11.10",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
@@ -35,17 +35,17 @@
         "license-checker-webpack-plugin": "0.2.1",
         "mermaid": "10.9.0",
         "mini-css-extract-plugin": "2.8.1",
-        "minimatch": "9.0.3",
+        "minimatch": "9.0.4",
         "monaco-editor": "0.47.0",
         "monaco-editor-webpack-plugin": "7.1.0",
         "pdfobject": "2.3.0",
         "postcss": "8.4.38",
         "postcss-loader": "8.1.1",
-        "postcss-nesting": "12.1.0",
+        "postcss-nesting": "12.1.1",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
-        "swagger-ui-dist": "5.12.0",
-        "tailwindcss": "3.4.1",
+        "swagger-ui-dist": "5.13.0",
+        "tailwindcss": "3.4.3",
         "temporal-polyfill": "0.2.3",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
@@ -66,9 +66,9 @@
       "devDependencies": {
         "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
         "@playwright/test": "1.42.1",
-        "@stoplight/spectral-cli": "6.11.0",
+        "@stoplight/spectral-cli": "6.11.1",
         "@stylistic/eslint-plugin-js": "1.7.0",
-        "@stylistic/stylelint-plugin": "2.1.0",
+        "@stylistic/stylelint-plugin": "2.1.1",
         "@vitejs/plugin-vue": "5.0.4",
         "eslint": "8.57.0",
         "eslint-plugin-array-func": "4.0.0",
@@ -78,17 +78,17 @@
         "eslint-plugin-no-jquery": "2.7.0",
         "eslint-plugin-no-use-extend-native": "0.5.0",
         "eslint-plugin-regexp": "2.4.0",
-        "eslint-plugin-sonarjs": "0.24.0",
-        "eslint-plugin-unicorn": "51.0.1",
-        "eslint-plugin-vitest": "0.4.0",
+        "eslint-plugin-sonarjs": "0.25.1",
+        "eslint-plugin-unicorn": "52.0.0",
+        "eslint-plugin-vitest": "0.4.1",
         "eslint-plugin-vitest-globals": "1.5.0",
         "eslint-plugin-vue": "9.24.0",
         "eslint-plugin-vue-scoped-css": "2.8.0",
         "eslint-plugin-wc": "2.0.4",
-        "happy-dom": "14.3.7",
+        "happy-dom": "14.5.0",
         "markdownlint-cli": "0.39.0",
         "postcss-html": "1.6.0",
-        "stylelint": "16.3.0",
+        "stylelint": "16.3.1",
         "stylelint-declaration-block-no-ignored-properties": "2.8.0",
         "stylelint-declaration-strict-value": "1.10.4",
         "stylelint-value-no-unknown-custom-properties": "6.0.1",
@@ -234,9 +234,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
-      "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
+      "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -245,9 +245,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
-      "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
+      "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -481,9 +481,9 @@
       }
     },
     "node_modules/@csstools/selector-specificity": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz",
-      "integrity": "sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz",
+      "integrity": "sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q==",
       "funding": [
         {
           "type": "github",
@@ -1059,9 +1059,9 @@
       }
     },
     "node_modules/@humanwhocodes/object-schema": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
-      "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
       "dev": true
     },
     "node_modules/@isaacs/cliui": {
@@ -1420,9 +1420,9 @@
       "dev": true
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz",
-      "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz",
+      "integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==",
       "cpu": [
         "arm"
       ],
@@ -1433,9 +1433,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz",
-      "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz",
+      "integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==",
       "cpu": [
         "arm64"
       ],
@@ -1446,9 +1446,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz",
-      "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz",
+      "integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==",
       "cpu": [
         "arm64"
       ],
@@ -1459,9 +1459,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz",
-      "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz",
+      "integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==",
       "cpu": [
         "x64"
       ],
@@ -1472,9 +1472,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz",
-      "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz",
+      "integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==",
       "cpu": [
         "arm"
       ],
@@ -1485,9 +1485,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz",
-      "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz",
+      "integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==",
       "cpu": [
         "arm64"
       ],
@@ -1498,9 +1498,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz",
-      "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz",
+      "integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==",
       "cpu": [
         "arm64"
       ],
@@ -1510,10 +1510,23 @@
         "linux"
       ]
     },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz",
+      "integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==",
+      "cpu": [
+        "ppc64le"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz",
-      "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz",
+      "integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==",
       "cpu": [
         "riscv64"
       ],
@@ -1523,10 +1536,23 @@
         "linux"
       ]
     },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz",
+      "integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz",
-      "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz",
+      "integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==",
       "cpu": [
         "x64"
       ],
@@ -1537,9 +1563,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz",
-      "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz",
+      "integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==",
       "cpu": [
         "x64"
       ],
@@ -1550,9 +1576,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz",
-      "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz",
+      "integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==",
       "cpu": [
         "arm64"
       ],
@@ -1563,9 +1589,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz",
-      "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz",
+      "integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==",
       "cpu": [
         "ia32"
       ],
@@ -1576,9 +1602,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz",
-      "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz",
+      "integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==",
       "cpu": [
         "x64"
       ],
@@ -1686,9 +1712,9 @@
       }
     },
     "node_modules/@stoplight/spectral-cli": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.11.0.tgz",
-      "integrity": "sha512-IURDN47BPIf3q4ZyUPujGpBzuHWFE5yT34w9rTJ1GKA4SgdscEdQO9KoTjOPT4G4cvDlEV3bNxwQ3uRm7+wRlA==",
+      "version": "6.11.1",
+      "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.11.1.tgz",
+      "integrity": "sha512-1zqsQ0TOuVSnxxZ9mHBfC0IygV6ex7nAY6Mp59mLmw5fW103U9yPVK5ZcX9ZngCmr3PdteAnMDUIIaoDGso6nA==",
       "dev": true,
       "dependencies": {
         "@stoplight/json": "~3.21.0",
@@ -1709,7 +1735,7 @@
         "pony-cause": "^1.0.0",
         "stacktracey": "^2.1.7",
         "tslib": "^2.3.0",
-        "yargs": "17.3.1"
+        "yargs": "~17.7.2"
       },
       "bin": {
         "spectral": "dist/index.js"
@@ -1873,20 +1899,33 @@
       }
     },
     "node_modules/@stoplight/spectral-parsers": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.3.tgz",
-      "integrity": "sha512-J0KW5Rh5cHWnJQ3yN+cr/ijNFVirPSR0pkQbdrNX30VboEl083UEDrQ3yov9kjLVIWEk9t9kKE7Eo3QT/k4JLA==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.4.tgz",
+      "integrity": "sha512-nCTVvtX6q71M8o5Uvv9kxU31Gk1TRmgD6/k8HBhdCmKG6FWcwgjiZouA/R3xHLn/VwTI/9k8SdG5Mkdy0RBqbQ==",
       "dev": true,
       "dependencies": {
         "@stoplight/json": "~3.21.0",
-        "@stoplight/types": "^13.6.0",
-        "@stoplight/yaml": "~4.2.3",
+        "@stoplight/types": "^14.1.1",
+        "@stoplight/yaml": "~4.3.0",
         "tslib": "^2.3.1"
       },
       "engines": {
         "node": "^12.20 || >=14.13"
       }
     },
+    "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz",
+      "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.4",
+        "utility-types": "^3.10.0"
+      },
+      "engines": {
+        "node": "^12.20 || >=14.13"
+      }
+    },
     "node_modules/@stoplight/spectral-ref-resolver": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.4.tgz",
@@ -1955,6 +1994,27 @@
         "node": ">=12"
       }
     },
+    "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz",
+      "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==",
+      "dev": true,
+      "dependencies": {
+        "@stoplight/ordered-object-literal": "^1.0.1",
+        "@stoplight/types": "^13.0.0",
+        "@stoplight/yaml-ast-parser": "0.0.48",
+        "tslib": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=10.8"
+      }
+    },
+    "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml-ast-parser": {
+      "version": "0.0.48",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz",
+      "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==",
+      "dev": true
+    },
     "node_modules/@stoplight/spectral-rulesets": {
       "version": "1.18.1",
       "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.18.1.tgz",
@@ -2025,14 +2085,14 @@
       }
     },
     "node_modules/@stoplight/yaml": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz",
-      "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz",
+      "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==",
       "dev": true,
       "dependencies": {
-        "@stoplight/ordered-object-literal": "^1.0.1",
-        "@stoplight/types": "^13.0.0",
-        "@stoplight/yaml-ast-parser": "0.0.48",
+        "@stoplight/ordered-object-literal": "^1.0.5",
+        "@stoplight/types": "^14.1.1",
+        "@stoplight/yaml-ast-parser": "0.0.50",
         "tslib": "^2.2.0"
       },
       "engines": {
@@ -2040,11 +2100,24 @@
       }
     },
     "node_modules/@stoplight/yaml-ast-parser": {
-      "version": "0.0.48",
-      "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz",
-      "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==",
+      "version": "0.0.50",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz",
+      "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==",
       "dev": true
     },
+    "node_modules/@stoplight/yaml/node_modules/@stoplight/types": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz",
+      "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.4",
+        "utility-types": "^3.10.0"
+      },
+      "engines": {
+        "node": "^12.20 || >=14.13"
+      }
+    },
     "node_modules/@stylistic/eslint-plugin-js": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.7.0.tgz",
@@ -2065,9 +2138,9 @@
       }
     },
     "node_modules/@stylistic/stylelint-plugin": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.1.0.tgz",
-      "integrity": "sha512-mUZEW9uImHSbXeyzbFmHb8WPBv56UTaEnWL/3dGdAiJ54C+8GTfDwDVdI6gbqT9wV7zynkPu7tCXc5746H9mZQ==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.1.1.tgz",
+      "integrity": "sha512-xqHTmQZN7EbnFDW7jw0rAsdFNO4IRqvXhrh3qhUlIwF/x09Zm7kgs/ADktHxsTJYcw346PpGihsB0t4pZhpeHw==",
       "dev": true,
       "dependencies": {
         "@csstools/css-parser-algorithms": "^2.5.0",
@@ -2144,9 +2217,9 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.6",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.6.tgz",
-      "integrity": "sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==",
+      "version": "8.56.7",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz",
+      "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==",
       "dependencies": {
         "@types/estree": "*",
         "@types/json-schema": "*"
@@ -2196,9 +2269,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.30",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
-      "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
+      "version": "20.12.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz",
+      "integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -2241,16 +2314,16 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz",
-      "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz",
+      "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "7.4.0",
-        "@typescript-eslint/type-utils": "7.4.0",
-        "@typescript-eslint/utils": "7.4.0",
-        "@typescript-eslint/visitor-keys": "7.4.0",
+        "@typescript-eslint/scope-manager": "7.5.0",
+        "@typescript-eslint/type-utils": "7.5.0",
+        "@typescript-eslint/utils": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
@@ -2276,15 +2349,15 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz",
-      "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
+      "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.4.0",
-        "@typescript-eslint/types": "7.4.0",
-        "@typescript-eslint/typescript-estree": "7.4.0",
-        "@typescript-eslint/visitor-keys": "7.4.0",
+        "@typescript-eslint/scope-manager": "7.5.0",
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/typescript-estree": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2304,13 +2377,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz",
-      "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz",
+      "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.4.0",
-        "@typescript-eslint/visitor-keys": "7.4.0"
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0"
       },
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -2321,13 +2394,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz",
-      "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
+      "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.4.0",
-        "@typescript-eslint/utils": "7.4.0",
+        "@typescript-eslint/typescript-estree": "7.5.0",
+        "@typescript-eslint/utils": "7.5.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2348,9 +2421,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz",
-      "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
+      "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==",
       "dev": true,
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -2361,13 +2434,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz",
-      "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz",
+      "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.4.0",
-        "@typescript-eslint/visitor-keys": "7.4.0",
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2388,18 +2461,33 @@
         }
       }
     },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz",
-      "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
+      "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "7.4.0",
-        "@typescript-eslint/types": "7.4.0",
-        "@typescript-eslint/typescript-estree": "7.4.0",
+        "@typescript-eslint/scope-manager": "7.5.0",
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/typescript-estree": "7.5.0",
         "semver": "^7.5.4"
       },
       "engines": {
@@ -2414,12 +2502,12 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz",
-      "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
+      "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.4.0",
+        "@typescript-eslint/types": "7.5.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2519,9 +2607,9 @@
       }
     },
     "node_modules/@vitest/snapshot/node_modules/magic-string": {
-      "version": "0.30.8",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
-      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "version": "0.30.9",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+      "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
       "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2610,9 +2698,9 @@
       }
     },
     "node_modules/@vue/compiler-sfc/node_modules/magic-string": {
-      "version": "0.30.8",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
-      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "version": "0.30.9",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+      "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
       },
@@ -3478,9 +3566,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001600",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
-      "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
+      "version": "1.0.30001605",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz",
+      "integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -3872,21 +3960,21 @@
       }
     },
     "node_modules/css-loader": {
-      "version": "6.10.0",
-      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz",
-      "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.0.0.tgz",
+      "integrity": "sha512-WrO4FVoamxt5zY9CauZjoJgXRi/LZKIk+Ta7YvpSGr5r/eMYPNp5/T9ODlMe4/1rF5DYlycG1avhV4g3A/tiAw==",
       "dependencies": {
         "icss-utils": "^5.1.0",
         "postcss": "^8.4.33",
-        "postcss-modules-extract-imports": "^3.0.0",
-        "postcss-modules-local-by-default": "^4.0.4",
-        "postcss-modules-scope": "^3.1.1",
+        "postcss-modules-extract-imports": "^3.1.0",
+        "postcss-modules-local-by-default": "^4.0.5",
+        "postcss-modules-scope": "^3.2.0",
         "postcss-modules-values": "^4.0.0",
         "postcss-value-parser": "^4.2.0",
         "semver": "^7.5.4"
       },
       "engines": {
-        "node": ">= 12.13.0"
+        "node": ">= 18.12.0"
       },
       "funding": {
         "type": "opencollective",
@@ -3894,7 +3982,7 @@
       },
       "peerDependencies": {
         "@rspack/core": "0.x || 1.x",
-        "webpack": "^5.0.0"
+        "webpack": "^5.27.0"
       },
       "peerDependenciesMeta": {
         "@rspack/core": {
@@ -4769,9 +4857,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.716",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz",
-      "integrity": "sha512-t/MXMzFKQC3UfMDpw7V5wdB/UAB8dWx4hEsy+fpPYJWW3gqh3u5T1uXp6vR+H6dGCPBxkRo+YBcapBLvbGQHRw=="
+      "version": "1.4.727",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.727.tgz",
+      "integrity": "sha512-brpv4KTeC4g0Fx2FeIKytLd4UGn1zBQq5Lauy7zEWT9oqkaj5mgsxblEZIAOf1HHLlXxzr6adGViiBy5Z39/CA=="
     },
     "node_modules/elkjs": {
       "version": "0.9.2",
@@ -4842,9 +4930,9 @@
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.23.2",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz",
-      "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==",
+      "version": "1.23.3",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
+      "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
       "dev": true,
       "dependencies": {
         "array-buffer-byte-length": "^1.0.1",
@@ -4886,11 +4974,11 @@
         "safe-regex-test": "^1.0.3",
         "string.prototype.trim": "^1.2.9",
         "string.prototype.trimend": "^1.0.8",
-        "string.prototype.trimstart": "^1.0.7",
+        "string.prototype.trimstart": "^1.0.8",
         "typed-array-buffer": "^1.0.2",
         "typed-array-byte-length": "^1.0.1",
         "typed-array-byte-offset": "^1.0.2",
-        "typed-array-length": "^1.0.5",
+        "typed-array-length": "^1.0.6",
         "unbox-primitive": "^1.0.2",
         "which-typed-array": "^1.1.15"
       },
@@ -5622,9 +5710,9 @@
       }
     },
     "node_modules/eslint-plugin-sonarjs": {
-      "version": "0.24.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.24.0.tgz",
-      "integrity": "sha512-87zp50mbbNrSTuoEOebdRQBPa0mdejA5UEjyuScyIw8hEpEjfWP89Qhkq5xVZfVyVSRQKZc9alVm7yRKQvvUmg==",
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.1.tgz",
+      "integrity": "sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==",
       "dev": true,
       "engines": {
         "node": ">=16"
@@ -5634,9 +5722,9 @@
       }
     },
     "node_modules/eslint-plugin-unicorn": {
-      "version": "51.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz",
-      "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==",
+      "version": "52.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
+      "integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
       "dev": true,
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
@@ -5667,12 +5755,12 @@
       }
     },
     "node_modules/eslint-plugin-vitest": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.4.0.tgz",
-      "integrity": "sha512-3oWgZIwdWVBQ5plvkmOBjreIGLQRdYb7x54OP8uIRHeZyRVJIdOn9o/qWVb9292fDMC8jn7H7d9TSFBZqhrykQ==",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.4.1.tgz",
+      "integrity": "sha512-+PnZ2u/BS+f5FiuHXz4zKsHPcMKHie+K+1Uvu/x91ovkCMEOJqEI8E9Tw1Wzx2QRz4MHOBHYf1ypO8N1K0aNAA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/utils": "^7.2.0"
+        "@typescript-eslint/utils": "^7.4.0"
       },
       "engines": {
         "node": "^18.0.0 || >= 20.0.0"
@@ -6506,9 +6594,9 @@
       }
     },
     "node_modules/happy-dom": {
-      "version": "14.3.7",
-      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.3.7.tgz",
-      "integrity": "sha512-lUfDRGzjrVJF2pnvh13OL+qEJ9eDpcedVLm77a3aMg8gPGKXfG+xFMNk3cOWetjucU8FveJ4qcSC/EX55nJ4fQ==",
+      "version": "14.5.0",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.5.0.tgz",
+      "integrity": "sha512-KvOtCq7eamc7cjihM0F1wj6FptuXzooc3Typa7Vgu6ns2uKGXC4BIFlK80SdH2w8zcW0gtxpBVI/sUqbYtljDA==",
       "dev": true,
       "dependencies": {
         "entities": "^4.5.0",
@@ -7956,16 +8044,16 @@
       }
     },
     "node_modules/markdownlint-cli/node_modules/glob": {
-      "version": "10.3.10",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
-      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "version": "10.3.12",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+      "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
       "dev": true,
       "dependencies": {
         "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.5",
+        "jackspeak": "^2.3.6",
         "minimatch": "^9.0.1",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-        "path-scurry": "^1.10.1"
+        "minipass": "^7.0.4",
+        "path-scurry": "^1.10.2"
       },
       "bin": {
         "glob": "dist/esm/bin.mjs"
@@ -8608,9 +8696,9 @@
       }
     },
     "node_modules/minimatch": {
-      "version": "9.0.3",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
-      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "version": "9.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+      "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -9133,11 +9221,11 @@
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
     },
     "node_modules/path-scurry": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
-      "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
+      "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
       "dependencies": {
-        "lru-cache": "^9.1.1 || ^10.0.0",
+        "lru-cache": "^10.2.0",
         "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
       },
       "engines": {
@@ -9456,9 +9544,9 @@
       }
     },
     "node_modules/postcss-modules-extract-imports": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
-      "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+      "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
       "engines": {
         "node": "^10 || ^12 || >= 14"
       },
@@ -9467,9 +9555,9 @@
       }
     },
     "node_modules/postcss-modules-local-by-default": {
-      "version": "4.0.4",
-      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz",
-      "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==",
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz",
+      "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==",
       "dependencies": {
         "icss-utils": "^5.0.0",
         "postcss-selector-parser": "^6.0.2",
@@ -9483,9 +9571,9 @@
       }
     },
     "node_modules/postcss-modules-scope": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz",
-      "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz",
+      "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==",
       "dependencies": {
         "postcss-selector-parser": "^6.0.4"
       },
@@ -9529,9 +9617,9 @@
       }
     },
     "node_modules/postcss-nesting": {
-      "version": "12.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.0.tgz",
-      "integrity": "sha512-QOYnosaZ+mlP6plQrAxFw09UUp2Sgtxj1BVHN+rSVbtV0Yx48zRt9/9F/ZOoxOKBBEsaJk2MYhhVRjeRRw5yuw==",
+      "version": "12.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.1.tgz",
+      "integrity": "sha512-qc74KvIAQNa5ujZKG1UV286dhaDW6basbUy2i9AzNU/T8C9hpvGu9NZzm1SfePe2yP7sPYgpA8d4sPVopn2Hhw==",
       "funding": [
         {
           "type": "github",
@@ -9544,7 +9632,7 @@
       ],
       "dependencies": {
         "@csstools/selector-resolve-nested": "^1.1.0",
-        "@csstools/selector-specificity": "^3.0.2",
+        "@csstools/selector-specificity": "^3.0.3",
         "postcss-selector-parser": "^6.0.13"
       },
       "engines": {
@@ -10765,17 +10853,23 @@
       }
     },
     "node_modules/strip-literal": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz",
-      "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz",
+      "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==",
       "dev": true,
       "dependencies": {
-        "js-tokens": "^8.0.2"
+        "js-tokens": "^9.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/strip-literal/node_modules/js-tokens": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz",
+      "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==",
+      "dev": true
+    },
     "node_modules/style-search": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
@@ -10783,9 +10877,9 @@
       "dev": true
     },
     "node_modules/stylelint": {
-      "version": "16.3.0",
-      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.3.0.tgz",
-      "integrity": "sha512-hqC6xNTbQ5HRGQXfIW4HwXcx09raIFz4W4XFbraeqWqYRVVY/ibYvI0dsu0ORMQY8re2bpDdCAeIa2cm+QJ4Sw==",
+      "version": "16.3.1",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.3.1.tgz",
+      "integrity": "sha512-/JOwQnBvxEKOT2RtNgGpBVXnCSMBgKOL2k7w0K52htwCyJls4+cHvc4YZgXlVoAZS9QJd2DgYAiRnja96pTgxw==",
       "dev": true,
       "dependencies": {
         "@csstools/css-parser-algorithms": "^2.6.1",
@@ -11036,15 +11130,15 @@
       }
     },
     "node_modules/sucrase/node_modules/glob": {
-      "version": "10.3.10",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
-      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "version": "10.3.12",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+      "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
       "dependencies": {
         "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.5",
+        "jackspeak": "^2.3.6",
         "minimatch": "^9.0.1",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-        "path-scurry": "^1.10.1"
+        "minipass": "^7.0.4",
+        "path-scurry": "^1.10.2"
       },
       "bin": {
         "glob": "dist/esm/bin.mjs"
@@ -11147,9 +11241,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.12.0",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.12.0.tgz",
-      "integrity": "sha512-Rt1xUpbHulJVGbiQjq9yy9/r/0Pg6TmpcG+fXTaMePDc8z5WUw4LfaWts5qcNv/8ewPvBIbY7DKq7qReIKNCCQ=="
+      "version": "5.13.0",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.13.0.tgz",
+      "integrity": "sha512-uaWhh6j18IIs5tOX0arvIBnVINAzpTXaQXkr7qAk8zoupegJVg0UU/5+S/FgsgVCnzVsJ9d7QLjIxkswEeTg0Q=="
     },
     "node_modules/sync-fetch": {
       "version": "0.4.5",
@@ -11180,9 +11274,9 @@
       }
     },
     "node_modules/table": {
-      "version": "6.8.1",
-      "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
-      "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+      "version": "6.8.2",
+      "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz",
+      "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==",
       "dev": true,
       "dependencies": {
         "ajv": "^8.0.1",
@@ -11196,9 +11290,9 @@
       }
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
-      "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
+      "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
         "arg": "^5.0.2",
@@ -11208,7 +11302,7 @@
         "fast-glob": "^3.3.0",
         "glob-parent": "^6.0.2",
         "is-glob": "^4.0.3",
-        "jiti": "^1.19.1",
+        "jiti": "^1.21.0",
         "lilconfig": "^2.1.0",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
@@ -11298,9 +11392,9 @@
       "integrity": "sha512-r1AT0XdEp8TMQ13FLvOt8mOtAxDQsRt2QU5rSWCA7YfshddU651Y1NHVrceLANvixKdf9fYO8B/S9fXHodB7HQ=="
     },
     "node_modules/terser": {
-      "version": "5.29.2",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
-      "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==",
+      "version": "5.30.3",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
+      "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
@@ -11655,9 +11749,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.4.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
-      "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
+      "version": "5.4.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
+      "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
       "devOptional": true,
       "peer": true,
       "bin": {
@@ -11854,13 +11948,13 @@
       "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
     },
     "node_modules/vite": {
-      "version": "5.2.6",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
-      "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==",
+      "version": "5.2.8",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
+      "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
       "dev": true,
       "dependencies": {
         "esbuild": "^0.20.1",
-        "postcss": "^8.4.36",
+        "postcss": "^8.4.38",
         "rollup": "^4.13.0"
       },
       "bin": {
@@ -11957,9 +12051,9 @@
       }
     },
     "node_modules/vite/node_modules/rollup": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
-      "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz",
+      "integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==",
       "dev": true,
       "dependencies": {
         "@types/estree": "1.0.5"
@@ -11972,19 +12066,21 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.13.0",
-        "@rollup/rollup-android-arm64": "4.13.0",
-        "@rollup/rollup-darwin-arm64": "4.13.0",
-        "@rollup/rollup-darwin-x64": "4.13.0",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.13.0",
-        "@rollup/rollup-linux-arm64-gnu": "4.13.0",
-        "@rollup/rollup-linux-arm64-musl": "4.13.0",
-        "@rollup/rollup-linux-riscv64-gnu": "4.13.0",
-        "@rollup/rollup-linux-x64-gnu": "4.13.0",
-        "@rollup/rollup-linux-x64-musl": "4.13.0",
-        "@rollup/rollup-win32-arm64-msvc": "4.13.0",
-        "@rollup/rollup-win32-ia32-msvc": "4.13.0",
-        "@rollup/rollup-win32-x64-msvc": "4.13.0",
+        "@rollup/rollup-android-arm-eabi": "4.14.0",
+        "@rollup/rollup-android-arm64": "4.14.0",
+        "@rollup/rollup-darwin-arm64": "4.14.0",
+        "@rollup/rollup-darwin-x64": "4.14.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.14.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.14.0",
+        "@rollup/rollup-linux-arm64-musl": "4.14.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.14.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.14.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.14.0",
+        "@rollup/rollup-linux-x64-gnu": "4.14.0",
+        "@rollup/rollup-linux-x64-musl": "4.14.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.14.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.14.0",
+        "@rollup/rollup-win32-x64-msvc": "4.14.0",
         "fsevents": "~2.3.2"
       }
     },
@@ -12054,9 +12150,9 @@
       }
     },
     "node_modules/vitest/node_modules/magic-string": {
-      "version": "0.30.8",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
-      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "version": "0.30.9",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+      "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
       "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -12664,18 +12760,18 @@
       }
     },
     "node_modules/yargs": {
-      "version": "17.3.1",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
-      "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
       "dev": true,
       "dependencies": {
-        "cliui": "^7.0.2",
+        "cliui": "^8.0.1",
         "escalade": "^3.1.1",
         "get-caller-file": "^2.0.5",
         "require-directory": "^2.1.1",
         "string-width": "^4.2.3",
         "y18n": "^5.0.5",
-        "yargs-parser": "^21.0.0"
+        "yargs-parser": "^21.1.1"
       },
       "engines": {
         "node": ">=12"
@@ -12690,6 +12786,37 @@
         "node": ">=12"
       }
     },
+    "node_modules/yargs/node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 004ac9e2bf..f58c3b4d8f 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
     "chartjs-adapter-dayjs-4": "1.0.4",
     "chartjs-plugin-zoom": "2.0.1",
     "clippie": "4.0.7",
-    "css-loader": "6.10.0",
+    "css-loader": "7.0.0",
     "dayjs": "1.11.10",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
@@ -34,17 +34,17 @@
     "license-checker-webpack-plugin": "0.2.1",
     "mermaid": "10.9.0",
     "mini-css-extract-plugin": "2.8.1",
-    "minimatch": "9.0.3",
+    "minimatch": "9.0.4",
     "monaco-editor": "0.47.0",
     "monaco-editor-webpack-plugin": "7.1.0",
     "pdfobject": "2.3.0",
     "postcss": "8.4.38",
     "postcss-loader": "8.1.1",
-    "postcss-nesting": "12.1.0",
+    "postcss-nesting": "12.1.1",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
-    "swagger-ui-dist": "5.12.0",
-    "tailwindcss": "3.4.1",
+    "swagger-ui-dist": "5.13.0",
+    "tailwindcss": "3.4.3",
     "temporal-polyfill": "0.2.3",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
@@ -65,9 +65,9 @@
   "devDependencies": {
     "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
     "@playwright/test": "1.42.1",
-    "@stoplight/spectral-cli": "6.11.0",
+    "@stoplight/spectral-cli": "6.11.1",
     "@stylistic/eslint-plugin-js": "1.7.0",
-    "@stylistic/stylelint-plugin": "2.1.0",
+    "@stylistic/stylelint-plugin": "2.1.1",
     "@vitejs/plugin-vue": "5.0.4",
     "eslint": "8.57.0",
     "eslint-plugin-array-func": "4.0.0",
@@ -77,17 +77,17 @@
     "eslint-plugin-no-jquery": "2.7.0",
     "eslint-plugin-no-use-extend-native": "0.5.0",
     "eslint-plugin-regexp": "2.4.0",
-    "eslint-plugin-sonarjs": "0.24.0",
-    "eslint-plugin-unicorn": "51.0.1",
-    "eslint-plugin-vitest": "0.4.0",
+    "eslint-plugin-sonarjs": "0.25.1",
+    "eslint-plugin-unicorn": "52.0.0",
+    "eslint-plugin-vitest": "0.4.1",
     "eslint-plugin-vitest-globals": "1.5.0",
     "eslint-plugin-vue": "9.24.0",
     "eslint-plugin-vue-scoped-css": "2.8.0",
     "eslint-plugin-wc": "2.0.4",
-    "happy-dom": "14.3.7",
+    "happy-dom": "14.5.0",
     "markdownlint-cli": "0.39.0",
     "postcss-html": "1.6.0",
-    "stylelint": "16.3.0",
+    "stylelint": "16.3.1",
     "stylelint-declaration-block-no-ignored-properties": "2.8.0",
     "stylelint-declaration-strict-value": "1.10.4",
     "stylelint-value-no-unknown-custom-properties": "6.0.1",

From 556099fa72f6239aa9446d06265876bc72b021b8 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 5 Apr 2024 13:11:26 +0200
Subject: [PATCH 628/679] Add gap to commit status details (#30284)

Before:
<img width="162" alt="Screenshot 2024-04-05 at 02 25 27"
src="https://github.com/go-gitea/gitea/assets/115237/9f786811-3e45-4b3c-aaf9-e1d2cad284d2">

After:
<img width="172" alt="Screenshot 2024-04-05 at 02 27 25"
src="https://github.com/go-gitea/gitea/assets/115237/f5254877-9e0d-44cb-9605-ba15c75872bb">
---
 web_src/css/repo.css | 1 +
 1 file changed, 1 insertion(+)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 705d652b54..653af379d5 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2855,6 +2855,7 @@ tbody.commit-list {
   display: flex;
   align-items: center;
   justify-content: flex-end;
+  gap: 8px;
 }
 
 @media (max-width: 767.98px) {

From b2b49c9bde63b311f01d500ada9212c46b9602fe Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 6 Apr 2024 02:03:07 +0800
Subject: [PATCH 629/679] Fix view commit link (#30297)

Fix #30098
---
 templates/repo/commits_list.tmpl | 173 +++++++++++++++----------------
 1 file changed, 86 insertions(+), 87 deletions(-)

diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index 53052333fa..be73c4ca18 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -1,92 +1,91 @@
 <div class="ui attached table segment commit-table">
-		<table class="ui very basic striped table unstackable" id="commits-table">
-			<thead>
+	<table class="ui very basic striped table unstackable" id="commits-table">
+		<thead>
+			<tr>
+				<th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th>
+				<th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th>
+				<th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th>
+				<th class="two wide right aligned">{{ctx.Locale.Tr "repo.commits.date"}}</th>
+				<th class="one wide"></th>
+			</tr>
+		</thead>
+		<tbody class="commit-list">
+			{{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}}
+			{{range .Commits}}
 				<tr>
-					<th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th>
-					<th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th>
-					<th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th>
-					<th class="two wide right aligned">{{ctx.Locale.Tr "repo.commits.date"}}</th>
-					<th class="one wide"></th>
-				</tr>
-			</thead>
-			<tbody class="commit-list">
-				{{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}}
-				{{range .Commits}}
-					<tr>
-						<td class="author tw-flex">
-							{{$userName := .Author.Name}}
-							{{if .User}}
-								{{if and .User.FullName DefaultShowFullName}}
-									{{$userName = .User.FullName}}
-								{{end}}
-								{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
-							{{else}}
-								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}}
-								<span class="author-wrapper">{{$userName}}</span>
+					<td class="author tw-flex">
+						{{$userName := .Author.Name}}
+						{{if .User}}
+							{{if and .User.FullName DefaultShowFullName}}
+								{{$userName = .User.FullName}}
 							{{end}}
-						</td>
-						<td class="sha">
-							{{$class := "ui sha label"}}
-							{{if .Signature}}
-								{{$class = (print $class " isSigned")}}
-								{{if .Verification.Verified}}
-									{{if eq .Verification.TrustStatus "trusted"}}
-										{{$class = (print $class " isVerified")}}
-									{{else if eq .Verification.TrustStatus "untrusted"}}
-										{{$class = (print $class " isVerifiedUntrusted")}}
-									{{else}}
-										{{$class = (print $class " isVerifiedUnmatched")}}
-									{{end}}
-								{{else if .Verification.Warning}}
-									{{$class = (print $class " isWarning")}}
-								{{end}}
-							{{end}}
-							{{$commitShaLink := ""}}
-							{{if $.PageIsWiki}}
-								{{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
-							{{else if $.PageIsPullCommits}}
-								{{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
-							{{else if $.Reponame}}
-								{{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
-							{{end}}
-							<a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}">
-								<span class="shortsha">{{ShortSha .ID.String}}</span>
-								{{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}}
-							</a>
-						</td>
-						<td class="message">
-							<span class="message-wrapper">
-							{{if $.PageIsWiki}}
-								<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span>
-							{{else}}
-								{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
-								<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
-							{{end}}
-							</span>
-							{{if IsMultilineCommitMessage .Message}}
-							<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
-							{{end}}
-							{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
-							{{if IsMultilineCommitMessage .Message}}
-							<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
-							{{end}}
-						</td>
-						{{if .Committer}}
-							<td class="text right aligned">{{TimeSince .Committer.When ctx.Locale}}</td>
+							{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
 						{{else}}
-							<td class="text right aligned">{{TimeSince .Author.When ctx.Locale}}</td>
+							{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}}
+							<span class="author-wrapper">{{$userName}}</span>
 						{{end}}
-						<td class="text right aligned tw-py-0">
-							<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
-							<a
-								class="btn interact-bg tw-p-2"
-								data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}"
-								href="{{if $.FileName}}{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}{{else}}{{printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}{{end}}">
-								{{svg "octicon-file-code"}}
-							</a>
-						</td>
-					</tr>
-				{{end}}
-			</tbody>
-		</table>
-	</div>
+					</td>
+					<td class="sha">
+						{{$class := "ui sha label"}}
+						{{if .Signature}}
+							{{$class = (print $class " isSigned")}}
+							{{if .Verification.Verified}}
+								{{if eq .Verification.TrustStatus "trusted"}}
+									{{$class = (print $class " isVerified")}}
+								{{else if eq .Verification.TrustStatus "untrusted"}}
+									{{$class = (print $class " isVerifiedUntrusted")}}
+								{{else}}
+									{{$class = (print $class " isVerifiedUnmatched")}}
+								{{end}}
+							{{else if .Verification.Warning}}
+								{{$class = (print $class " isWarning")}}
+							{{end}}
+						{{end}}
+						{{$commitShaLink := ""}}
+						{{if $.PageIsWiki}}
+							{{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
+						{{else if $.PageIsPullCommits}}
+							{{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
+						{{else if $.Reponame}}
+							{{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
+						{{end}}
+						<a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}">
+							<span class="shortsha">{{ShortSha .ID.String}}</span>
+							{{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}}
+						</a>
+					</td>
+					<td class="message">
+						<span class="message-wrapper">
+						{{if $.PageIsWiki}}
+							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span>
+						{{else}}
+							{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
+							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
+						{{end}}
+						</span>
+						{{if IsMultilineCommitMessage .Message}}
+						<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
+						{{end}}
+						{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
+						{{if IsMultilineCommitMessage .Message}}
+						<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
+						{{end}}
+					</td>
+					{{if .Committer}}
+						<td class="text right aligned">{{TimeSince .Committer.When ctx.Locale}}</td>
+					{{else}}
+						<td class="text right aligned">{{TimeSince .Author.When ctx.Locale}}</td>
+					{{end}}
+					<td class="text right aligned tw-py-0">
+						<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
+						{{if not $.PageIsWiki}}{{/* at the moment, wiki doesn't support "view at history point*/}}
+							{{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
+							{{if $.FileName}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileName)}}{{end}}
+							<a class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{$viewCommitLink}}">{{svg "octicon-file-code"}}</a>
+						{{end}}
+					</td>
+				</tr>
+			{{end}}
+		</tbody>
+	</table>
+</div>

From 9c1f4dae2ee85b748250ba7b161d70bd529088d3 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 6 Apr 2024 10:25:39 +0200
Subject: [PATCH 630/679] Always use `octicon-eye` on watch button (#30288)

This might appear odd but I think it's the right thing to do: On Github,
the "Watch" button always has the open eye icon:

<img width="177" alt="Screenshot 2024-04-05 at 08 26 48"
src="https://github.com/go-gitea/gitea/assets/115237/0c1188d1-145b-4c6d-909f-2e1460499941">
<img width="179" alt="Screenshot 2024-04-05 at 08 26 40"
src="https://github.com/go-gitea/gitea/assets/115237/e29d91fa-f122-4e10-9589-f79c1d612cf9">

On Gitea, while watching, the icon is this and this sometimes confuses
me slightly, being used to above:

<img width="158" alt="Screenshot 2024-04-05 at 08 29 08"
src="https://github.com/go-gitea/gitea/assets/115237/3301021b-744e-409f-a9d8-887ec2772fdc">

After this PR, both states will use the same icon:

<img width="145" alt="Screenshot 2024-04-05 at 08 26 27"
src="https://github.com/go-gitea/gitea/assets/115237/8addfa5b-c009-4bdb-bfa1-4f3dfaffa4cd">
<img width="161" alt="Screenshot 2024-04-05 at 08 26 33"
src="https://github.com/go-gitea/gitea/assets/115237/cef383e6-2cc0-460f-a4d3-83ebb321debe">
---
 templates/repo/watch_unwatch.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/watch_unwatch.tmpl b/templates/repo/watch_unwatch.tmpl
index 2bf2c7bd21..64be971416 100644
--- a/templates/repo/watch_unwatch.tmpl
+++ b/templates/repo/watch_unwatch.tmpl
@@ -3,7 +3,7 @@
 		{{$buttonText := ctx.Locale.Tr "repo.watch"}}
 		{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}
 		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}} aria-label="{{$buttonText}}">
-			{{if $.IsWatchingRepo}}{{svg "octicon-eye-closed"}}{{else}}{{svg "octicon-eye"}}{{end}}
+			{{svg "octicon-eye"}}
 			<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
 		</button>
 		<a hx-boost="false" class="ui basic label" href="{{.RepoLink}}/watchers">

From 7396172a02a9ea8d80f9763469fd65a5a12ff3f7 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 6 Apr 2024 20:07:08 +0800
Subject: [PATCH 631/679] Fix code block style for code preview (#30298)

Fix #30292

To avoid unnecessary style overriding, use "div" instead of "code"
---
 modules/markup/sanitizer.go                         | 2 +-
 services/markup/processorhelper_codepreview_test.go | 6 +++---
 templates/base/markup_codepreview.tmpl              | 2 +-
 web_src/css/markup/content.css                      | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 77fbdf4520..570a1da248 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -65,7 +65,7 @@ func createDefaultPolicy() *bluemonday.Policy {
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
 	policy.AllowAttrs("data-line-number").OnElements("span")
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("code")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
 
 	// For code preview (unicode escape)
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/processorhelper_codepreview_test.go
index 01db792925..154e4e8e44 100644
--- a/services/markup/processorhelper_codepreview_test.go
+++ b/services/markup/processorhelper_codepreview_test.go
@@ -36,10 +36,10 @@ func TestProcessorHelperCodePreview(t *testing.T) {
 	<table class="file-view">
 		<tbody><tr>
 				<td class="lines-num"><span data-line-number="1"></span></td>
-				<td class="lines-code chroma"><code class="code-inner"><span class="gh"># repo1</code></td>
+				<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td>
 			</tr><tr>
 				<td class="lines-num"><span data-line-number="2"></span></td>
-				<td class="lines-code chroma"><code class="code-inner"></span><span class="gh"></span></code></td>
+				<td class="lines-code chroma"><div class="code-inner"></span><span class="gh"></span></div></td>
 			</tr></tbody>
 	</table>
 </div>
@@ -63,7 +63,7 @@ func TestProcessorHelperCodePreview(t *testing.T) {
 	<table class="file-view">
 		<tbody><tr>
 				<td class="lines-num"><span data-line-number="1"></span></td>
-				<td class="lines-code chroma"><code class="code-inner"><span class="gh"># repo1</code></td>
+				<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td>
 			</tr></tbody>
 	</table>
 </div>
diff --git a/templates/base/markup_codepreview.tmpl b/templates/base/markup_codepreview.tmpl
index c65ab28406..a1a4f26b47 100644
--- a/templates/base/markup_codepreview.tmpl
+++ b/templates/base/markup_codepreview.tmpl
@@ -17,7 +17,7 @@
 					{{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}}
 					<td class="lines-escape">{{if $lineEscapeStatus.Escaped}}<a href="#" class="toggle-escape-button btn interact-bg" title="{{if $lineEscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if $lineEscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></a>{{end}}</td>
 				{{- end}}
-				<td class="lines-code chroma"><code class="code-inner">{{$line.FormattedContent}}</code></td>
+				<td class="lines-code chroma"><div class="code-inner">{{$line.FormattedContent}}</div></td>{{/* only div works, span generates incorrect HTML structure */}}
 			</tr>
 			{{- end -}}
 		</tbody>
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index 376d3030c7..d44e727a25 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -432,7 +432,7 @@
   text-align: right;
 }
 
-.markup code:not(.code-inner),
+.markup code,
 .markup tt {
   padding: 0.2em 0.4em;
   margin: 0;

From 662eb4b0852f9ce2c161e7fea5ac66bf912fc9f6 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 6 Apr 2024 23:06:27 +0200
Subject: [PATCH 632/679] Markup color and font size fixes (#30282)

1. Distinguish inline an block code with new CSS variable
`--color-markup-code-inline`
2. Various color tweaks, better contrast from background

<img width="447" alt="Screenshot 2024-04-05 at 00 51 00"
src="https://github.com/go-gitea/gitea/assets/115237/93e069f4-6807-4f2c-9331-2d69730919d4">
<img width="456" alt="Screenshot 2024-04-05 at 00 50 44"
src="https://github.com/go-gitea/gitea/assets/115237/0dc9c745-c531-40fa-94ec-b0ba10bd7ccf">
---
 web_src/css/markup/content.css           | 4 ++--
 web_src/css/themes/theme-gitea-dark.css  | 5 +++--
 web_src/css/themes/theme-gitea-light.css | 5 +++--
 3 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index d44e727a25..6ba4e40072 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -438,7 +438,7 @@
   margin: 0;
   font-size: 85%;
   white-space: break-spaces;
-  background-color: var(--color-markup-code-block);
+  background-color: var(--color-markup-code-inline);
   border-radius: var(--border-radius);
 }
 
@@ -508,7 +508,7 @@
   line-height: 10px;
   color: var(--color-text-light);
   vertical-align: middle;
-  background-color: var(--color-markup-code-block);
+  background-color: var(--color-markup-code-inline);
   border: 1px solid var(--color-secondary);
   border-radius: var(--border-radius);
   box-shadow: inset 0 -1px 0 var(--color-secondary);
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index 626590ca54..07e217742d 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -204,8 +204,9 @@
   --color-active: #e8e8ff24;
   --color-menu: #151a1e;
   --color-card: #151a1e;
-  --color-markup-table-row: #e8e8ff06;
-  --color-markup-code-block: #e8e8ff16;
+  --color-markup-table-row: #e8e8ff0f;
+  --color-markup-code-block: #e8e8ff12;
+  --color-markup-code-inline: #e8e8ff28;
   --color-button: #151a1e;
   --color-code-bg: #14171a;
   --color-shadow: #00001758;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index f6913fbe22..2741e0e0bd 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -204,8 +204,9 @@
   --color-active: #00001714;
   --color-menu: #f8f9fb;
   --color-card: #f8f9fb;
-  --color-markup-table-row: #00001708;
-  --color-markup-code-block: #00001710;
+  --color-markup-table-row: #0030600a;
+  --color-markup-code-block: #00306010;
+  --color-markup-code-inline: #00306012;
   --color-button: #f8f9fb;
   --color-code-bg: #fafdff;
   --color-shadow: #00001726;

From 649aada3664f5adccdaecc7dd24b8252ae070220 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 6 Apr 2024 23:33:45 +0200
Subject: [PATCH 633/679] Remove fomantic list module (#30281)

Likely still some unnecessary CSS but any combinations with the `ui
list` classes are covered. There was only on instance of `horizontal
list` which I removed. It was this part of the commit page:

<img width="396" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/c49ec4f5-93c3-41d6-a907-cdbedf8abc44">
---
 templates/repo/commit_page.tmpl     |   4 +-
 templates/user/settings/repos.tmpl  |   2 +-
 web_src/css/base.css                |  29 +-
 web_src/css/index.css               |   1 +
 web_src/css/modules/list.css        | 187 ++++++
 web_src/fomantic/build/semantic.css | 977 ----------------------------
 web_src/fomantic/semantic.json      |   1 -
 7 files changed, 192 insertions(+), 1009 deletions(-)
 create mode 100644 web_src/css/modules/list.css

diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 3ae7fffa1c..49a0b445b1 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -164,9 +164,9 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="ui horizontal list tw-flex tw-items-center">
+				<div class="tw-flex tw-items-center">
 					{{if .Parents}}
-						<div class="item">
+						<div>
 							<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
 							{{range .Parents}}
 								{{if $.PageIsWiki}}
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index c874ccd878..26b9dfeed9 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -6,7 +6,7 @@
 		<div class="ui attached segment">
 			{{if or .allowAdopt .allowDelete}}
 				{{if .Dirs}}
-					<div class="ui middle aligned divided list">
+					<div class="ui list">
 						{{range $dirI, $dir := .Dirs}}
 							{{$repo := index $.ReposMap $dir}}
 							<div class="item {{if not $repo}}tw-py-1{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}}
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 05ddba3223..096b67058e 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -44,7 +44,7 @@ html, body {
 }
 
 body {
-  line-height: 1.4285rem;
+  line-height: 20px;
   font-family: var(--fonts-regular);
   color: var(--color-text);
   background-color: var(--color-body);
@@ -305,14 +305,6 @@ a.label,
   background-color: var(--color-label-bg);
 }
 
-/* fix Fomantic's line-height causing vertical scrollbars to appear */
-ul.ui.list li,
-ol.ui.list li,
-.ui.list > .item,
-.ui.list .list > .item {
-  line-height: var(--line-height-default);
-}
-
 .ui.menu {
   display: flex;
 }
@@ -456,21 +448,6 @@ ol.ui.list li,
   color: var(--color-text-light-2);
 }
 
-.ui.list .list > .item .header,
-.ui.list > .item .header {
-  color: var(--color-text-dark);
-}
-
-.ui.list .list > .item > .content,
-.ui.list > .item > .content {
-  color: var(--color-text);
-}
-
-.ui.list .list > .item .description,
-.ui.list > .item .description {
-  color: var(--color-text);
-}
-
 /* replace item margin on secondary menu items with gap and remove both the
    negative margins on the menu as well as margin on the items */
 .ui.secondary.menu {
@@ -589,10 +566,6 @@ img.ui.avatar,
   aspect-ratio: 1;
 }
 
-.ui.divided.list > .item {
-  border-color: var(--color-secondary);
-}
-
 .ui.error.message .header,
 .ui.warning.message .header {
   color: inherit;
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 7be8065dc7..ad59f32636 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -8,6 +8,7 @@
 @import "./modules/header.css";
 @import "./modules/input.css";
 @import "./modules/label.css";
+@import "./modules/list.css";
 @import "./modules/segment.css";
 @import "./modules/grid.css";
 @import "./modules/message.css";
diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css
new file mode 100644
index 0000000000..73760390de
--- /dev/null
+++ b/web_src/css/modules/list.css
@@ -0,0 +1,187 @@
+/* based on Fomantic UI list module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.list {
+  list-style-type: none;
+  margin: 1em 0;
+  padding: 0;
+  font-size: 1em;
+}
+
+.ui.list:first-child {
+  margin-top: 0;
+  padding-top: 0;
+}
+
+.ui.list:last-child {
+  margin-bottom: 0;
+  padding-bottom: 0;
+}
+
+.ui.list > .item,
+.ui.list .list > .item {
+  display: list-item;
+  table-layout: fixed;
+  list-style-type: none;
+  list-style-position: outside;
+}
+
+.ui.list > .list > .item::after,
+.ui.list > .item::after {
+  content: "";
+  display: block;
+  height: 0;
+  clear: both;
+  visibility: hidden;
+}
+
+.ui.list .list:not(.icon) {
+  clear: both;
+  margin: 0;
+  padding: 0.75em 0 0.25em 0.5em;
+}
+
+.ui.list .list > .item {
+  padding: 0.14285714em 0;
+}
+
+.ui.list .list > .item > i.icon,
+.ui.list > .item > i.icon {
+  display: table-cell;
+  min-width: 1.55em;
+  padding-top: 0;
+  transition: color 0.1s ease;
+  padding-right: 0.28571429em;
+  vertical-align: top;
+}
+.ui.list .list > .item > i.icon:only-child,
+.ui.list > .item > i.icon:only-child {
+  display: inline-block;
+  min-width: auto;
+  vertical-align: top;
+}
+
+.ui.list .list > .item > .image,
+.ui.list > .item > .image {
+  display: table-cell;
+  background-color: transparent;
+  vertical-align: top;
+}
+.ui.list .list > .item > .image:not(:only-child):not(img),
+.ui.list > .item > .image:not(:only-child):not(img) {
+  padding-right: 0.5em;
+}
+.ui.list .list > .item > .image img,
+.ui.list > .item > .image img {
+  vertical-align: top;
+}
+.ui.list .list > .item > img.image,
+.ui.list .list > .item > .image:only-child,
+.ui.list > .item > img.image,
+.ui.list > .item > .image:only-child {
+  display: inline-block;
+}
+
+.ui.list .list > .item > .content,
+.ui.list > .item > .content {
+  color: var(--color-text);
+}
+.ui.list .list > .item > .image + .content,
+.ui.list .list > .item > i.icon + .content,
+.ui.list > .item > .image + .content,
+.ui.list > .item > i.icon + .content {
+  display: table-cell;
+  width: 100%;
+  padding: 0 0 0 0.5em;
+  vertical-align: top;
+}
+.ui.list .list > .item > img.image + .content,
+.ui.list > .item > img.image + .content {
+  display: inline-block;
+  width: auto;
+}
+.ui.list .list > .item > .content > .list,
+.ui.list > .item > .content > .list {
+  margin-left: 0;
+  padding-left: 0;
+}
+
+.ui.list .list > .item .header,
+.ui.list > .item .header {
+  display: block;
+  margin: 0;
+  font-family: var(--fonts-regular);
+  font-weight: var(--font-weight-medium);
+  color: var(--color-text-dark);
+}
+
+.ui.list .list > .item .description,
+.ui.list > .item .description {
+  display: block;
+  color: var(--color-text);
+}
+
+.ui.list > .item a,
+.ui.list .list > .item a {
+  cursor: pointer;
+}
+
+.ui.menu .ui.list > .item,
+.ui.menu .ui.list .list > .item {
+  display: list-item;
+  table-layout: fixed;
+  background-color: transparent;
+  list-style-type: none;
+  list-style-position: outside;
+  padding: 0.21428571em 0;
+}
+.ui.menu .ui.list .list > .item::before,
+.ui.menu .ui.list > .item::before {
+  border: none;
+  background: none;
+}
+.ui.menu .ui.list .list > .item:first-child,
+.ui.menu .ui.list > .item:first-child {
+  padding-top: 0;
+}
+.ui.menu .ui.list .list > .item:last-child,
+.ui.menu .ui.list > .item:last-child {
+  padding-bottom: 0;
+}
+
+.ui.list .list > .disabled.item,
+.ui.list > .disabled.item {
+  pointer-events: none;
+  opacity: var(--opacity-disabled);
+}
+
+.ui.list .list > a.item:hover > .icons,
+.ui.list > a.item:hover > .icons,
+.ui.list .list > a.item:hover > i.icon,
+.ui.list > a.item:hover > i.icon {
+  color: var(--color-text-dark);
+}
+
+.ui.divided.list > .item {
+  border-top: 1px solid var(--color-secondary);
+}
+.ui.divided.list .list > .item {
+  border-top: none;
+}
+.ui.divided.list .item .list > .item {
+  border-top: none;
+}
+.ui.divided.list .list > .item:first-child,
+.ui.divided.list > .item:first-child {
+  border-top: none;
+}
+.ui.divided.list .list > .item:first-child {
+  border-top-width: 1px;
+}
+
+.ui.relaxed.list > .item:not(:first-child) {
+  padding-top: 0.42857143em;
+}
+.ui.relaxed.list > .item:not(:last-child) {
+  padding-bottom: 0.42857143em;
+}
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index 5cb6a371e5..49c00c4dad 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -6477,983 +6477,6 @@ select.ui.dropdown {
 /*******************************
          Site Overrides
 *******************************/
-/*!
- * # Fomantic-UI - List
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            List
-*******************************/
-
-ul.ui.list,
-ol.ui.list,
-.ui.list {
-  list-style-type: none;
-  margin: 1em 0;
-  padding: 0 0;
-}
-
-ul.ui.list:first-child,
-ol.ui.list:first-child,
-.ui.list:first-child {
-  margin-top: 0;
-  padding-top: 0;
-}
-
-ul.ui.list:last-child,
-ol.ui.list:last-child,
-.ui.list:last-child {
-  margin-bottom: 0;
-  padding-bottom: 0;
-}
-
-/*******************************
-            Content
-*******************************/
-
-/* List Item */
-
-ul.ui.list li,
-ol.ui.list li,
-.ui.list > .item,
-.ui.list .list > .item {
-  display: list-item;
-  table-layout: fixed;
-  list-style-type: none;
-  list-style-position: outside;
-  padding: 0.21428571em 0;
-  line-height: 1.14285714em;
-}
-
-ul.ui.list > li:first-child:after,
-ol.ui.list > li:first-child:after,
-.ui.list > .list > .item:after,
-.ui.list > .item:after {
-  content: '';
-  display: block;
-  height: 0;
-  clear: both;
-  visibility: hidden;
-}
-
-ul.ui.list li:first-child,
-ol.ui.list li:first-child,
-.ui.list .list > .item:first-child,
-.ui.list > .item:first-child {
-  padding-top: 0;
-}
-
-ul.ui.list li:last-child,
-ol.ui.list li:last-child,
-.ui.list .list > .item:last-child,
-.ui.list > .item:last-child {
-  padding-bottom: 0;
-}
-
-/* Child List */
-
-ul.ui.list ul,
-ol.ui.list ol,
-.ui.list .list:not(.icon) {
-  clear: both;
-  margin: 0;
-  padding: 0.75em 0 0.25em 0.5em;
-}
-
-/* Child Item */
-
-ul.ui.list ul li,
-ol.ui.list ol li,
-.ui.list .list > .item {
-  padding: 0.14285714em 0;
-  line-height: inherit;
-}
-
-/* Icon */
-
-.ui.list .list > .item > i.icon,
-.ui.list > .item > i.icon {
-  display: table-cell;
-  min-width: 1.55em;
-  margin: 0;
-  padding-top: 0;
-  transition: color 0.1s ease;
-}
-
-.ui.list .list > .item > i.icon:not(.loading),
-.ui.list > .item > i.icon:not(.loading) {
-  padding-right: 0.28571429em;
-  vertical-align: top;
-}
-
-.ui.list .list > .item > i.icon:only-child,
-.ui.list > .item > i.icon:only-child {
-  display: inline-block;
-  min-width: auto;
-  vertical-align: top;
-}
-
-/* Image */
-
-.ui.list .list > .item > .image,
-.ui.list > .item > .image {
-  display: table-cell;
-  background-color: transparent;
-  margin: 0;
-  vertical-align: top;
-}
-
-.ui.list .list > .item > .image:not(:only-child):not(img),
-.ui.list > .item > .image:not(:only-child):not(img) {
-  padding-right: 0.5em;
-}
-
-.ui.list .list > .item > .image img,
-.ui.list > .item > .image img {
-  vertical-align: top;
-}
-
-.ui.list .list > .item > img.image,
-.ui.list .list > .item > .image:only-child,
-.ui.list > .item > img.image,
-.ui.list > .item > .image:only-child {
-  display: inline-block;
-}
-
-/* Content */
-
-.ui.list .list > .item > .content,
-.ui.list > .item > .content {
-  line-height: 1.14285714em;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.list .list > .item > .image + .content,
-.ui.list .list > .item > i.icon + .content,
-.ui.list > .item > .image + .content,
-.ui.list > .item > i.icon + .content {
-  display: table-cell;
-  width: 100%;
-  padding: 0 0 0 0.5em;
-  vertical-align: top;
-}
-
-.ui.list .list > .item > i.loading.icon + .content,
-.ui.list > .item > i.loading.icon + .content {
-  padding-left: calc(0.2857142857142857em + 0.5em);
-}
-
-.ui.list .list > .item > img.image + .content,
-.ui.list > .item > img.image + .content {
-  display: inline-block;
-  width: auto;
-}
-
-.ui.list .list > .item > .content > .list,
-.ui.list > .item > .content > .list {
-  margin-left: 0;
-  padding-left: 0;
-}
-
-/* Header */
-
-.ui.list .list > .item .header,
-.ui.list > .item .header {
-  display: block;
-  margin: 0;
-  font-family: var(--fonts-regular);
-  font-weight: 500;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/* Description */
-
-.ui.list .list > .item .description,
-.ui.list > .item .description {
-  display: block;
-  color: rgba(0, 0, 0, 0.7);
-}
-
-/* Child Link */
-
-.ui.list > .item a,
-.ui.list .list > .item a {
-  cursor: pointer;
-}
-
-/* Linking Item */
-
-.ui.list .list > a.item,
-.ui.list > a.item {
-  cursor: pointer;
-  color: #4183C4;
-}
-
-.ui.list .list > a.item:hover,
-.ui.list > a.item:hover {
-  color: #1e70bf;
-}
-
-/* Linked Item Icons */
-
-.ui.list .list > a.item > i.icons,
-.ui.list > a.item > i.icons,
-.ui.list .list > a.item > i.icon,
-.ui.list > a.item > i.icon {
-  color: rgba(0, 0, 0, 0.4);
-}
-
-/* Header Link */
-
-.ui.list .list > .item a.header,
-.ui.list > .item a.header {
-  cursor: pointer;
-  color: #4183C4 !important;
-}
-
-.ui.list .list > .item > a.header:hover,
-.ui.list > .item > a.header:hover {
-  color: #1e70bf !important;
-}
-
-/* Floated Content */
-
-.ui[class*="left floated"].list {
-  float: left;
-}
-
-.ui[class*="right floated"].list {
-  float: right;
-}
-
-.ui.list .list > .item [class*="left floated"],
-.ui.list > .item [class*="left floated"] {
-  float: left;
-  margin: 0 1em 0 0;
-}
-
-.ui.list .list > .item [class*="right floated"],
-.ui.list > .item [class*="right floated"] {
-  float: right;
-  margin: 0 0 0 1em;
-}
-
-/*******************************
-            Coupling
-*******************************/
-
-.ui.menu .ui.list > .item,
-.ui.menu .ui.list .list > .item {
-  display: list-item;
-  table-layout: fixed;
-  background-color: transparent;
-  list-style-type: none;
-  list-style-position: outside;
-  padding: 0.21428571em 0;
-  line-height: 1.14285714em;
-}
-
-.ui.menu .ui.list .list > .item:before,
-.ui.menu .ui.list > .item:before {
-  border: none;
-  background: none;
-}
-
-.ui.menu .ui.list .list > .item:first-child,
-.ui.menu .ui.list > .item:first-child {
-  padding-top: 0;
-}
-
-.ui.menu .ui.list .list > .item:last-child,
-.ui.menu .ui.list > .item:last-child {
-  padding-bottom: 0;
-}
-
-/*******************************
-            Types
-*******************************/
-
-/*-------------------
-        Horizontal
-  --------------------*/
-
-.ui.horizontal.list {
-  display: inline-block;
-  font-size: 0;
-}
-
-.ui.horizontal.list > .item {
-  display: inline-block;
-  margin-right: 1em;
-  font-size: 1rem;
-}
-
-.ui.horizontal.list:not(.celled) > .item:last-child {
-  margin-right: 0;
-  padding-right: 0;
-}
-
-.ui.horizontal.list .list:not(.icon) {
-  padding-left: 0;
-  padding-bottom: 0;
-}
-
-.ui.horizontal.list > .item > .image,
-.ui.horizontal.list .list > .item > .image,
-.ui.horizontal.list > .item > i.icon,
-.ui.horizontal.list .list > .item > i.icon,
-.ui.horizontal.list > .item > .content,
-.ui.horizontal.list .list > .item > .content {
-  vertical-align: middle;
-}
-
-/* Padding on all elements */
-
-.ui.horizontal.list > .item:first-child,
-.ui.horizontal.list > .item:last-child {
-  padding-top: 0.21428571em;
-  padding-bottom: 0.21428571em;
-}
-
-/* Horizontal List */
-
-.ui.horizontal.list > .item > i.icon,
-.ui.horizontal.list .item > i.icons > i.icon {
-  margin: 0;
-  padding: 0 0.25em 0 0;
-}
-
-.ui.horizontal.list > .item > .image + .content,
-.ui.horizontal.list > .item > i.icon,
-.ui.horizontal.list > .item > i.icon + .content {
-  float: none;
-  display: inline-block;
-  width: auto;
-}
-
-.ui.horizontal.list > .item > .image {
-  display: inline-block;
-}
-
-/*******************************
-             States
-*******************************/
-
-/*-------------------
-         Disabled
-  --------------------*/
-
-.ui.list .list > .disabled.item,
-.ui.list > .disabled.item {
-  pointer-events: none;
-  color: rgba(40, 40, 40, 0.3) !important;
-}
-
-/*-------------------
-        Hover
---------------------*/
-
-.ui.list .list > a.item:hover > .icons,
-.ui.list > a.item:hover > .icons,
-.ui.list .list > a.item:hover > i.icon,
-.ui.list > a.item:hover > i.icon {
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-         Aligned
-  --------------------*/
-
-.ui.list[class*="top aligned"] .image,
-.ui.list[class*="top aligned"] .content,
-.ui.list [class*="top aligned"] {
-  vertical-align: top !important;
-}
-
-.ui.list[class*="middle aligned"] .image,
-.ui.list[class*="middle aligned"] .content,
-.ui.list [class*="middle aligned"] {
-  vertical-align: middle !important;
-}
-
-.ui.list[class*="bottom aligned"] .image,
-.ui.list[class*="bottom aligned"] .content,
-.ui.list [class*="bottom aligned"] {
-  vertical-align: bottom !important;
-}
-
-/*-------------------
-         Link
-  --------------------*/
-
-.ui.link.list .item,
-.ui.link.list a.item,
-.ui.link.list .item a:not(.ui) {
-  color: rgba(0, 0, 0, 0.4);
-  transition: 0.1s color ease;
-}
-
-.ui.link.list.list a.item:hover,
-.ui.link.list.list .item a:not(.ui):hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.link.list.list a.item:active,
-.ui.link.list.list .item a:not(.ui):active {
-  color: rgba(0, 0, 0, 0.9);
-}
-
-.ui.link.list.list .active.item,
-.ui.link.list.list .active.item a:not(.ui) {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*-------------------
-        Selection
-  --------------------*/
-
-.ui.selection.list .list > .item,
-.ui.selection.list > .item {
-  cursor: pointer;
-  background: transparent;
-  padding: 0.5em 0.5em;
-  margin: 0;
-  color: rgba(0, 0, 0, 0.4);
-  border-radius: 0.5em;
-  transition: 0.1s color ease, 0.1s padding-left ease, 0.1s background-color ease;
-}
-
-.ui.selection.list .list > .item:last-child,
-.ui.selection.list > .item:last-child {
-  margin-bottom: 0;
-}
-
-.ui.selection.list .list > .item:hover,
-.ui.selection.list > .item:hover {
-  background: rgba(0, 0, 0, 0.03);
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.selection.list .list > .item:active,
-.ui.selection.list > .item:active {
-  background: rgba(0, 0, 0, 0.05);
-  color: rgba(0, 0, 0, 0.9);
-}
-
-.ui.selection.list .list > .item.active,
-.ui.selection.list > .item.active {
-  background: rgba(0, 0, 0, 0.05);
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/* Celled / Divided Selection List */
-
-.ui.celled.selection.list .list > .item,
-.ui.divided.selection.list .list > .item,
-.ui.celled.selection.list > .item,
-.ui.divided.selection.list > .item {
-  border-radius: 0;
-}
-
-/*-------------------
-         Animated
-  --------------------*/
-
-.ui.animated.list > .item {
-  transition: 0.25s color ease 0.1s, 0.25s padding-left ease 0.1s, 0.25s background-color ease 0.1s;
-}
-
-.ui.animated.list:not(.horizontal) > .item:hover {
-  padding-left: 1em;
-}
-
-/*-------------------
-         Fitted
-  --------------------*/
-
-.ui.fitted.list:not(.selection) .list > .item,
-.ui.fitted.list:not(.selection) > .item {
-  padding-left: 0;
-  padding-right: 0;
-}
-
-.ui.fitted.selection.list .list > .item,
-.ui.fitted.selection.list > .item {
-  margin-left: -0.5em;
-  margin-right: -0.5em;
-}
-
-/*-------------------
-        Bulleted
-  --------------------*/
-
-ul.ui.list,
-.ui.bulleted.list {
-  margin-left: 1.25rem;
-}
-
-ul.ui.list li,
-.ui.bulleted.list .list > .item,
-.ui.bulleted.list > .item {
-  position: relative;
-}
-
-ul.ui.list li:before,
-.ui.bulleted.list .list > .item:before,
-.ui.bulleted.list > .item:before {
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  pointer-events: none;
-  position: absolute;
-  top: auto;
-  left: auto;
-  font-weight: normal;
-  margin-left: -1.25rem;
-  content: '\2022';
-  opacity: 1;
-  color: inherit;
-  vertical-align: top;
-}
-
-ul.ui.list li:before,
-.ui.bulleted.list .list > a.item:before,
-.ui.bulleted.list > a.item:before {
-  color: rgba(0, 0, 0, 0.87);
-}
-
-ul.ui.list ul,
-.ui.bulleted.list .list:not(.icon) {
-  padding-left: 1.25rem;
-}
-
-/* Horizontal Bulleted */
-
-ul.ui.horizontal.bulleted.list,
-.ui.horizontal.bulleted.list {
-  margin-left: 0;
-}
-
-ul.ui.horizontal.bulleted.list li,
-.ui.horizontal.bulleted.list > .item {
-  margin-left: 1.75rem;
-}
-
-ul.ui.horizontal.bulleted.list li:first-child,
-.ui.horizontal.bulleted.list > .item:first-child {
-  margin-left: 0;
-}
-
-ul.ui.horizontal.bulleted.list li::before,
-.ui.horizontal.bulleted.list > .item::before {
-  color: rgba(0, 0, 0, 0.87);
-}
-
-ul.ui.horizontal.bulleted.list li:first-child::before,
-.ui.horizontal.bulleted.list > .item:first-child::before {
-  display: none;
-}
-
-/*-------------------
-         Ordered
-  --------------------*/
-
-ol.ui.list,
-.ui.ordered.list,
-.ui.ordered.list .list:not(.icon),
-ol.ui.list ol {
-  counter-reset: ordered;
-  margin-left: 1.25rem;
-  list-style-type: none;
-}
-
-ol.ui.list li,
-.ui.ordered.list .list > .item,
-.ui.ordered.list > .item {
-  list-style-type: none;
-  position: relative;
-}
-
-ol.ui.list li:before,
-.ui.ordered.list .list > .item:before,
-.ui.ordered.list > .item:before {
-  position: absolute;
-  top: auto;
-  left: auto;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  pointer-events: none;
-  margin-left: -1.25rem;
-  counter-increment: ordered;
-  content: counters(ordered, ".") " ";
-  text-align: right;
-  color: rgba(0, 0, 0, 0.87);
-  vertical-align: middle;
-  opacity: 0.8;
-}
-
-/* Value */
-
-.ui.ordered.list .list > .item[data-value]:before,
-.ui.ordered.list > .item[data-value]:before {
-  content: attr(data-value);
-}
-
-ol.ui.list li[value]:before {
-  content: attr(value);
-}
-
-/* Child Lists */
-
-ol.ui.list ol,
-.ui.ordered.list .list:not(.icon) {
-  margin-left: 1em;
-}
-
-ol.ui.list ol li:before,
-.ui.ordered.list .list > .item:before {
-  margin-left: -2em;
-}
-
-/* Horizontal Ordered */
-
-ol.ui.horizontal.list,
-.ui.ordered.horizontal.list {
-  margin-left: 0;
-}
-
-ol.ui.horizontal.list li:before,
-.ui.ordered.horizontal.list .list > .item:before,
-.ui.ordered.horizontal.list > .item:before {
-  position: static;
-  margin: 0 0.5em 0 0;
-}
-
-/* Suffixed Ordered */
-
-ol.ui.suffixed.list li:before,
-.ui.suffixed.ordered.list .list > .item:before,
-.ui.suffixed.ordered.list > .item:before {
-  content: counters(ordered, ".") ".";
-}
-
-/*-------------------
-         Divided
-  --------------------*/
-
-.ui.divided.list > .item {
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.divided.list .list > .item {
-  border-top: none;
-}
-
-.ui.divided.list .item .list > .item {
-  border-top: none;
-}
-
-.ui.divided.list .list > .item:first-child,
-.ui.divided.list > .item:first-child {
-  border-top: none;
-}
-
-/* Sub Menu */
-
-.ui.divided.list:not(.horizontal) .list > .item:first-child {
-  border-top-width: 1px;
-}
-
-/* Divided bulleted */
-
-.ui.divided.bulleted.list:not(.horizontal),
-.ui.divided.bulleted.list .list:not(.icon) {
-  margin-left: 0;
-  padding-left: 0;
-}
-
-.ui.divided.bulleted.list > .item:not(.horizontal) {
-  padding-left: 1.25rem;
-}
-
-/* Divided Ordered */
-
-.ui.divided.ordered.list {
-  margin-left: 0;
-}
-
-.ui.divided.ordered.list .list > .item,
-.ui.divided.ordered.list > .item {
-  padding-left: 1.25rem;
-}
-
-.ui.divided.ordered.list .item .list:not(.icon) {
-  margin-left: 0;
-  margin-right: 0;
-  padding-bottom: 0.21428571em;
-}
-
-.ui.divided.ordered.list .item .list > .item {
-  padding-left: 1em;
-}
-
-/* Divided Selection */
-
-.ui.divided.selection.list .list > .item,
-.ui.divided.selection.list > .item {
-  margin: 0;
-  border-radius: 0;
-}
-
-/* Divided horizontal */
-
-.ui.divided.horizontal.list {
-  margin-left: 0;
-}
-
-.ui.divided.horizontal.list > .item {
-  padding-left: 0.5em;
-}
-
-.ui.divided.horizontal.list > .item:not(:last-child) {
-  padding-right: 0.5em;
-}
-
-.ui.divided.horizontal.list > .item {
-  border-top: none;
-  border-right: 1px solid rgba(34, 36, 38, 0.15);
-  margin: 0;
-  line-height: 0.6;
-}
-
-.ui.horizontal.divided.list > .item:last-child {
-  border-right: none;
-}
-
-/*-------------------
-          Celled
-  --------------------*/
-
-.ui.celled.list > .item,
-.ui.celled.list > .list {
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-}
-
-.ui.celled.list > .item:last-child {
-  border-bottom: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/* Padding on all elements */
-
-.ui.celled.list > .item:first-child,
-.ui.celled.list > .item:last-child {
-  padding-top: 0.21428571em;
-  padding-bottom: 0.21428571em;
-}
-
-/* Sub Menu */
-
-.ui.celled.list .item .list > .item {
-  border-width: 0;
-}
-
-.ui.celled.list .list > .item:first-child {
-  border-top-width: 0;
-}
-
-/* Celled Bulleted */
-
-.ui.celled.bulleted.list {
-  margin-left: 0;
-}
-
-.ui.celled.bulleted.list .list > .item,
-.ui.celled.bulleted.list > .item {
-  padding-left: 1.25rem;
-}
-
-.ui.celled.bulleted.list .item .list:not(.icon) {
-  margin-left: -1.25rem;
-  margin-right: -1.25rem;
-  padding-bottom: 0.21428571em;
-}
-
-/* Celled Ordered */
-
-.ui.celled.ordered.list {
-  margin-left: 0;
-}
-
-.ui.celled.ordered.list .list > .item,
-.ui.celled.ordered.list > .item {
-  padding-left: 1.25rem;
-}
-
-.ui.celled.ordered.list .item .list:not(.icon) {
-  margin-left: 0;
-  margin-right: 0;
-  padding-bottom: 0.21428571em;
-}
-
-.ui.celled.ordered.list .list > .item {
-  padding-left: 1em;
-}
-
-/* Celled Horizontal */
-
-.ui.horizontal.celled.list {
-  margin-left: 0;
-}
-
-.ui.horizontal.celled.list .list > .item,
-.ui.horizontal.celled.list > .item {
-  border-top: none;
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-  margin: 0;
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-  line-height: 0.6;
-}
-
-.ui.horizontal.celled.list .list > .item:last-child,
-.ui.horizontal.celled.list > .item:last-child {
-  border-bottom: none;
-  border-right: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/*-------------------
-         Relaxed
-  --------------------*/
-
-.ui.relaxed.list:not(.horizontal) > .item:not(:first-child) {
-  padding-top: 0.42857143em;
-}
-
-.ui.relaxed.list:not(.horizontal) > .item:not(:last-child) {
-  padding-bottom: 0.42857143em;
-}
-
-.ui.horizontal.relaxed.list .list > .item:not(:first-child),
-.ui.horizontal.relaxed.list > .item:not(:first-child) {
-  padding-left: 1rem;
-}
-
-.ui.horizontal.relaxed.list .list > .item:not(:last-child),
-.ui.horizontal.relaxed.list > .item:not(:last-child) {
-  padding-right: 1rem;
-}
-
-/* Very Relaxed */
-
-.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:first-child) {
-  padding-top: 0.85714286em;
-}
-
-.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:last-child) {
-  padding-bottom: 0.85714286em;
-}
-
-.ui.horizontal[class*="very relaxed"].list .list > .item:not(:first-child),
-.ui.horizontal[class*="very relaxed"].list > .item:not(:first-child) {
-  padding-left: 1.5rem;
-}
-
-.ui.horizontal[class*="very relaxed"].list .list > .item:not(:last-child),
-.ui.horizontal[class*="very relaxed"].list > .item:not(:last-child) {
-  padding-right: 1.5rem;
-}
-
-/*-------------------
-      Sizes
---------------------*/
-
-.ui.list {
-  font-size: 1em;
-}
-
-.ui.mini.list {
-  font-size: 0.78571429em;
-}
-
-.ui.mini.horizontal.list .list > .item,
-.ui.mini.horizontal.list > .item {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.list {
-  font-size: 0.85714286em;
-}
-
-.ui.tiny.horizontal.list .list > .item,
-.ui.tiny.horizontal.list > .item {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.list {
-  font-size: 0.92857143em;
-}
-
-.ui.small.horizontal.list .list > .item,
-.ui.small.horizontal.list > .item {
-  font-size: 0.92857143rem;
-}
-
-.ui.large.list {
-  font-size: 1.14285714em;
-}
-
-.ui.large.horizontal.list .list > .item,
-.ui.large.horizontal.list > .item {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.list {
-  font-size: 1.28571429em;
-}
-
-.ui.big.horizontal.list .list > .item,
-.ui.big.horizontal.list > .item {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.list {
-  font-size: 1.42857143em;
-}
-
-.ui.huge.horizontal.list .list > .item,
-.ui.huge.horizontal.list > .item {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.list {
-  font-size: 1.71428571em;
-}
-
-.ui.massive.horizontal.list .list > .item,
-.ui.massive.horizontal.list > .item {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-    User Variable Overrides
-*******************************/
 /*
  * # Fomantic - Menu
  * http://github.com/fomantic/Fomantic-UI/
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 7ec520f315..5db57bc8d4 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -26,7 +26,6 @@
     "dimmer",
     "dropdown",
     "form",
-    "list",
     "menu",
     "modal",
     "search",

From 48223909be0511bcd773bceea76918bfd7cc7d46 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Sun, 7 Apr 2024 00:27:31 +0000
Subject: [PATCH 634/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_ja-JP.ini | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 7e725b4647..57b2aff254 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -652,7 +652,7 @@ block.unblock.failure=ユーザーのブロック解除に失敗しました: %s
 block.blocked=あなたはこのユーザーをブロックしています。
 block.title=ユーザーをブロックする
 block.info=ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。 ユーザーのブロックについてはよく確認してください。
-block.info_1=ユーザーをブロックすることで、あなたのアカウントとリポジトリに対する以下の行為を防ぎます:
+block.info_1=ユーザーをブロックすることで、あなたのアカウントとあなたのリポジトリに対する以下の行為を阻止します:
 block.info_2=あなたのアカウントのフォロー
 block.info_3=あなたのユーザー名で@メンションして通知を送ること
 block.info_4=そのユーザーのリポジトリに、あなたを共同作業者として招待すること
@@ -709,8 +709,8 @@ language=言語
 ui=テーマ
 hidden_comment_types=非表示にするコメントの種類
 hidden_comment_types_description=ここでチェックを入れたコメントの種類は、イシューのページには表示されません。 たとえば「ラベル」にチェックを入れると、「<ユーザー> が <ラベル> を追加/削除」といったコメントはすべて除去されます。
-hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照されたというコメント
-hidden_comment_types.issue_ref_tooltip=このイシューに関連付けるブランチやタグをユーザーが変更したというコメント
+hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照された、というコメント
+hidden_comment_types.issue_ref_tooltip=このイシューのブランチやタグへの関連付けをユーザーが変更した、というコメント
 comment_type_group_reference=参照
 comment_type_group_label=ラベル
 comment_type_group_milestone=マイルストーン
@@ -780,7 +780,7 @@ add_email_success=新しいメールアドレスを追加しました。
 email_preference_set_success=メール設定を保存しました。
 add_openid_success=新しいOpenIDアドレスを追加しました。
 keep_email_private=メールアドレスを隠す
-keep_email_private_popup=これによりプロフィールでメールアドレスが隠され、Webインターフェースでのプルリクエスト作成やファイル編集でもメールアドレスが隠されます。 プッシュ済みのコミットは変更されません。
+keep_email_private_popup=あなたのプロフィールからメールアドレスが隠され、Webインターフェースを使ったプルリクエスト作成やファイル編集でも、メールアドレスが隠されます。 プッシュ済みのコミットは変更されません。 コミットであなたのアカウントに関連付ける場合は %s を使用してください。
 openid_desc=OpenIDを使うと外部プロバイダーに認証を委任することができます。
 
 manage_ssh_keys=SSHキーの管理
@@ -2961,12 +2961,12 @@ packages.size=サイズ
 packages.published=配布
 
 defaulthooks=デフォルトWebhook
-defaulthooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義されたWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
+defaulthooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
 defaulthooks.add_webhook=デフォルトWebhookの追加
 defaulthooks.update_webhook=デフォルトWebhookの更新
 
 systemhooks=システムWebhook
-systemhooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義したWebhookはシステム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
+systemhooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookは、システム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
 systemhooks.add_webhook=システムWebhookを追加
 systemhooks.update_webhook=システムWebhookを更新
 
@@ -3342,9 +3342,9 @@ raw_seconds=秒
 raw_minutes=分
 
 [dropzone]
-default_message=ここにファイルをドロップまたはクリックしてアップロードします。
+default_message=ファイルをここにドロップ、またはここをクリックしてアップロード
 invalid_input_type=この種類のファイルはアップロードできません。
-file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) が最大サイズ ({{maxFilesize}} MB) を超えています。
+file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) は、最大サイズ ({{maxFilesize}} MB) を超えています。
 remove_file=ファイル削除
 
 [notification]
@@ -3369,7 +3369,7 @@ error.no_committer_account=コミッターのメールアドレスに対応す
 error.no_gpg_keys_found=この署名に対応する既知のキーがデータベースに存在しません
 error.not_signed_commit=署名されたコミットではありません
 error.failed_retrieval_gpg_keys=コミッターのアカウントに登録されたキーを取得できませんでした
-error.probable_bad_signature=警告! このIDの鍵はデータベースに登録されていますが、その鍵でコミットの検証が通りません! これは疑わしいコミットです。
+error.probable_bad_signature=警告! このIDに該当する鍵がデータベースにありますが、コミットの検証が通りません! これは疑わしいコミットです。
 error.probable_bad_default_signature=警告! これはデフォルト鍵のIDですが、デフォルト鍵ではコミットの検証が通りません! これは疑わしいコミットです。
 
 [units]
@@ -3382,7 +3382,7 @@ title=パッケージ
 desc=リポジトリ パッケージを管理します。
 empty=パッケージはまだありません。
 empty.documentation=パッケージレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
-empty.repo=パッケージはアップロードしたけども、ここに表示されない? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。
+empty.repo=パッケージはアップロード済みで、ここに表示されていないですか? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。
 registry.documentation=%sレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
 filter.type=タイプ
 filter.type.all=すべて

From bbe5cd7c92ccc3793473ae0163398cdbccdd4246 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 7 Apr 2024 09:11:25 +0800
Subject: [PATCH 635/679] Refactor startup deprecation messages (#30305)

It doesn't change logic, it only does:

1. Rename the variable and function names
2. Use more consistent format when mentioning config section&key
3. Improve some messages
---
 modules/setting/config_provider.go | 16 ++++++++++------
 modules/setting/indexer.go         |  2 +-
 modules/setting/oauth2.go          |  2 +-
 modules/setting/repository.go      |  2 +-
 modules/setting/server.go          |  2 +-
 modules/setting/session.go         |  2 +-
 modules/setting/setting.go         |  4 +---
 modules/setting/storage.go         |  2 +-
 routers/web/admin/admin.go         | 18 +++++++++---------
 routers/web/admin/config.go        |  2 +-
 templates/admin/self_check.tmpl    |  6 +++---
 11 files changed, 30 insertions(+), 28 deletions(-)

diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go
index 3fa3f3b50b..03f27ba203 100644
--- a/modules/setting/config_provider.go
+++ b/modules/setting/config_provider.go
@@ -315,21 +315,25 @@ func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
 	}
 }
 
-// DeprecatedWarnings contains the warning message for various deprecations, including: setting option, file/folder, etc
-var DeprecatedWarnings []string
+// StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc
+var StartupProblems []string
+
+func logStartupProblem(skip int, level log.Level, format string, args ...any) {
+	msg := fmt.Sprintf(format, args...)
+	log.Log(skip+1, level, "%s", msg)
+	StartupProblems = append(StartupProblems, msg)
+}
 
 func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
 	if rootCfg.Section(oldSection).HasKey(oldKey) {
-		msg := fmt.Sprintf("Deprecated config option `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
-		log.Error("%v", msg)
-		DeprecatedWarnings = append(DeprecatedWarnings, msg)
+		logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
 	}
 }
 
 // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
 func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
 	if rootCfg.Section(oldSection).HasKey(oldKey) {
-		log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
+		logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
 	}
 }
 
diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index cec364d370..6877d70e3c 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -58,7 +58,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
 		if !filepath.IsAbs(Indexer.IssuePath) {
 			Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
 		}
-		checkOverlappedPath("indexer.ISSUE_INDEXER_PATH", Indexer.IssuePath)
+		checkOverlappedPath("[indexer].ISSUE_INDEXER_PATH", Indexer.IssuePath)
 	} else {
 		Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
 		if Indexer.IssueType == "meilisearch" {
diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index 4d3bfd3eb6..1429a7585c 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -168,7 +168,7 @@ func GetGeneralTokenSigningSecret() []byte {
 		}
 		if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
 			// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
-			log.Warn("OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
+			logStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
 			return jwtSecret
 		}
 		return *generalSigningSecret.Load()
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index a332d6adb3..8656ebc7ec 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -286,7 +286,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
 		RepoRootPath = filepath.Clean(RepoRootPath)
 	}
 
-	checkOverlappedPath("repository.ROOT", RepoRootPath)
+	checkOverlappedPath("[repository].ROOT", RepoRootPath)
 
 	defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
 	for _, charset := range Repository.DetectedCharsetsOrder {
diff --git a/modules/setting/server.go b/modules/setting/server.go
index 315faaeb21..7d6ece2727 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -331,7 +331,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
 	if !filepath.IsAbs(PprofDataPath) {
 		PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
 	}
-	checkOverlappedPath("server.PPROF_DATA_PATH", PprofDataPath)
+	checkOverlappedPath("[server].PPROF_DATA_PATH", PprofDataPath)
 
 	landingPage := sec.Key("LANDING_PAGE").MustString("home")
 	switch landingPage {
diff --git a/modules/setting/session.go b/modules/setting/session.go
index 3cb1bfe7b5..afe63bfdb7 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -46,7 +46,7 @@ func loadSessionFrom(rootCfg ConfigProvider) {
 	SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
 	if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
 		SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
-		checkOverlappedPath("session.PROVIDER_CONFIG", SessionConfig.ProviderConfig)
+		checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
 	}
 	SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
 	SessionConfig.CookiePath = AppSubURL
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 6aca9ec6cf..92bb0b6541 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -235,9 +235,7 @@ var configuredPaths = make(map[string]string)
 func checkOverlappedPath(name, path string) {
 	// TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path)
 	if targetName, ok := configuredPaths[path]; ok && targetName != name {
-		msg := fmt.Sprintf("Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
-		log.Error("%s", msg)
-		DeprecatedWarnings = append(DeprecatedWarnings, msg)
+		logStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
 	}
 	configuredPaths[path] = name
 }
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index f4e33a53af..aeb61ac513 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -240,7 +240,7 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
 		}
 	}
 
-	checkOverlappedPath("storage."+name+".PATH", storage.Path)
+	checkOverlappedPath("[storage."+name+"].PATH", storage.Path)
 
 	return &storage, nil
 }
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index 4dc0dfdef8..e6585d8833 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -117,11 +117,11 @@ func updateSystemStatus() {
 	sysStatus.NumGC = m.NumGC
 }
 
-func prepareDeprecatedWarningsAlert(ctx *context.Context) {
-	if len(setting.DeprecatedWarnings) > 0 {
-		content := setting.DeprecatedWarnings[0]
-		if len(setting.DeprecatedWarnings) > 1 {
-			content += fmt.Sprintf(" (and %d more)", len(setting.DeprecatedWarnings)-1)
+func prepareStartupProblemsAlert(ctx *context.Context) {
+	if len(setting.StartupProblems) > 0 {
+		content := setting.StartupProblems[0]
+		if len(setting.StartupProblems) > 1 {
+			content += fmt.Sprintf(" (and %d more)", len(setting.StartupProblems)-1)
 		}
 		ctx.Flash.Error(content, true)
 	}
@@ -136,7 +136,7 @@ func Dashboard(ctx *context.Context) {
 	updateSystemStatus()
 	ctx.Data["SysStatus"] = sysStatus
 	ctx.Data["SSH"] = setting.SSH
-	prepareDeprecatedWarningsAlert(ctx)
+	prepareStartupProblemsAlert(ctx)
 	ctx.HTML(http.StatusOK, tplDashboard)
 }
 
@@ -191,10 +191,10 @@ func DashboardPost(ctx *context.Context) {
 func SelfCheck(ctx *context.Context) {
 	ctx.Data["PageIsAdminSelfCheck"] = true
 
-	ctx.Data["DeprecatedWarnings"] = setting.DeprecatedWarnings
-	if len(setting.DeprecatedWarnings) == 0 && !setting.IsProd {
+	ctx.Data["StartupProblems"] = setting.StartupProblems
+	if len(setting.StartupProblems) == 0 && !setting.IsProd {
 		if time.Now().Unix()%2 == 0 {
-			ctx.Data["DeprecatedWarnings"] = []string{"This is a test warning message in dev mode"}
+			ctx.Data["StartupProblems"] = []string{"This is a test warning message in dev mode"}
 		}
 	}
 
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index 2f5f17e201..48f80dbbf1 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -165,7 +165,7 @@ func Config(ctx *context.Context) {
 
 	ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
 	config.GetDynGetter().InvalidateCache()
-	prepareDeprecatedWarningsAlert(ctx)
+	prepareStartupProblemsAlert(ctx)
 
 	ctx.HTML(http.StatusOK, tplConfig)
 }
diff --git a/templates/admin/self_check.tmpl b/templates/admin/self_check.tmpl
index c100ffd504..a6c2ac1ac9 100644
--- a/templates/admin/self_check.tmpl
+++ b/templates/admin/self_check.tmpl
@@ -5,11 +5,11 @@
 		{{ctx.Locale.Tr "admin.self_check"}}
 	</h4>
 
-	{{if .DeprecatedWarnings}}
+	{{if .StartupProblems}}
 	<div class="ui attached segment">
 		<div class="ui warning message">
 			<div>{{ctx.Locale.Tr "admin.self_check.startup_warnings"}}</div>
-			<ul class="tw-w-full">{{range .DeprecatedWarnings}}<li>{{.}}</li>{{end}}</ul>
+			<ul class="tw-w-full">{{range .StartupProblems}}<li>{{.}}</li>{{end}}</ul>
 		</div>
 	</div>
 	{{end}}
@@ -40,7 +40,7 @@
 	</div>
 	{{end}}
 
-	{{if and (not .DeprecatedWarnings) (not .DatabaseCheckHasProblems)}}
+	{{if and (not .StartupProblems) (not .DatabaseCheckHasProblems)}}
 	<div class="ui attached segment">
 		{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}
 	</div>

From 94aad35a120b05897a0b6b97f0d9605a52ea9642 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 7 Apr 2024 10:53:28 +0200
Subject: [PATCH 636/679] Fix right-aligned input icons (#30301)

Fix regression from https://github.com/go-gitea/gitea/pull/30194 where
right-aligned items would not display correctly.

Before and After:

<img width="285" alt="Screenshot 2024-04-06 at 01 12 11"
src="https://github.com/go-gitea/gitea/assets/115237/f9168db5-0f69-4b5d-ba17-b60145ac4a09">
<img width="285" alt="Screenshot 2024-04-06 at 01 11 49"
src="https://github.com/go-gitea/gitea/assets/115237/639ab6ed-d018-4e3a-9980-1f079e4ebe9d">

Frontpage search tweaked to accommodate (which was the reason for the
changes that broken above):

<img width="445" alt="Screenshot 2024-04-06 at 01 11 34"
src="https://github.com/go-gitea/gitea/assets/115237/1919220b-390e-463a-8e3d-33a3556bf111">
<img width="438" alt="Screenshot 2024-04-06 at 01 11 39"
src="https://github.com/go-gitea/gitea/assets/115237/fd94f8e4-1d56-4b04-99e3-1cd240bd7ab4">
---
 web_src/css/modules/input.css | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/web_src/css/modules/input.css b/web_src/css/modules/input.css
index 48cd2fa9ff..18b785ac82 100644
--- a/web_src/css/modules/input.css
+++ b/web_src/css/modules/input.css
@@ -48,8 +48,11 @@
   cursor: default;
   position: absolute;
   text-align: center;
-  top: 50%;
-  transform: translateY(-50%);
+  top: 0;
+  right: 0;
+  margin: 0;
+  height: 100%;
+  width: 2.67142857em;
   opacity: 0.5;
   border-radius: 0 0.28571429rem 0.28571429rem 0;
   pointer-events: none;
@@ -58,6 +61,8 @@
 
 .ui.icon.input > i.icon.is-loading {
   position: absolute !important;
+  height: 28px;
+  top: 4px;
 }
 
 .ui.icon.input > i.icon.is-loading > * {
@@ -78,7 +83,7 @@
 
 .ui[class*="left icon"].input > i.icon {
   right: auto;
-  left: 8px;
+  left: 1px;
   border-radius: 0.28571429rem 0 0 0.28571429rem;
 }
 .ui[class*="left icon"].input > i.circular.icon {

From 83f83019ef3471b847a300f0821499b3896ec987 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 7 Apr 2024 19:17:06 +0800
Subject: [PATCH 637/679] Clean up log messages (#30313)

`log.Xxx("%v")` is not ideal, this PR adds necessary context messages.
Remove some unnecessary logs.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 cmd/web.go                               |  2 +-
 models/asymkey/ssh_key_fingerprint.go    | 17 ++++-------------
 models/repo/issue.go                     |  2 +-
 modules/util/util.go                     |  2 +-
 routers/api/v1/notify/repo.go            |  2 --
 routers/private/actions.go               | 16 ++++++++--------
 routers/private/hook_verification.go     |  3 +--
 routers/private/mail.go                  |  2 +-
 routers/web/admin/users.go               |  1 -
 routers/web/auth/password.go             |  2 --
 routers/web/user/setting/account.go      |  1 -
 services/context/captcha.go              |  4 ++--
 services/notify/notify.go                |  4 ++--
 services/repository/files/cherry_pick.go |  2 +-
 services/repository/files/patch.go       |  2 +-
 services/repository/files/update.go      |  2 +-
 services/wiki/wiki.go                    | 14 +++++++-------
 17 files changed, 31 insertions(+), 47 deletions(-)

diff --git a/cmd/web.go b/cmd/web.go
index 01386251be..ef8a7426c1 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -114,7 +114,7 @@ func showWebStartupMessage(msg string) {
 	log.Info("* WorkPath: %s", setting.AppWorkPath)
 	log.Info("* CustomPath: %s", setting.CustomPath)
 	log.Info("* ConfigFile: %s", setting.CustomConf)
-	log.Info("%s", msg)
+	log.Info("%s", msg) // show startup message
 }
 
 func serveInstall(ctx *cli.Context) error {
diff --git a/models/asymkey/ssh_key_fingerprint.go b/models/asymkey/ssh_key_fingerprint.go
index b9cfb1b251..1ed3b5df2a 100644
--- a/models/asymkey/ssh_key_fingerprint.go
+++ b/models/asymkey/ssh_key_fingerprint.go
@@ -76,23 +76,14 @@ func calcFingerprintNative(publicKeyContent string) (string, error) {
 // CalcFingerprint calculate public key's fingerprint
 func CalcFingerprint(publicKeyContent string) (string, error) {
 	// Call the method based on configuration
-	var (
-		fnName, fp string
-		err        error
-	)
-	if len(setting.SSH.KeygenPath) == 0 {
-		fnName = "calcFingerprintNative"
-		fp, err = calcFingerprintNative(publicKeyContent)
-	} else {
-		fnName = "calcFingerprintSSHKeygen"
-		fp, err = calcFingerprintSSHKeygen(publicKeyContent)
-	}
+	useNative := setting.SSH.KeygenPath == ""
+	calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen)
+	fp, err := calcFn(publicKeyContent)
 	if err != nil {
 		if IsErrKeyUnableVerify(err) {
-			log.Info("%s", publicKeyContent)
 			return "", err
 		}
-		return "", fmt.Errorf("%s: %w", fnName, err)
+		return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err)
 	}
 	return fp, nil
 }
diff --git a/models/repo/issue.go b/models/repo/issue.go
index 6f6b565a00..0dd4fd5ed4 100644
--- a/models/repo/issue.go
+++ b/models/repo/issue.go
@@ -53,7 +53,7 @@ func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool {
 	var u *RepoUnit
 	var err error
 	if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
-		log.Trace("%s", err)
+		log.Trace("IsDependenciesEnabled: %v", err)
 		return setting.Service.DefaultEnableDependencies
 	}
 	return u.IssuesConfig().EnableDependencies
diff --git a/modules/util/util.go b/modules/util/util.go
index 3921002e2a..44b5a6ed81 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -214,7 +214,7 @@ func ToPointer[T any](val T) *T {
 }
 
 // Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
-func Iif[T comparable](condition bool, trueVal, falseVal T) T {
+func Iif[T any](condition bool, trueVal, falseVal T) T {
 	if condition {
 		return trueVal
 	}
diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go
index 8d97e8a3f8..1744426ee8 100644
--- a/routers/api/v1/notify/repo.go
+++ b/routers/api/v1/notify/repo.go
@@ -10,7 +10,6 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
@@ -201,7 +200,6 @@ func ReadRepoNotifications(ctx *context.APIContext) {
 	if !ctx.FormBool("all") {
 		statuses := ctx.FormStrings("status-types")
 		opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
-		log.Error("%v", opts.Status)
 	}
 	nl, err := db.Find[activities_model.Notification](ctx, opts)
 	if err != nil {
diff --git a/routers/private/actions.go b/routers/private/actions.go
index 53c2412308..696634b5e7 100644
--- a/routers/private/actions.go
+++ b/routers/private/actions.go
@@ -26,7 +26,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
 	defer rd.Close()
 
 	if err := json.NewDecoder(rd).Decode(&genRequest); err != nil {
-		log.Error("%v", err)
+		log.Error("JSON Decode failed: %v", err)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
@@ -35,7 +35,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
 
 	owner, repo, err := parseScope(ctx, genRequest.Scope)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("parseScope failed: %v", err)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
@@ -45,18 +45,18 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
 	if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
 		token, err = actions_model.NewRunnerToken(ctx, owner, repo)
 		if err != nil {
-			err := fmt.Sprintf("error while creating runner token: %v", err)
-			log.Error("%v", err)
+			errMsg := fmt.Sprintf("error while creating runner token: %v", err)
+			log.Error("NewRunnerToken failed: %v", errMsg)
 			ctx.JSON(http.StatusInternalServerError, private.Response{
-				Err: err,
+				Err: errMsg,
 			})
 			return
 		}
 	} else if err != nil {
-		err := fmt.Sprintf("could not get unactivated runner token: %v", err)
-		log.Error("%v", err)
+		errMsg := fmt.Sprintf("could not get unactivated runner token: %v", err)
+		log.Error("GetLatestRunnerToken failed: %v", errMsg)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
-			Err: err,
+			Err: errMsg,
 		})
 		return
 	}
diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go
index 42b8e5abed..764c976fa9 100644
--- a/routers/private/hook_verification.go
+++ b/routers/private/hook_verification.go
@@ -47,7 +47,7 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []
 			_ = stdoutWriter.Close()
 			err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
 			if err != nil {
-				log.Error("%v", err)
+				log.Error("readAndVerifyCommitsFromShaReader failed: %v", err)
 				cancel()
 			}
 			_ = stdoutReader.Close()
@@ -66,7 +66,6 @@ func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository
 		line := scanner.Text()
 		err := readAndVerifyCommit(line, repo, env)
 		if err != nil {
-			log.Error("%v", err)
 			return err
 		}
 	}
diff --git a/routers/private/mail.go b/routers/private/mail.go
index c19ee67896..cf3abb31c6 100644
--- a/routers/private/mail.go
+++ b/routers/private/mail.go
@@ -35,7 +35,7 @@ func SendEmail(ctx *context.PrivateContext) {
 	defer rd.Close()
 
 	if err := json.NewDecoder(rd).Decode(&mail); err != nil {
-		log.Error("%v", err)
+		log.Error("JSON Decode failed: %v", err)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index b93668c5a2..ea9d6f4c9c 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -403,7 +403,6 @@ func EditUserPost(ctx *context.Context) {
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
 		case password.IsErrIsPwnedRequest(err):
-			log.Error("%s", err.Error())
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
 		default:
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index f6b76c1ffd..0e88fe68f9 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -214,7 +214,6 @@ func ResetPasswdPost(ctx *context.Context) {
 		case errors.Is(err, password.ErrIsPwned):
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplResetPassword, nil)
 		case password.IsErrIsPwnedRequest(err):
-			log.Error("%s", err.Error())
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
 		default:
 			ctx.ServerError("UpdateAuth", err)
@@ -298,7 +297,6 @@ func MustChangePasswordPost(ctx *context.Context) {
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
 		case password.IsErrIsPwnedRequest(err):
-			log.Error("%s", err.Error())
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
 		default:
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index c93b70af76..8ea7548e51 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -74,7 +74,6 @@ func AccountPost(ctx *context.Context) {
 			case errors.Is(err, password.ErrIsPwned):
 				ctx.Flash.Error(ctx.Tr("auth.password_pwned"))
 			case password.IsErrIsPwnedRequest(err):
-				log.Error("%s", err.Error())
 				ctx.Flash.Error(ctx.Tr("auth.password_pwned_err"))
 			default:
 				ctx.ServerError("UpdateAuth", err)
diff --git a/services/context/captcha.go b/services/context/captcha.go
index a1999900c9..fa8d779f56 100644
--- a/services/context/captcha.go
+++ b/services/context/captcha.go
@@ -79,11 +79,11 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form any) {
 	case setting.CfTurnstile:
 		valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField))
 	default:
-		ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
+		ctx.ServerError("Unknown Captcha Type", fmt.Errorf("unknown Captcha Type: %s", setting.Service.CaptchaType))
 		return
 	}
 	if err != nil {
-		log.Debug("%v", err)
+		log.Debug("Captcha Verify failed: %v", err)
 	}
 
 	if !valid {
diff --git a/services/notify/notify.go b/services/notify/notify.go
index 16fbb6325d..0c8262ef7a 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -91,7 +91,7 @@ func AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues
 // NewPullRequest notifies new pull request to notifiers
 func NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) {
 	if err := pr.LoadIssue(ctx); err != nil {
-		log.Error("%v", err)
+		log.Error("LoadIssue failed: %v", err)
 		return
 	}
 	if err := pr.Issue.LoadPoster(ctx); err != nil {
@@ -112,7 +112,7 @@ func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *iss
 // PullRequestReview notifies new pull request review
 func PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
 	if err := review.LoadReviewer(ctx); err != nil {
-		log.Error("%v", err)
+		log.Error("LoadReviewer failed: %v", err)
 		return
 	}
 	for _, notifier := range notifiers {
diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go
index 613b46d8f6..451a182155 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -28,7 +28,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
 
 	t, err := NewTemporaryUploadRepository(ctx, repo)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("NewTemporaryUploadRepository failed: %v", err)
 	}
 	defer t.Close()
 	if err := t.Clone(opts.OldBranch, false); err != nil {
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index f6d5643dc9..e5f7e2af96 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -111,7 +111,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
 
 	t, err := NewTemporaryUploadRepository(ctx, repo)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("NewTemporaryUploadRepository failed: %v", err)
 	}
 	defer t.Close()
 	if err := t.Clone(opts.OldBranch, true); err != nil {
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index 4f7178184b..f029a9aefe 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -143,7 +143,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
 
 	t, err := NewTemporaryUploadRepository(ctx, repo)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("NewTemporaryUploadRepository failed: %v", err)
 	}
 	defer t.Close()
 	hasOldBranch := true
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 1b921a44bd..f8387416c1 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -161,7 +161,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		if isOldWikiExist {
 			err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
 			if err != nil {
-				log.Error("%v", err)
+				log.Error("RemoveFilesFromIndex failed: %v", err)
 				return err
 			}
 		}
@@ -171,18 +171,18 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 
 	objectHash, err := gitRepo.HashObject(strings.NewReader(content))
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("HashObject failed: %v", err)
 		return err
 	}
 
 	if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
-		log.Error("%v", err)
+		log.Error("AddObjectToIndex failed: %v", err)
 		return err
 	}
 
 	tree, err := gitRepo.WriteTree()
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("WriteTree failed: %v", err)
 		return err
 	}
 
@@ -207,7 +207,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 
 	commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("CommitTree failed: %v", err)
 		return err
 	}
 
@@ -222,11 +222,11 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 			0,
 		),
 	}); err != nil {
-		log.Error("%v", err)
+		log.Error("Push failed: %v", err)
 		if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
 			return err
 		}
-		return fmt.Errorf("Push: %w", err)
+		return fmt.Errorf("failed to push: %w", err)
 	}
 
 	return nil

From 644ade5ae6805a3db063b3f81a596febe49bacaf Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 7 Apr 2024 14:36:33 +0200
Subject: [PATCH 638/679] Fix checkboxes on mobile view, remove some dead css
 (#30308)

Fix the checkbox issues in
https://github.com/go-gitea/gitea/issues/30303 which were existing
problems with these selectors, but made visible with
https://github.com/go-gitea/gitea/pull/30162.

There is a lot of dead/useless CSS in `form.css`, I only fixed the two
problems and remove CSS that was definitely not in use or needed.

<img width="369" alt="Screenshot 2024-04-06 at 18 00 08"
src="https://github.com/go-gitea/gitea/assets/115237/720f178b-1b22-48d4-8704-becb8ce66129">
<img width="405" alt="Screenshot 2024-04-06 at 18 00 28"
src="https://github.com/go-gitea/gitea/assets/115237/61c0f8ec-34af-46c5-a3fa-7c5c4d30c7d2">

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/form.css | 30 ++++--------------------------
 1 file changed, 4 insertions(+), 26 deletions(-)

diff --git a/web_src/css/form.css b/web_src/css/form.css
index 2a976c056d..a8f73b6b66 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -249,21 +249,6 @@ textarea:focus,
   .user.signup form .optional .title {
     margin-left: 250px !important;
   }
-  .user.activate form .inline.field > input,
-  .user.forgot.password form .inline.field > input,
-  .user.reset.password form .inline.field > input,
-  .user.link-account form .inline.field > input,
-  .user.signin form .inline.field > input,
-  .user.signup form .inline.field > input,
-  .user.activate form .inline.field > textarea,
-  .user.forgot.password form .inline.field > textarea,
-  .user.reset.password form .inline.field > textarea,
-  .user.link-account form .inline.field > textarea,
-  .user.signin form .inline.field > textarea,
-  .user.signup form .inline.field > textarea,
-  .oauth-login-link {
-    width: 50%;
-  }
 }
 
 @media (max-width: 767.98px) {
@@ -310,14 +295,7 @@ textarea:focus,
   .user.reset.password form .inline.field > label,
   .user.link-account form .inline.field > label,
   .user.signin form .inline.field > label,
-  .user.signup form .inline.field > label,
-  .user.activate form input,
-  .user.forgot.password form input,
-  .user.reset.password form input,
-  .user.link-account form input,
-  .user.signin form input,
-  .user.signup form input,
-  .oauth-login-link {
+  .user.signup form .inline.field > label {
     width: 100% !important;
   }
 }
@@ -435,9 +413,9 @@ textarea:focus,
   .repository.new.repo form label,
   .repository.new.migrate form label,
   .repository.new.fork form label,
-  .repository.new.repo form input,
-  .repository.new.migrate form input,
-  .repository.new.fork form input,
+  .repository.new.repo form .inline.field > input,
+  .repository.new.migrate form .inline.field > input,
+  .repository.new.fork form .inline.field > input,
   .repository.new.fork form .field a,
   .repository.new.repo form .selection.dropdown,
   .repository.new.migrate form .selection.dropdown,

From 0178eaec256a349371c75e582edd7fefca2085d0 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 7 Apr 2024 14:41:42 +0200
Subject: [PATCH 639/679] Action view mobile improvements and fixes (#30309)

Fix the action issue in https://github.com/go-gitea/gitea/issues/30303,
specifically:

- Use opaque step header hover background to avoid transparency issue
- Un-sticky the `action-view-left` on mobile, it would otherwise overlap
into right view
- Improve commit summary, let it wrap
- Fix and comment z-indexes
- Tweak width for run-list-item-right so it wastes less space on desktop
- Synced latest changes to console colors from dark to light theme

<img width="467" alt="Screenshot 2024-04-06 at 18 58 15"
src="https://github.com/go-gitea/gitea/assets/115237/8ad26b72-6cd9-4522-8ad1-6fd86b2d0d53">
---
 web_src/css/actions.css                  |  2 +-
 web_src/css/themes/theme-gitea-dark.css  |  2 +-
 web_src/css/themes/theme-gitea-light.css | 12 +++++-----
 web_src/js/components/RepoActionView.vue | 28 +++++++++++++++++++-----
 4 files changed, 30 insertions(+), 14 deletions(-)

diff --git a/web_src/css/actions.css b/web_src/css/actions.css
index e7b9a3855a..1d5bea2395 100644
--- a/web_src/css/actions.css
+++ b/web_src/css/actions.css
@@ -44,7 +44,7 @@
 }
 
 .run-list-item-right {
-  flex: 0 0 15%;
+  flex: 0 0 min(20%, 130px);
   display: flex;
   flex-direction: column;
   gap: 3px;
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index 07e217742d..ed6718e40c 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -65,7 +65,7 @@
   --color-console-fg-subtle: #bec4c8;
   --color-console-bg: #171b1e;
   --color-console-border: #2e353b;
-  --color-console-hover-bg: #e8e8ff16;
+  --color-console-hover-bg: #292d31;
   --color-console-active-bg: #2e353b;
   --color-console-menu-bg: #252b30;
   --color-console-menu-border: #424b51;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 2741e0e0bd..b10ad7d840 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -63,12 +63,12 @@
   /* console colors - used for actions console and console files */
   --color-console-fg: #f8f8f9;
   --color-console-fg-subtle: #bec4c8;
-  --color-console-bg: #181b1d;
-  --color-console-border: #313538;
-  --color-console-hover-bg: #ffffff16;
-  --color-console-active-bg: #313538;
-  --color-console-menu-bg: #272b2e;
-  --color-console-menu-border: #464a4d;
+  --color-console-bg: #171b1e;
+  --color-console-border: #2e353b;
+  --color-console-hover-bg: #292d31;
+  --color-console-active-bg: #2e353b;
+  --color-console-menu-bg: #252b30;
+  --color-console-menu-border: #424b51;
   /* named colors */
   --color-red: #db2828;
   --color-orange: #f2711c;
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 75cd1db70a..06c42f0b35 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -517,8 +517,16 @@ export function initRepositoryActionView() {
 
 .action-commit-summary {
   display: flex;
+  flex-wrap: wrap;
   gap: 5px;
-  margin: 0 0 0 28px;
+  margin-left: 28px;
+}
+
+@media (max-width: 767.98px) {
+  .action-commit-summary {
+    margin-left: 0;
+    margin-top: 8px;
+  }
 }
 
 /* ================ */
@@ -531,6 +539,14 @@ export function initRepositoryActionView() {
   top: 12px;
   max-height: 100vh;
   overflow-y: auto;
+  background: var(--color-body);
+  z-index: 2; /* above .job-info-header */
+}
+
+@media (max-width: 767.98px) {
+  .action-view-left {
+    position: static; /* can not sticky because multiple jobs would overlap into right view */
+  }
 }
 
 .job-artifacts-title {
@@ -692,7 +708,9 @@ export function initRepositoryActionView() {
   position: sticky;
   top: 0;
   height: 60px;
-  z-index: 1;
+  z-index: 1; /* above .job-step-container */
+  background: var(--color-console-bg);
+  border-radius: 3px;
 }
 
 .job-info-header:has(+ .job-step-container) {
@@ -730,7 +748,7 @@ export function initRepositoryActionView() {
 
 .job-step-container .job-step-summary.step-expandable:hover {
   color: var(--color-console-fg);
-  background-color: var(--color-console-hover-bg);
+  background: var(--color-console-hover-bg);
 }
 
 .job-step-container .job-step-summary .step-summary-msg {
@@ -748,17 +766,15 @@ export function initRepositoryActionView() {
   top: 60px;
 }
 
-@media (max-width: 768px) {
+@media (max-width: 767.98px) {
   .action-view-body {
     flex-direction: column;
   }
   .action-view-left, .action-view-right {
     width: 100%;
   }
-
   .action-view-left {
     max-width: none;
-    overflow-y: hidden;
   }
 }
 </style>

From 019857a7015cae32c12b5eac0b895c05f0264b77 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 7 Apr 2024 17:45:36 +0200
Subject: [PATCH 640/679] Add `--page-spacing` variable, fix admin dashboard
 notice (#30302)

Fixes https://github.com/go-gitea/gitea/issues/30293 and introduce the
`--page-spacing` variable which holds the spacing between the elements
on the page. This is working vertically for all pages, including ones
that have fomantic grid, and horizontally for all that use
`flex-container`.

The `.page-content > :first-child:not(.secondary-nav)` selector uses
margin which in some cases enables to adjacent margins to overlap, which
is nice.

<img width="1320" alt="Screenshot 2024-04-06 at 01 35 19"
src="https://github.com/go-gitea/gitea/assets/115237/3e81e707-e9ff-4b7f-a211-3d98f4f85353">
---
<img width="1327" alt="Screenshot 2024-04-06 at 01 35 45"
src="https://github.com/go-gitea/gitea/assets/115237/aad196c0-9e21-4c06-ae59-7e33a76c61e1">
---
<img width="1321" alt="Screenshot 2024-04-06 at 01 35 31"
src="https://github.com/go-gitea/gitea/assets/115237/785f6c5d-08b6-4e66-aa16-aeca7cfed3ad">
---
 templates/user/notification/notification_div.tmpl |  2 +-
 web_src/css/base.css                              | 12 ++++++++----
 web_src/css/modules/flexcontainer.css             |  3 ++-
 3 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index 04e79ba749..bf3b51ee3b 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -1,7 +1,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
 	<div class="ui container">
 		{{$notificationUnreadCount := call .NotificationUnreadCount}}
-		<div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
+		<div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]">
 			<div class="small-menu-items ui compact tiny menu">
 				<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
 					{{ctx.Locale.Tr "notification.unread"}}
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 096b67058e..44dc83e6f3 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -24,6 +24,7 @@
   --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
   --tab-size: 4;
   --checkbox-size: 16px; /* height and width of checkbox and radio inputs */
+  --page-spacing: 16px; /* space between page elements */
 }
 
 :root * {
@@ -582,11 +583,14 @@ img.ui.avatar,
   margin-bottom: 14px;
 }
 
-/* add padding to all content when there is no .secondary.nav. this uses padding instead of
-   margin because with the negative margin on .ui.grid we would have to set margin-top: 0,
-   but that does not work universally for all pages */
+/* add margin to all pages when there is no .secondary.nav */
 .page-content > :first-child:not(.secondary-nav) {
-  padding-top: 14px;
+  margin-top: var(--page-spacing);
+}
+/* if .ui.grid is the first child the first grid-column has 'padding-top: 1rem' which we need
+   to compensate here */
+.page-content > :first-child.ui.grid {
+  margin-top: calc(var(--page-spacing) - 1rem);
 }
 
 .ui.pagination.menu .active.item {
diff --git a/web_src/css/modules/flexcontainer.css b/web_src/css/modules/flexcontainer.css
index 0b559f1e7d..1ca513687f 100644
--- a/web_src/css/modules/flexcontainer.css
+++ b/web_src/css/modules/flexcontainer.css
@@ -2,7 +2,8 @@
 
 .flex-container {
   display: flex !important;
-  gap: 16px;
+  gap: var(--page-spacing);
+  margin-top: var(--page-spacing);
 }
 
 .flex-container-nav {

From 36887ed3921d03f1864360c95bd2ecf853bfbe72 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sun, 7 Apr 2024 18:19:25 +0200
Subject: [PATCH 641/679] Fix and rewrite contrast color calculation, fix
 project-related bugs (#30237)

1. The previous color contrast calculation function was incorrect at
least for the `#84b6eb` where it output low-contrast white instead of
black. I've rewritten these functions now to accept hex colors and to
match GitHub's calculation and to output pure white/black for maximum
contrast. Before and after:
<img width="94" alt="Screenshot 2024-04-02 at 01 53 46"
src="https://github.com/go-gitea/gitea/assets/115237/00b39e15-a377-4458-95cf-ceec74b78228"><img
width="90" alt="Screenshot 2024-04-02 at 01 51 30"
src="https://github.com/go-gitea/gitea/assets/115237/1677067a-8d8f-47eb-82c0-76330deeb775">

2. Fix project-related issues:

- Expose the new `ContrastColor` function as template helper and use it
for project cards, replacing the previous JS solution which eliminates a
flash of wrong color on page load.
- Fix a bug where if editing a project title, the counter would get
lost.
- Move `rgbToHex` function to color utils.

@HesterG fyi

---------

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 modules/templates/helper.go              |  6 +--
 modules/templates/util_render.go         | 11 ++---
 modules/util/color.go                    | 42 +++++++---------
 modules/util/color_test.go               | 46 +++++++++---------
 templates/projects/view.tmpl             |  8 ++--
 web_src/css/features/projects.css        | 27 +++++------
 web_src/css/repo.css                     | 15 +++++-
 web_src/css/repo/issue-list.css          | 17 -------
 web_src/css/themes/theme-gitea-dark.css  |  2 -
 web_src/css/themes/theme-gitea-light.css |  2 -
 web_src/js/components/ContextPopup.vue   | 20 +++-----
 web_src/js/features/repo-projects.js     | 61 ++++++++----------------
 web_src/js/utils/color.js                | 30 ++++++------
 web_src/js/utils/color.test.js           | 39 +++++++--------
 14 files changed, 135 insertions(+), 191 deletions(-)

diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 9e770a2606..5d2fa79bc5 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -53,13 +53,13 @@ func NewFuncMap() template.FuncMap {
 		"JsonUtils":   NewJsonUtils,
 
 		// -----------------------------------------------------------------
-		// svg / avatar / icon
+		// svg / avatar / icon / color
 		"svg":           svg.RenderHTML,
 		"EntryIcon":     base.EntryIcon,
 		"MigrationIcon": MigrationIcon,
 		"ActionIcon":    ActionIcon,
-
-		"SortArrow": SortArrow,
+		"SortArrow":     SortArrow,
+		"ContrastColor": util.ContrastColor,
 
 		// -----------------------------------------------------------------
 		// time / number / format
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index d1c9b082fa..0b53965f25 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -123,16 +123,10 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
 func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
 	var (
 		archivedCSSClass string
-		textColor        = "#111"
+		textColor        = util.ContrastColor(label.Color)
 		labelScope       = label.ExclusiveScope()
 	)
 
-	r, g, b := util.HexToRBGColor(label.Color)
-	// Determine if label text should be light or dark to be readable on background color
-	if util.UseLightTextOnBackground(r, g, b) {
-		textColor = "#eee"
-	}
-
 	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
 
 	if label.IsArchived() {
@@ -153,7 +147,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
 
 	// Make scope and item background colors slightly darker and lighter respectively.
 	// More contrast needed with higher luminance, empirically tweaked.
-	luminance := util.GetLuminance(r, g, b)
+	luminance := util.GetRelativeLuminance(label.Color)
 	contrast := 0.01 + luminance*0.03
 	// Ensure we add the same amount of contrast also near 0 and 1.
 	darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
@@ -162,6 +156,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
 	darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
 	lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
 
+	r, g, b := util.HexToRBGColor(label.Color)
 	scopeBytes := []byte{
 		uint8(math.Min(math.Round(r*darkenFactor), 255)),
 		uint8(math.Min(math.Round(g*darkenFactor), 255)),
diff --git a/modules/util/color.go b/modules/util/color.go
index 240b045c28..9c520dce78 100644
--- a/modules/util/color.go
+++ b/modules/util/color.go
@@ -4,22 +4,10 @@ package util
 
 import (
 	"fmt"
-	"math"
 	"strconv"
 	"strings"
 )
 
-// Check similar implementation in web_src/js/utils/color.js and keep synchronization
-
-// Return R, G, B values defined in reletive luminance
-func getLuminanceRGB(channel float64) float64 {
-	sRGB := channel / 255
-	if sRGB <= 0.03928 {
-		return sRGB / 12.92
-	}
-	return math.Pow((sRGB+0.055)/1.055, 2.4)
-}
-
 // Get color as RGB values in 0..255 range from the hex color string (with or without #)
 func HexToRBGColor(colorString string) (float64, float64, float64) {
 	hexString := colorString
@@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
 	return r, g, b
 }
 
-// return luminance given RGB channels
-// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
-func GetLuminance(r, g, b float64) float64 {
-	R := getLuminanceRGB(r)
-	G := getLuminanceRGB(g)
-	B := getLuminanceRGB(b)
-	luminance := 0.2126*R + 0.7152*G + 0.0722*B
-	return luminance
+// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
+// Keep this in sync with web_src/js/utils/color.js
+func GetRelativeLuminance(color string) float64 {
+	r, g, b := HexToRBGColor(color)
+	return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255
 }
 
-// Reference from: https://firsching.ch/github_labels.html
-// In the future WCAG 3 APCA may be a better solution.
-// Check if text should use light color based on RGB of background
-func UseLightTextOnBackground(r, g, b float64) bool {
-	return GetLuminance(r, g, b) < 0.453
+func UseLightText(backgroundColor string) bool {
+	return GetRelativeLuminance(backgroundColor) < 0.453
+}
+
+// Given a background color, returns a black or white foreground color that the highest
+// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
+// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
+func ContrastColor(backgroundColor string) string {
+	if UseLightText(backgroundColor) {
+		return "#fff"
+	}
+	return "#000"
 }
diff --git a/modules/util/color_test.go b/modules/util/color_test.go
index d96ac36730..be6e6b122a 100644
--- a/modules/util/color_test.go
+++ b/modules/util/color_test.go
@@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) {
 	}
 }
 
-func Test_UseLightTextOnBackground(t *testing.T) {
+func Test_UseLightText(t *testing.T) {
 	cases := []struct {
-		r        float64
-		g        float64
-		b        float64
-		expected bool
+		color    string
+		expected string
 	}{
-		{215, 58, 74, true},
-		{0, 117, 202, true},
-		{207, 211, 215, false},
-		{162, 238, 239, false},
-		{112, 87, 255, true},
-		{0, 134, 114, true},
-		{228, 230, 105, false},
-		{216, 118, 227, true},
-		{255, 255, 255, false},
-		{43, 134, 133, true},
-		{43, 135, 134, true},
-		{44, 135, 134, true},
-		{59, 182, 179, true},
-		{124, 114, 104, true},
-		{126, 113, 108, true},
-		{129, 112, 109, true},
-		{128, 112, 112, true},
+		{"#d73a4a", "#fff"},
+		{"#0075ca", "#fff"},
+		{"#cfd3d7", "#000"},
+		{"#a2eeef", "#000"},
+		{"#7057ff", "#fff"},
+		{"#008672", "#fff"},
+		{"#e4e669", "#000"},
+		{"#d876e3", "#000"},
+		{"#ffffff", "#000"},
+		{"#2b8684", "#fff"},
+		{"#2b8786", "#fff"},
+		{"#2c8786", "#000"},
+		{"#3bb6b3", "#000"},
+		{"#7c7268", "#fff"},
+		{"#7e716c", "#fff"},
+		{"#81706d", "#fff"},
+		{"#807070", "#fff"},
+		{"#84b6eb", "#000"},
 	}
 	for n, c := range cases {
-		result := UseLightTextOnBackground(c.r, c.g, c.b)
-		assert.Equal(t, c.expected, result, "case %d: error should match", n)
+		assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n)
 	}
 }
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 33dd758c79..f9b85360e0 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -66,13 +66,13 @@
 <div id="project-board">
 	<div class="board {{if .CanWriteProjects}}sortable{{end}}">
 		{{range .Columns}}
-			<div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
+			<div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
 				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
 					<div class="ui large label project-column-title tw-py-1">
 						<div class="ui small circular grey label project-column-issue-count">
 							{{.NumIssues ctx}}
 						</div>
-						{{.Title}}
+						<span class="project-column-title-label">{{.Title}}</span>
 					</div>
 					{{if $canWriteProject}}
 						<div class="ui dropdown jump item">
@@ -153,9 +153,7 @@
 						</div>
 					{{end}}
 				</div>
-
-				<div class="divider"></div>
-
+				<div class="divider"{{if .Color}} style="color: {{ContrastColor .Color}} !important"{{end}}></div>
 				<div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
 					{{range (index $.IssuesMap .ID)}}
 						<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index cec5e6fc64..e23c146748 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -22,34 +22,27 @@
   cursor: default;
 }
 
+.project-column .issue-card {
+  color: var(--color-text);
+}
+
 .project-column-header {
   display: flex;
   align-items: center;
   justify-content: space-between;
 }
 
-.project-column-header.dark-label {
-  color: var(--color-project-board-dark-label) !important;
-}
-
-.project-column-header.dark-label .project-column-title {
-  color: var(--color-project-board-dark-label) !important;
-}
-
-.project-column-header.light-label {
-  color: var(--color-project-board-light-label) !important;
-}
-
-.project-column-header.light-label .project-column-title {
-  color: var(--color-project-board-light-label) !important;
-}
-
 .project-column-title {
   background: none !important;
   line-height: 1.25 !important;
   cursor: inherit;
 }
 
+.project-column-title,
+.project-column-issue-count {
+  color: inherit !important;
+}
+
 .project-column > .cards {
   flex: 1;
   display: flex;
@@ -64,6 +57,8 @@
 
 .project-column > .divider {
   margin: 5px 0;
+  border-color: currentcolor;
+  opacity: .5;
 }
 
 .project-column:first-child {
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 653af379d5..c50d13a174 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2273,8 +2273,21 @@
   height: 0.5em;
 }
 
+.labels-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.25em;
+}
+
+.labels-list a {
+  display: flex;
+  text-decoration: none;
+}
+
 .labels-list .label {
-  margin: 2px 0;
+  padding: 0 6px;
+  margin: 0 !important;
+  min-height: 20px;
   display: inline-flex !important;
   line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
 }
diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css
index fe8231d718..77905956f0 100644
--- a/web_src/css/repo/issue-list.css
+++ b/web_src/css/repo/issue-list.css
@@ -34,23 +34,6 @@
   }
 }
 
-#issue-list .flex-item-title .labels-list {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 0.25em;
-}
-
-#issue-list .flex-item-title .labels-list a {
-  display: flex;
-  text-decoration: none;
-}
-
-#issue-list .flex-item-title .labels-list .label {
-  padding: 0 6px;
-  margin: 0;
-  min-height: 20px;
-}
-
 #issue-list .flex-item-body .branches {
   display: inline-flex;
 }
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index ed6718e40c..c74f334c2d 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -215,8 +215,6 @@
   --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-5);
   --color-project-board-bg: var(--color-secondary-light-2);
-  --color-project-board-dark-label: #0e1011;
-  --color-project-board-light-label: #dde0e2;
   --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
   --color-reaction-bg: #e8e8ff12;
   --color-reaction-hover-bg: var(--color-primary-light-4);
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index b10ad7d840..01dd8ba4f7 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -215,8 +215,6 @@
   --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-6);
   --color-project-board-bg: var(--color-secondary-light-4);
-  --color-project-board-dark-label: #0e1114;
-  --color-project-board-light-label: #eaeef2;
   --color-caret: var(--color-text-dark);
   --color-reaction-bg: #0000170a;
   --color-reaction-hover-bg: var(--color-primary-light-5);
diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue
index d87eb1a180..65a6089522 100644
--- a/web_src/js/components/ContextPopup.vue
+++ b/web_src/js/components/ContextPopup.vue
@@ -1,7 +1,6 @@
 <script>
 import {SvgIcon} from '../svg.js';
-import {useLightTextOnBackground} from '../utils/color.js';
-import tinycolor from 'tinycolor2';
+import {contrastColor} from '../utils/color.js';
 import {GET} from '../modules/fetch.js';
 
 const {appSubUrl, i18n} = window.config;
@@ -59,16 +58,11 @@ export default {
     },
 
     labels() {
-      return this.issue.labels.map((label) => {
-        let textColor;
-        const {r, g, b} = tinycolor(label.color).toRgb();
-        if (useLightTextOnBackground(r, g, b)) {
-          textColor = '#eeeeee';
-        } else {
-          textColor = '#111111';
-        }
-        return {name: label.name, color: `#${label.color}`, textColor};
-      });
+      return this.issue.labels.map((label) => ({
+        name: label.name,
+        color: `#${label.color}`,
+        textColor: contrastColor(`#${label.color}`),
+      }));
     },
   },
   mounted() {
@@ -108,7 +102,7 @@ export default {
       <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
       <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
       <p>{{ body }}</p>
-      <div>
+      <div class="labels-list">
         <div
           v-for="label in labels"
           :key="label.name"
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index 80e945a0f2..a869c24c82 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -1,8 +1,8 @@
 import $ from 'jquery';
-import {useLightTextOnBackground} from '../utils/color.js';
-import tinycolor from 'tinycolor2';
+import {contrastColor} from '../utils/color.js';
 import {createSortable} from '../modules/sortable.js';
 import {POST, DELETE, PUT} from '../modules/fetch.js';
+import tinycolor from 'tinycolor2';
 
 function updateIssueCount(cards) {
   const parent = cards.parentElement;
@@ -65,14 +65,11 @@ async function initRepoProjectSortable() {
       boardColumns = mainBoard.getElementsByClassName('project-column');
       for (let i = 0; i < boardColumns.length; i++) {
         const column = boardColumns[i];
-        if (parseInt($(column).data('sorting')) !== i) {
+        if (parseInt(column.getAttribute('data-sorting')) !== i) {
           try {
-            await PUT($(column).data('url'), {
-              data: {
-                sorting: i,
-                color: rgbToHex(window.getComputedStyle($(column)[0]).backgroundColor),
-              },
-            });
+            const bgColor = column.style.backgroundColor; // will be rgb() string
+            const color = bgColor ? tinycolor(bgColor).toHexString() : '';
+            await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}});
           } catch (error) {
             console.error(error);
           }
@@ -102,16 +99,10 @@ export function initRepoProject() {
 
   for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
     const projectHeader = modal.closest('.project-column-header');
-    const projectTitleLabel = projectHeader?.querySelector('.project-column-title');
+    const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
     const projectTitleInput = modal.querySelector('.project-column-title-input');
     const projectColorInput = modal.querySelector('#new_project_column_color');
     const boardColumn = modal.closest('.project-column');
-    const bgColor = boardColumn?.style.backgroundColor;
-
-    if (bgColor) {
-      setLabelColor(projectHeader, rgbToHex(bgColor));
-    }
-
     modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
       e.preventDefault();
       try {
@@ -126,10 +117,21 @@ export function initRepoProject() {
       } finally {
         projectTitleLabel.textContent = projectTitleInput?.value;
         projectTitleInput.closest('form')?.classList.remove('dirty');
-        if (projectColorInput?.value) {
-          setLabelColor(projectHeader, projectColorInput.value);
+        const dividers = boardColumn.querySelectorAll(':scope > .divider');
+        if (projectColorInput.value) {
+          const color = contrastColor(projectColorInput.value);
+          boardColumn.style.setProperty('background', projectColorInput.value, 'important');
+          boardColumn.style.setProperty('color', color, 'important');
+          for (const divider of dividers) {
+            divider.style.setProperty('color', color);
+          }
+        } else {
+          boardColumn.style.removeProperty('background');
+          boardColumn.style.removeProperty('color');
+          for (const divider of dividers) {
+            divider.style.removeProperty('color');
+          }
         }
-        boardColumn.style = `background: ${projectColorInput.value} !important`;
         $('.ui.modal').modal('hide');
       }
     });
@@ -182,24 +184,3 @@ export function initRepoProject() {
     createNewColumn(url, $columnTitle, $projectColorInput);
   });
 }
-
-function setLabelColor(label, color) {
-  const {r, g, b} = tinycolor(color).toRgb();
-  if (useLightTextOnBackground(r, g, b)) {
-    label.classList.remove('dark-label');
-    label.classList.add('light-label');
-  } else {
-    label.classList.remove('light-label');
-    label.classList.add('dark-label');
-  }
-}
-
-function rgbToHex(rgb) {
-  rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/);
-  return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`;
-}
-
-function hex(x) {
-  const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
-  return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
-}
diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js
index 0ba6af49ee..198f97c454 100644
--- a/web_src/js/utils/color.js
+++ b/web_src/js/utils/color.js
@@ -1,23 +1,21 @@
-// Check similar implementation in modules/util/color.go and keep synchronization
-// Return R, G, B values defined in reletive luminance
-function getLuminanceRGB(channel) {
-  const sRGB = channel / 255;
-  return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
+import tinycolor from 'tinycolor2';
+
+// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
+// Keep this in sync with modules/util/color.go
+function getRelativeLuminance(color) {
+  const {r, g, b} = tinycolor(color).toRgb();
+  return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
 }
 
-// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
-function getLuminance(r, g, b) {
-  const R = getLuminanceRGB(r);
-  const G = getLuminanceRGB(g);
-  const B = getLuminanceRGB(b);
-  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
+function useLightText(backgroundColor) {
+  return getRelativeLuminance(backgroundColor) < 0.453;
 }
 
-// Reference from: https://firsching.ch/github_labels.html
-// In the future WCAG 3 APCA may be a better solution.
-// Check if text should use light color based on RGB of background
-export function useLightTextOnBackground(r, g, b) {
-  return getLuminance(r, g, b) < 0.453;
+// Given a background color, returns a black or white foreground color that the highest
+// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
+// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
+export function contrastColor(backgroundColor) {
+  return useLightText(backgroundColor) ? '#fff' : '#000';
 }
 
 function resolveColors(obj) {
diff --git a/web_src/js/utils/color.test.js b/web_src/js/utils/color.test.js
index e129109ef0..fee9afc776 100644
--- a/web_src/js/utils/color.test.js
+++ b/web_src/js/utils/color.test.js
@@ -1,21 +1,22 @@
-import {useLightTextOnBackground} from './color.js';
+import {contrastColor} from './color.js';
 
-test('useLightTextOnBackground', () => {
-  expect(useLightTextOnBackground(215, 58, 74)).toBe(true);
-  expect(useLightTextOnBackground(0, 117, 202)).toBe(true);
-  expect(useLightTextOnBackground(207, 211, 215)).toBe(false);
-  expect(useLightTextOnBackground(162, 238, 239)).toBe(false);
-  expect(useLightTextOnBackground(112, 87, 255)).toBe(true);
-  expect(useLightTextOnBackground(0, 134, 114)).toBe(true);
-  expect(useLightTextOnBackground(228, 230, 105)).toBe(false);
-  expect(useLightTextOnBackground(216, 118, 227)).toBe(true);
-  expect(useLightTextOnBackground(255, 255, 255)).toBe(false);
-  expect(useLightTextOnBackground(43, 134, 133)).toBe(true);
-  expect(useLightTextOnBackground(43, 135, 134)).toBe(true);
-  expect(useLightTextOnBackground(44, 135, 134)).toBe(true);
-  expect(useLightTextOnBackground(59, 182, 179)).toBe(true);
-  expect(useLightTextOnBackground(124, 114, 104)).toBe(true);
-  expect(useLightTextOnBackground(126, 113, 108)).toBe(true);
-  expect(useLightTextOnBackground(129, 112, 109)).toBe(true);
-  expect(useLightTextOnBackground(128, 112, 112)).toBe(true);
+test('contrastColor', () => {
+  expect(contrastColor('#d73a4a')).toBe('#fff');
+  expect(contrastColor('#0075ca')).toBe('#fff');
+  expect(contrastColor('#cfd3d7')).toBe('#000');
+  expect(contrastColor('#a2eeef')).toBe('#000');
+  expect(contrastColor('#7057ff')).toBe('#fff');
+  expect(contrastColor('#008672')).toBe('#fff');
+  expect(contrastColor('#e4e669')).toBe('#000');
+  expect(contrastColor('#d876e3')).toBe('#000');
+  expect(contrastColor('#ffffff')).toBe('#000');
+  expect(contrastColor('#2b8684')).toBe('#fff');
+  expect(contrastColor('#2b8786')).toBe('#fff');
+  expect(contrastColor('#2c8786')).toBe('#000');
+  expect(contrastColor('#3bb6b3')).toBe('#000');
+  expect(contrastColor('#7c7268')).toBe('#fff');
+  expect(contrastColor('#7e716c')).toBe('#fff');
+  expect(contrastColor('#81706d')).toBe('#fff');
+  expect(contrastColor('#807070')).toBe('#fff');
+  expect(contrastColor('#84b6eb')).toBe('#000');
 });

From 8498e67309478bd0a65a7b1c6f3c8e6ce62c0956 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sun, 7 Apr 2024 18:46:59 +0200
Subject: [PATCH 642/679] Some NuGet package enhancements (#30280)

Fixes #30265

1. Read second type of dependencies
2. Render `Description` and `ReleaseNotes`

old:

![grafik](https://github.com/go-gitea/gitea/assets/1666336/abac057c-11cd-4d25-b196-01ff899d948e)

new:

![grafik](https://github.com/go-gitea/gitea/assets/1666336/35302273-740c-481a-a031-1f80d2d7d336)

The NuGet spec does not specify what kind of text can be stored in the
description but we can best guess markdown. The official NuGet registry
just [converts the newlines to html
lines](https://www.nuget.org/packages/rb.Firefox#readme-body-tab).

3. Extract and render the readme. This is the new and better place to
store larger text than in the description. The content is markdown.

![grafik](https://github.com/go-gitea/gitea/assets/1666336/f442264e-3735-4b55-92c4-3b89a8ebafb0)

---------

Co-authored-by: Benjamin Heemann <benjamin.heemann@raith.de>
---
 modules/packages/nuget/metadata.go      | 34 +++++++++++++-
 modules/packages/nuget/metadata_test.go | 62 +++++++++++++++----------
 templates/package/content/nuget.tmpl    |  9 ++--
 3 files changed, 73 insertions(+), 32 deletions(-)

diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index 3c478b1c02..6769c514cc 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -58,6 +58,7 @@ type Package struct {
 type Metadata struct {
 	Description              string                  `json:"description,omitempty"`
 	ReleaseNotes             string                  `json:"release_notes,omitempty"`
+	Readme                   string                  `json:"readme,omitempty"`
 	Authors                  string                  `json:"authors,omitempty"`
 	ProjectURL               string                  `json:"project_url,omitempty"`
 	RepositoryURL            string                  `json:"repository_url,omitempty"`
@@ -71,6 +72,7 @@ type Dependency struct {
 	Version string `json:"version"`
 }
 
+// https://learn.microsoft.com/en-us/nuget/reference/nuspec
 type nuspecPackage struct {
 	Metadata struct {
 		ID                       string `xml:"id"`
@@ -80,6 +82,7 @@ type nuspecPackage struct {
 		ProjectURL               string `xml:"projectUrl"`
 		Description              string `xml:"description"`
 		ReleaseNotes             string `xml:"releaseNotes"`
+		Readme                   string `xml:"readme"`
 		PackageTypes             struct {
 			PackageType []struct {
 				Name string `xml:"name,attr"`
@@ -89,6 +92,11 @@ type nuspecPackage struct {
 			URL string `xml:"url,attr"`
 		} `xml:"repository"`
 		Dependencies struct {
+			Dependency []struct {
+				ID      string `xml:"id,attr"`
+				Version string `xml:"version,attr"`
+				Exclude string `xml:"exclude,attr"`
+			} `xml:"dependency"`
 			Group []struct {
 				TargetFramework string `xml:"targetFramework,attr"`
 				Dependency      []struct {
@@ -122,14 +130,14 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
 			}
 			defer f.Close()
 
-			return ParseNuspecMetaData(f)
+			return ParseNuspecMetaData(archive, f)
 		}
 	}
 	return nil, ErrMissingNuspecFile
 }
 
 // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
-func ParseNuspecMetaData(r io.Reader) (*Package, error) {
+func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
 	var p nuspecPackage
 	if err := xml.NewDecoder(r).Decode(&p); err != nil {
 		return nil, err
@@ -166,6 +174,28 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
 		Dependencies:             make(map[string][]Dependency),
 	}
 
+	if p.Metadata.Readme != "" {
+		f, err := archive.Open(p.Metadata.Readme)
+		if err == nil {
+			buf, _ := io.ReadAll(f)
+			m.Readme = string(buf)
+			_ = f.Close()
+		}
+	}
+
+	if len(p.Metadata.Dependencies.Dependency) > 0 {
+		deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
+		for _, dep := range p.Metadata.Dependencies.Dependency {
+			if dep.ID == "" || dep.Version == "" {
+				continue
+			}
+			deps = append(deps, Dependency{
+				ID:      dep.ID,
+				Version: dep.Version,
+			})
+		}
+		m.Dependencies[""] = deps
+	}
 	for _, group := range p.Metadata.Dependencies.Group {
 		deps := make([]Dependency, 0, len(group.Dependency))
 		for _, dep := range group.Dependency {
diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go
index bba2bff4a5..f466492f8a 100644
--- a/modules/packages/nuget/metadata_test.go
+++ b/modules/packages/nuget/metadata_test.go
@@ -6,7 +6,6 @@ package nuget
 import (
 	"archive/zip"
 	"bytes"
-	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -19,6 +18,7 @@ const (
 	projectURL        = "https://gitea.io"
 	description       = "Package Description"
 	releaseNotes      = "Package Release Notes"
+	readme            = "Readme"
 	repositoryURL     = "https://gitea.io/gitea/gitea"
 	targetFramework   = ".NETStandard2.1"
 	dependencyID      = "System.Text.Json"
@@ -36,6 +36,7 @@ const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
     <description>` + description + `</description>
     <releaseNotes>` + releaseNotes + `</releaseNotes>
     <repository url="` + repositoryURL + `" />
+    <readme>README.md</readme>
     <dependencies>
       <group targetFramework="` + targetFramework + `">
         <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
@@ -60,17 +61,19 @@ const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
 </package>`
 
 func TestParsePackageMetaData(t *testing.T) {
-	createArchive := func(name, content string) []byte {
+	createArchive := func(files map[string]string) []byte {
 		var buf bytes.Buffer
 		archive := zip.NewWriter(&buf)
-		w, _ := archive.Create(name)
-		w.Write([]byte(content))
+		for name, content := range files {
+			w, _ := archive.Create(name)
+			w.Write([]byte(content))
+		}
 		archive.Close()
 		return buf.Bytes()
 	}
 
 	t.Run("MissingNuspecFile", func(t *testing.T) {
-		data := createArchive("dummy.txt", "")
+		data := createArchive(map[string]string{"dummy.txt": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -78,7 +81,7 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
-		data := createArchive("sub/package.nuspec", "")
+		data := createArchive(map[string]string{"sub/package.nuspec": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -86,7 +89,7 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidNuspecFile", func(t *testing.T) {
-		data := createArchive("package.nuspec", "")
+		data := createArchive(map[string]string{"package.nuspec": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -94,10 +97,10 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidPackageId", func(t *testing.T) {
-		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
+		data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
 		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 		  <metadata></metadata>
-		</package>`)
+		</package>`})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -105,30 +108,34 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidPackageVersion", func(t *testing.T) {
-		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
+		data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
 		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 		  <metadata>
-			<id>`+id+`</id>
+			<id>` + id + `</id>
 		  </metadata>
-		</package>`)
+		</package>`})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
 		assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
 	})
 
-	t.Run("Valid", func(t *testing.T) {
-		data := createArchive("package.nuspec", nuspecContent)
+	t.Run("MissingReadme", func(t *testing.T) {
+		data := createArchive(map[string]string{"package.nuspec": nuspecContent})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
+		assert.Empty(t, np.Metadata.Readme)
 	})
-}
 
-func TestParseNuspecMetaData(t *testing.T) {
 	t.Run("Dependency Package", func(t *testing.T) {
-		np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent))
+		data := createArchive(map[string]string{
+			"package.nuspec": nuspecContent,
+			"README.md":      readme,
+		})
+
+		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
 		assert.Equal(t, DependencyPackage, np.PackageType)
@@ -139,6 +146,7 @@ func TestParseNuspecMetaData(t *testing.T) {
 		assert.Equal(t, projectURL, np.Metadata.ProjectURL)
 		assert.Equal(t, description, np.Metadata.Description)
 		assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
+		assert.Equal(t, readme, np.Metadata.Readme)
 		assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
 		assert.Len(t, np.Metadata.Dependencies, 1)
 		assert.Contains(t, np.Metadata.Dependencies, targetFramework)
@@ -148,13 +156,15 @@ func TestParseNuspecMetaData(t *testing.T) {
 		assert.Equal(t, dependencyVersion, deps[0].Version)
 
 		t.Run("NormalizedVersion", func(t *testing.T) {
-			np, err := ParseNuspecMetaData(strings.NewReader(`<?xml version="1.0" encoding="utf-8"?>
-<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
-  <metadata>
-	<id>test</id>
-	<version>1.04.5.2.5-rc.1+metadata</version>
-  </metadata>
-</package>`))
+			data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
+				<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+				  <metadata>
+					<id>test</id>
+					<version>1.04.5.2.5-rc.1+metadata</version>
+				  </metadata>
+				</package>`})
+
+			np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 			assert.NoError(t, err)
 			assert.NotNil(t, np)
 			assert.Equal(t, "1.4.5.2-rc.1", np.Version)
@@ -162,7 +172,9 @@ func TestParseNuspecMetaData(t *testing.T) {
 	})
 
 	t.Run("Symbols Package", func(t *testing.T) {
-		np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent))
+		data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
+
+		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
 		assert.Equal(t, SymbolsPackage, np.PackageType)
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl
index 0911260fba..f1fe420c0b 100644
--- a/templates/package/content/nuget.tmpl
+++ b/templates/package/content/nuget.tmpl
@@ -16,12 +16,11 @@
 		</div>
 	</div>
 
-	{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes}}
+	{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}}
 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
-		<div class="ui attached segment">
-			{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
-			{{if .PackageDescriptor.Metadata.ReleaseNotes}}{{.PackageDescriptor.Metadata.ReleaseNotes}}{{end}}
-		</div>
+		{{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Description}}</div>{{end}}
+		{{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment markup markdown">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}</div>{{end}}
+		{{if .PackageDescriptor.Metadata.ReleaseNotes}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.ReleaseNotes}}</div>{{end}}
 	{{end}}
 
 	{{if .PackageDescriptor.Metadata.Dependencies}}

From 0c7b0c5acaae911d3d3fefa1d8b394594c860620 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 8 Apr 2024 00:25:35 +0000
Subject: [PATCH 643/679] [skip ci] Updated licenses and gitignores

---
 options/license/APL-1.0      | 44 ++++++++++++++++++------------------
 options/license/IBM-pibs     |  4 ++--
 options/license/NCGL-UK-2.0  |  6 ++---
 options/license/NPL-1.1      |  8 +++----
 options/license/OCCT-PL      |  8 +++----
 options/license/OGL-UK-1.0   | 22 +++++++++---------
 options/license/OSET-PL-2.1  |  3 ++-
 options/license/SHL-2.0      | 18 +++++++--------
 options/license/SHL-2.1      |  2 +-
 options/license/SISSL        | 10 ++++----
 options/license/W3C-19980720 |  2 +-
 11 files changed, 64 insertions(+), 63 deletions(-)

diff --git a/options/license/APL-1.0 b/options/license/APL-1.0
index 261f2d687c..0748f90cd9 100644
--- a/options/license/APL-1.0
+++ b/options/license/APL-1.0
@@ -210,21 +210,21 @@ PART 1: INITIAL CONTRIBUTOR AND DESIGNATED WEB SITE
 
 The Initial Contributor is:
 ____________________________________________________
- 
+ 
 [Enter full name of Initial Contributor]
 
 Address of Initial Contributor:
 ________________________________________________
- 
+ 
 ________________________________________________
- 
+ 
 ________________________________________________
- 
+ 
 [Enter address above]
 
 The Designated Web Site is:
 __________________________________________________
- 
+ 
 [Enter URL for Designated Web Site of Initial Contributor]
 
 NOTE: The Initial Contributor is to complete this Part 1, along with Parts 2, 3, and 5, and, if applicable, Parts 4 and 6.
@@ -237,27 +237,27 @@ The date on which the Initial Work was first available under this License: _____
 
 PART 3: GOVERNING JURISDICTION
 
-For the purposes of this License, the Governing Jurisdiction is _________________________________________________. 
[Initial Contributor to Enter Governing Jurisdiction here]
+For the purposes of this License, the Governing Jurisdiction is _________________________________________________. [Initial Contributor to Enter Governing Jurisdiction here]
 
 PART 4: THIRD PARTIES
 
 For the purposes of this License, "Third Party" has the definition set forth below in the ONE paragraph selected by the Initial Contributor from paragraphs A, B, C, D and E when the Initial Work is distributed or otherwise made available by the Initial Contributor. To select one of the following paragraphs, the Initial Contributor must place an "X" or "x" in the selection box alongside the one respective paragraph selected.
 SELECTION
- 
+ 
 BOX   PARAGRAPH
-[  ]  A. "THIRD PARTY" means any third party.
- 
- 
-[  ]  B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
- 
- 
-[  ]  C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
- 
- 
-[  ]  D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
- 
- 
-[  ]  E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
+[  ]  A. "THIRD PARTY" means any third party.
+ 
+ 
+[  ]  B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
+ 
+ 
+[  ]  C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
+ 
+ 
+[  ]  D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
+ 
+ 
+[  ]  E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
 The default definition of "THIRD PARTY" is the definition set forth in paragraph A, if NONE OR MORE THAN ONE of paragraphs A, B, C, D or E in this Part 4 are selected by the Initial Contributor.
 
 PART 5: NOTICE
@@ -271,8 +271,8 @@ PART 6: PATENT LICENSING TERMS
 For the purposes of this License, paragraphs A, B, C, D and E of this Part 6 of Exhibit A are only incorporated and form part of the terms of the License if the Initial Contributor places an "X" or "x" in the selection box alongside the YES answer to the question immediately below.
 
 Is this a Patents-Included License pursuant to Section 2.2 of the License?
-YES   [      ]
-NO    [      ]
+YES   [      ]
+NO    [      ]
 
 By default, if YES is not selected by the Initial Contributor, the answer is NO.
 
diff --git a/options/license/IBM-pibs b/options/license/IBM-pibs
index 49454b8b1e..ee9c7be36d 100644
--- a/options/license/IBM-pibs
+++ b/options/license/IBM-pibs
@@ -4,5 +4,5 @@ Any user of this software should understand that IBM cannot provide technical su
 
 Any person who transfers this source code or any derivative work must include the IBM copyright notice, this paragraph, and the preceding two paragraphs in the transferred software.
 
-COPYRIGHT   I B M   CORPORATION 2002
-LICENSED MATERIAL  -  PROGRAM PROPERTY OF I B M
+COPYRIGHT   I B M   CORPORATION 2002
+LICENSED MATERIAL  -  PROGRAM PROPERTY OF I B M
diff --git a/options/license/NCGL-UK-2.0 b/options/license/NCGL-UK-2.0
index 31fbad6f83..15c4f63c22 100644
--- a/options/license/NCGL-UK-2.0
+++ b/options/license/NCGL-UK-2.0
@@ -12,15 +12,15 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
 This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
 
 You are free to:
-		copy, publish, distribute and transmit the Information;
+		copy, publish, distribute and transmit the Information;
 		adapt the Information;
 		exploit the Information for Non-Commercial purposes for example, by combining it with other information in your own product or application.
 
 You are not permitted to:
-		exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
+		exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
 
 You must, where you do any of the above:
-		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
+		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
 
 If the Information Provider does not provide a specific attribution statement, you must use the following:
    Contains information licensed under the Non-Commercial Government Licence v2.0.
diff --git a/options/license/NPL-1.1 b/options/license/NPL-1.1
index 62c5296400..0d5457ff04 100644
--- a/options/license/NPL-1.1
+++ b/options/license/NPL-1.1
@@ -2,7 +2,7 @@ Netscape Public LIcense version 1.1
 
 AMENDMENTS
 
-The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License.  Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
+The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License.  Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
 
 Additional Terms applicable to the Netscape Public License.
 
@@ -28,7 +28,7 @@ Additional Terms applicable to the Netscape Public License.
      Notwithstanding the limitations of Section 11 above, the provisions regarding litigation in Section 11(a), (b) and (c) of the License shall apply to all disputes relating to this License.
 
 	 EXHIBIT A-Netscape Public License.
-	 
+	 
 "The contents of this file are subject to the Netscape Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/NPL/
 
 Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
@@ -37,8 +37,8 @@ The Original Code is Mozilla Communicator client code, released March 31, 1998.
 
 The Initial Developer of the Original Code is Netscape Communications Corporation. Portions created by Netscape are Copyright (C) 1998-1999 Netscape Communications Corporation. All Rights Reserved.
 Contributor(s): ______________________________________.
-	 
-Alternatively, the contents of this file may be used under the terms of the _____ license (the  "[___] License"), in which case the provisions of [______] License are applicable  instead of those above.  If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting  the provisions above and replace  them with the notice and other provisions required by the [___] License.  If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
+	 
+Alternatively, the contents of this file may be used under the terms of the _____ license (the  "[___] License"), in which case the provisions of [______] License are applicable  instead of those above.  If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting  the provisions above and replace  them with the notice and other provisions required by the [___] License.  If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
 
 
 Mozilla Public License Version 1.1
diff --git a/options/license/OCCT-PL b/options/license/OCCT-PL
index 85df3c73c5..9b6fccc1c9 100644
--- a/options/license/OCCT-PL
+++ b/options/license/OCCT-PL
@@ -6,7 +6,7 @@ OPEN CASCADE releases and makes publicly available the source code of the softwa
 It is not the purpose of this license to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this license has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
 
 Please read this license carefully and completely before downloading this software. By downloading, using, modifying, distributing and sublicensing this software, you indicate your acceptance to be bound by the terms and conditions of this license. If you do not want to accept or cannot accept for any reasons the terms and conditions of this license, please do not download or use in any manner this software.
- 
+ 
 1. Definitions
 
 Unless there is something in the subject matter or in the context inconsistent therewith, the capitalized terms used in this License shall have the following meaning.
@@ -26,13 +26,13 @@ Unless there is something in the subject matter or in the context inconsistent t
 "Software": means the Original Code, the Modifications, the combination of Original Code and any Modifications or any respective portions thereof.
 
 "You" or "Your": means an individual or a legal entity exercising rights under this License
- 
+ 
 2. Acceptance of license
 By using, reproducing, modifying, distributing or sublicensing the Software or any portion thereof, You expressly indicate Your acceptance of the terms and conditions of this License and undertake to act in accordance with all the provisions of this License applicable to You.
- 
+ 
 3. Scope and purpose
 This License applies to the Software and You may not use, reproduce, modify, distribute, sublicense or circulate the Software, or any portion thereof, except as expressly provided under this License. Any attempt to otherwise use, reproduce, modify, distribute or sublicense the Software is void and will automatically terminate Your rights under this License.
- 
+ 
 4. Contributor license
 Subject to the terms and conditions of this License, the Initial Developer and each of the Contributors hereby grant You a world-wide, royalty-free, irrevocable and non-exclusive license under the Applicable Intellectual Property Rights they own or control, to use, reproduce, modify, distribute and sublicense the Software provided that:
 
diff --git a/options/license/OGL-UK-1.0 b/options/license/OGL-UK-1.0
index a761c9916f..867c0e353b 100644
--- a/options/license/OGL-UK-1.0
+++ b/options/license/OGL-UK-1.0
@@ -10,20 +10,20 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
 This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
 
 You are free to:
-		copy, publish, distribute and transmit the Information;
+		copy, publish, distribute and transmit the Information;
 		adapt the Information;
 		exploit the Information commercially for example, by combining it with other Information, or by including it in your own product or application.
 
 You must, where you do any of the above:
-		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
-		 If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following:
 Contains public sector information licensed under the Open Government Licence v1.0.
+		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
+		 If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following: Contains public sector information licensed under the Open Government Licence v1.0.
 		ensure that you do not use the Information in a way that suggests any official status or that the Information Provider endorses you or your use of the Information;
 		ensure that you do not mislead others or misrepresent the Information or its source;
 		ensure that your use of the Information does not breach the Data Protection Act 1998 or the Privacy and Electronic Communications (EC Directive) Regulations 2003.
 
 These are important conditions of this licence and if you fail to comply with them the rights granted to you under this licence, or any similar licence granted by the Licensor, will end automatically.
 
- Exemptions
+ Exemptions
 
 This licence does not cover the use of:
 	- personal data in the Information;
@@ -48,22 +48,22 @@ Definitions
 
 In this licence, the terms below have the following meanings:
 
-‘Information’
means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
+‘Information’ means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
 
-‘Information Provider’
means the person or organisation providing the Information under this licence.
+‘Information Provider’ means the person or organisation providing the Information under this licence.
 
-‘Licensor’
means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
+‘Licensor’ means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
 
-‘Use’
as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
+‘Use’ as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
 
-‘You’
means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
+‘You’ means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
 
 About the Open Government Licence
 The Controller of Her Majesty’s Stationery Office (HMSO) has developed this licence as a tool to enable Information Providers in the public sector to license the use and re-use of their Information under a common open licence. The Controller invites public sector bodies owning their own copyright and database rights to permit the use of their Information under this licence.
 
-The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
+The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
 
 This is version 1.0 of the Open Government Licence. The Controller of HMSO may, from time to time, issue new versions of the Open Government Licence. However, you may continue to use Information licensed under this version should you wish to do so.
 These terms have been aligned to be interoperable with any Creative Commons Attribution Licence, which covers copyright, and Open Data Commons Attribution License, which covers database rights and applicable copyrights.
 
-Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
+Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
diff --git a/options/license/OSET-PL-2.1 b/options/license/OSET-PL-2.1
index 15f0c7758c..e0ed2e1398 100644
--- a/options/license/OSET-PL-2.1
+++ b/options/license/OSET-PL-2.1
@@ -100,7 +100,8 @@ If it is impossible for You to comply with any of the terms of this License with
      5.1 Failure to Comply
 	 The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60-days after You have come back into compliance.  Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30-days after Your receipt of the notice.
 
-     5.2 Patent Infringement Claims
     If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
+     5.2 Patent Infringement Claims
+	 If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
 
      5.3 Additional Compliance Terms
 	 Notwithstanding the foregoing in this Section 5, for purposes of this Section, if You breach Section 3.1 (Distribution of Source Form), Section 3.2 (Distribution of Executable Form), Section 3.3 (Distribution of a Larger Work), or Section 3.4 (Notices), then becoming compliant as described in Section 5.1 must also include, no later than 30 days after receipt by You of notice of such violation by a Contributor, making the Covered Software available in Source Code Form as required by this License on a publicly available computer network for a period of no less than three (3) years.
diff --git a/options/license/SHL-2.0 b/options/license/SHL-2.0
index e522a396fe..9218b47a72 100644
--- a/options/license/SHL-2.0
+++ b/options/license/SHL-2.0
@@ -1,22 +1,22 @@
 # Solderpad Hardware Licence Version 2.0
 
-This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
+This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
 
 1. The definitions set out in the Apache License are modified as follows:
 
-Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
+Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
 
-Contribution also includes any design, as well as any work of authorship.
+Contribution also includes any design, as well as any work of authorship.
 
-Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
+Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
 
-Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
+Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
 
-Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
+Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
 
-Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
-Work also includes a design or work of authorship, whether in Source form or other Object form.
+Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
+Work also includes a design or work of authorship, whether in Source form or other Object form.
 
 2. Grant of Licence
 
-2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
+2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
diff --git a/options/license/SHL-2.1 b/options/license/SHL-2.1
index 4815a9e5ed..c9ae53741f 100644
--- a/options/license/SHL-2.1
+++ b/options/license/SHL-2.1
@@ -19,7 +19,7 @@ The following definitions shall replace the corresponding definitions in the Apa
 "License" shall mean this Solderpad Hardware License version 2.1, being the terms and conditions for use, manufacture, instantiation, adaptation, reproduction, and distribution as defined by Sections 1 through 9 of this document.
 
 "Licensor" shall mean the Rights owner or entity authorized by the Rights owner that is granting the License.
- 
+ 
 "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship or design. For the purposes of this License, Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the Work and Derivative Works thereof.
 
 "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object or material and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
diff --git a/options/license/SISSL b/options/license/SISSL
index 7d6ad9d66c..af38d02d92 100644
--- a/options/license/SISSL
+++ b/options/license/SISSL
@@ -36,13 +36,13 @@ Sun Industry Standards Source License - Version 1.1
 
 2.0 SOURCE CODE LICENSE
 
-     2.1 The Initial Developer Grant  The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: 
+     2.1 The Initial Developer Grant  The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: 
 
           (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, and/or as part of a Larger Work; and
 
           (b) under Patents Claims infringed by the making, using or selling of Original Code, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Code (or portions thereof).
           (c) the licenses granted in this Section 2.1(a) and (b) are effective on the date Initial Developer first distributes Original Code under the terms of this License.
-          (d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications. 
+          (d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications. 
 
 3.0 DISTRIBUTION OBLIGATIONS
 
@@ -92,14 +92,14 @@ This License represents the complete agreement concerning subject matter hereof.
 
 EXHIBIT A - Sun Standards License
 
-"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
+"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
 
-Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
+Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
 express or implied. See the License for the specific language governing rights and limitations under the License.
 
 The Original Code is ______________________________________.
 
-The Initial Developer of the Original Code is: 
+The Initial Developer of the Original Code is: 
 Sun Microsystems, Inc..
 
 Portions created by: _______________________________________
diff --git a/options/license/W3C-19980720 b/options/license/W3C-19980720
index a8554039ef..134879044d 100644
--- a/options/license/W3C-19980720
+++ b/options/license/W3C-19980720
@@ -4,7 +4,7 @@ Copyright (c) 1994-2002 World Wide Web Consortium, (Massachusetts Institute of T
 
 This W3C work (including software, documents, or other related items) is being provided by the copyright holders under the following license. By obtaining, using and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:
 
-Permission to use, copy, modify, and distribute this software and its documentation, with or without modification,  for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
+Permission to use, copy, modify, and distribute this software and its documentation, with or without modification,  for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
 
      1.	The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
 

From 074a3e05f665ad8c635a314f49080f8846e6d315 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 8 Apr 2024 12:13:34 +0800
Subject: [PATCH 644/679] Fix oauth2 builtin application logic (#30304)

Fix #29074 (allow to disable all builtin apps) and don't make the doctor
command remove the builtin apps.

By the way, rename refobject and joincond to camel case.
---
 models/db/consistency.go              | 12 +++----
 modules/setting/oauth2.go             |  4 +++
 modules/setting/oauth2_test.go        | 18 ++++++++++
 services/doctor/dbconsistency.go      | 25 +++++++------
 services/doctor/dbconsistency_test.go | 51 +++++++++++++++++++++++++++
 services/doctor/main_test.go          | 14 ++++++++
 6 files changed, 107 insertions(+), 17 deletions(-)
 create mode 100644 services/doctor/dbconsistency_test.go
 create mode 100644 services/doctor/main_test.go

diff --git a/models/db/consistency.go b/models/db/consistency.go
index d19732cf80..d0b0ab8315 100644
--- a/models/db/consistency.go
+++ b/models/db/consistency.go
@@ -10,21 +10,21 @@ import (
 )
 
 // CountOrphanedObjects count subjects with have no existing refobject anymore
-func CountOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) (int64, error) {
+func CountOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) (int64, error) {
 	return GetEngine(ctx).
 		Table("`"+subject+"`").
-		Join("LEFT", "`"+refobject+"`", joinCond).
-		Where(builder.IsNull{"`" + refobject + "`.id"}).
+		Join("LEFT", "`"+refObject+"`", joinCond).
+		Where(builder.IsNull{"`" + refObject + "`.id"}).
 		Select("COUNT(`" + subject + "`.`id`)").
 		Count()
 }
 
 // DeleteOrphanedObjects delete subjects with have no existing refobject anymore
-func DeleteOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) error {
+func DeleteOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) error {
 	subQuery := builder.Select("`"+subject+"`.id").
 		From("`"+subject+"`").
-		Join("LEFT", "`"+refobject+"`", joinCond).
-		Where(builder.IsNull{"`" + refobject + "`.id"})
+		Join("LEFT", "`"+refObject+"`", joinCond).
+		Where(builder.IsNull{"`" + refObject + "`.id"})
 	b := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`")
 	_, err := GetEngine(ctx).Exec(b)
 	return err
diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index 1429a7585c..830472db32 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -118,6 +118,10 @@ func loadOAuth2From(rootCfg ConfigProvider) {
 		return
 	}
 
+	if sec.HasKey("DEFAULT_APPLICATIONS") && sec.Key("DEFAULT_APPLICATIONS").String() == "" {
+		OAuth2.DefaultApplications = nil
+	}
+
 	// Handle the rename of ENABLE to ENABLED
 	deprecatedSetting(rootCfg, "oauth2", "ENABLE", "oauth2", "ENABLED", "v1.23.0")
 	if sec.HasKey("ENABLE") && !sec.HasKey("ENABLED") {
diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go
index d822198619..4403f35892 100644
--- a/modules/setting/oauth2_test.go
+++ b/modules/setting/oauth2_test.go
@@ -32,3 +32,21 @@ JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
 	assert.Len(t, actual, 32)
 	assert.EqualValues(t, expected, actual)
 }
+
+func TestOauth2DefaultApplications(t *testing.T) {
+	cfg, _ := NewConfigProviderFromData(``)
+	loadOAuth2From(cfg)
+	assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea"}, OAuth2.DefaultApplications)
+
+	cfg, _ = NewConfigProviderFromData(`[oauth2]
+DEFAULT_APPLICATIONS = tea
+`)
+	loadOAuth2From(cfg)
+	assert.Equal(t, []string{"tea"}, OAuth2.DefaultApplications)
+
+	cfg, _ = NewConfigProviderFromData(`[oauth2]
+DEFAULT_APPLICATIONS =
+`)
+	loadOAuth2From(cfg)
+	assert.Nil(t, nil, OAuth2.DefaultApplications)
+}
diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go
index e2dcb63f33..dfdf7b547a 100644
--- a/services/doctor/dbconsistency.go
+++ b/services/doctor/dbconsistency.go
@@ -61,26 +61,20 @@ func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int6
 	}
 }
 
-func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCheck {
+func genericOrphanCheck(name, subject, refObject, joinCond string) consistencyCheck {
 	return consistencyCheck{
 		Name: name,
 		Counter: func(ctx context.Context) (int64, error) {
-			return db.CountOrphanedObjects(ctx, subject, refobject, joincond)
+			return db.CountOrphanedObjects(ctx, subject, refObject, joinCond)
 		},
 		Fixer: func(ctx context.Context) (int64, error) {
-			err := db.DeleteOrphanedObjects(ctx, subject, refobject, joincond)
+			err := db.DeleteOrphanedObjects(ctx, subject, refObject, joinCond)
 			return -1, err
 		},
 	}
 }
 
-func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
-	// make sure DB version is uptodate
-	if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
-		logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
-		return err
-	}
-
+func prepareDBConsistencyChecks() []consistencyCheck {
 	consistencyChecks := []consistencyCheck{
 		{
 			// find labels without existing repo or org
@@ -210,7 +204,7 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 			"oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"),
 		// find OAuth2Application without existing user
 		genericOrphanCheck("Orphaned OAuth2Application without existing User",
-			"oauth2_application", "user", "oauth2_application.uid=`user`.id"),
+			"oauth2_application", "user", "oauth2_application.uid=0 OR oauth2_application.uid=`user`.id"),
 		// find OAuth2AuthorizationCode without existing OAuth2Grant
 		genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant",
 			"oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"),
@@ -224,7 +218,16 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 		genericOrphanCheck("Orphaned Redirects without existing redirect user",
 			"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
 	)
+	return consistencyChecks
+}
 
+func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
+	// make sure DB version is uptodate
+	if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
+		logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
+		return err
+	}
+	consistencyChecks := prepareDBConsistencyChecks()
 	for _, c := range consistencyChecks {
 		if err := c.Run(ctx, logger, autofix); err != nil {
 			return err
diff --git a/services/doctor/dbconsistency_test.go b/services/doctor/dbconsistency_test.go
new file mode 100644
index 0000000000..4e4ac535b7
--- /dev/null
+++ b/services/doctor/dbconsistency_test.go
@@ -0,0 +1,51 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+	"slices"
+	"testing"
+
+	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConsistencyCheck(t *testing.T) {
+	checks := prepareDBConsistencyChecks()
+	idx := slices.IndexFunc(checks, func(check consistencyCheck) bool {
+		return check.Name == "Orphaned OAuth2Application without existing User"
+	})
+	if !assert.NotEqual(t, -1, idx) {
+		return
+	}
+
+	_ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &user.User{})
+	_ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &auth.OAuth2Application{})
+
+	err := db.Insert(db.DefaultContext, &user.User{ID: 1})
+	assert.NoError(t, err)
+	err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-1", ClientID: "client-id-1"})
+	assert.NoError(t, err)
+	err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-2", ClientID: "client-id-2", UID: 1})
+	assert.NoError(t, err)
+	err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-3", ClientID: "client-id-3", UID: 99999999})
+	assert.NoError(t, err)
+
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-1"})
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-2"})
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-3"})
+
+	oauth2AppCheck := checks[idx]
+	err = oauth2AppCheck.Run(db.DefaultContext, log.GetManager().GetLogger(log.DEFAULT), true)
+	assert.NoError(t, err)
+
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-1"})
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-2"})
+	unittest.AssertNotExistsBean(t, &auth.OAuth2Application{ClientID: "client-id-3"})
+}
diff --git a/services/doctor/main_test.go b/services/doctor/main_test.go
new file mode 100644
index 0000000000..0f365e21d0
--- /dev/null
+++ b/services/doctor/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m)
+}

From 7d66b9ea65cc416046ec7075bc327932a4f2094f Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 8 Apr 2024 20:43:23 +0900
Subject: [PATCH 645/679] Avoid showing `Failed to change the default wiki
 branch` if repo has no wiki when saving repo settings (#30329)

---
 routers/web/repo/wiki_test.go | 6 ++++++
 services/wiki/wiki.go         | 4 ++++
 2 files changed, 10 insertions(+)

diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 8b5207f9d9..2894c06fbd 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -226,6 +226,12 @@ func TestWikiRaw(t *testing.T) {
 func TestDefaultWikiBranch(t *testing.T) {
 	unittest.PrepareTestEnv(t)
 
+	// repo with no wiki
+	repoWithNoWiki := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+	assert.False(t, repoWithNoWiki.HasWiki())
+	assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
+
+	// repo with wiki
 	assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
 
 	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index f8387416c1..fdcc5feefa 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -370,6 +370,10 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
 			return fmt.Errorf("unable to update database: %w", err)
 		}
 
+		if !repo.HasWiki() {
+			return nil
+		}
+
 		oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo)
 		if err != nil {
 			return fmt.Errorf("unable to get default branch: %w", err)

From d872ce006c0400edb10a05f7555f9b08070442e3 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 8 Apr 2024 23:08:26 +0900
Subject: [PATCH 646/679] Avoid running action when action unit is disabled
 after workflows detected (#30331)

Fix #30243

We only checking unit disabled when detecting workflows, but not in
runner `FetchTask`.
So if a workflow was detected when action unit is enabled, but disabled
later, `FetchTask` will still return these detected actions.

Global setting: repo.ENABLED and repository.`DISABLED_REPO_UNITS` will
not effect this.
---
 models/actions/task.go | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/models/actions/task.go b/models/actions/task.go
index 96a6d2e80c..1e279659c7 100644
--- a/models/actions/task.go
+++ b/models/actions/task.go
@@ -11,6 +11,7 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -227,7 +228,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
 	if runner.RepoID != 0 {
 		jobCond = builder.Eq{"repo_id": runner.RepoID}
 	} else if runner.OwnerID != 0 {
-		jobCond = builder.In("repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": runner.OwnerID}))
+		jobCond = builder.In("repo_id", builder.Select("id").From("repository").
+			Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id").
+			Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions}))
 	}
 	if jobCond.IsValid() {
 		jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond))

From ff7aab44032cbb22cb6696a1939d1f619621f067 Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Mon, 8 Apr 2024 22:59:09 +0200
Subject: [PATCH 647/679] Add optional doctor storage init (#30330)

Add optional storage init to doctor
---
 services/doctor/doctor.go | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/services/doctor/doctor.go b/services/doctor/doctor.go
index 559f8e06da..a4eb5e16b9 100644
--- a/services/doctor/doctor.go
+++ b/services/doctor/doctor.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/storage"
 )
 
 // Check represents a Doctor check
@@ -25,6 +26,7 @@ type Check struct {
 	AbortIfFailed              bool
 	SkipDatabaseInitialization bool
 	Priority                   int
+	InitStorage                bool
 }
 
 func initDBSkipLogger(ctx context.Context) error {
@@ -84,6 +86,7 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
 	logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
 	loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize})
 	dbIsInit := false
+	storageIsInit := false
 	for i, check := range checks {
 		if !dbIsInit && !check.SkipDatabaseInitialization {
 			// Only open database after the most basic configuration check
@@ -94,6 +97,14 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
 			}
 			dbIsInit = true
 		}
+		if !storageIsInit && check.InitStorage {
+			if err := storage.Init(); err != nil {
+				logger.Error("Error whilst initializing the storage: %v", err)
+				logger.Error("Check if you are using the right config file. You can use a --config directive to specify one.")
+				return nil
+			}
+			storageIsInit = true
+		}
 		logger.Info("\n[%d] %s", i+1, check.Title)
 		if err := check.Run(ctx, loggerStep, autofix); err != nil {
 			if check.AbortIfFailed {

From 908426aa0fcc58961c345994f0f66056f6cf5f48 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 9 Apr 2024 05:26:41 +0800
Subject: [PATCH 648/679] Fix missed doer (#30231)

Fix #29879

Co-authored-by: Giteabot <teabot@gitea.io>
---
 routers/api/v1/repo/issue.go              | 10 ++++----
 routers/api/v1/repo/issue_attachment.go   |  2 +-
 routers/api/v1/repo/issue_dependency.go   | 12 +++++-----
 routers/api/v1/repo/issue_pin.go          |  2 +-
 routers/api/v1/repo/issue_tracked_time.go | 10 ++++----
 routers/web/repo/issue.go                 |  6 ++---
 services/actions/notifier.go              | 10 ++++----
 services/convert/issue.go                 | 28 +++++++++++------------
 services/convert/issue_comment.go         |  6 ++---
 services/convert/pull.go                  |  2 +-
 services/webhook/notifier.go              | 22 +++++++++---------
 11 files changed, 55 insertions(+), 55 deletions(-)

diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 6934b34b24..5e173abf88 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -311,7 +311,7 @@ func SearchIssues(ctx *context.APIContext) {
 
 	ctx.SetLinkHeader(int(total), limit)
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 // ListIssues list the issues of a repository
@@ -548,7 +548,7 @@ func ListIssues(ctx *context.APIContext) {
 
 	ctx.SetLinkHeader(int(total), listOptions.PageSize)
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
@@ -614,7 +614,7 @@ func GetIssue(ctx *context.APIContext) {
 		ctx.NotFound()
 		return
 	}
-	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
 }
 
 // CreateIssue create an issue of a repository
@@ -737,7 +737,7 @@ func CreateIssue(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
 		return
 	}
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
 }
 
 // EditIssue modify an issue of a repository
@@ -911,7 +911,7 @@ func EditIssue(ctx *context.APIContext) {
 		ctx.InternalServerError(err)
 		return
 	}
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
 }
 
 func DeleteIssue(ctx *context.APIContext) {
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
index d62e23aa02..7a5c6d554d 100644
--- a/routers/api/v1/repo/issue_attachment.go
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -107,7 +107,7 @@ func ListIssueAttachments(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue).Attachments)
+	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue).Attachments)
 }
 
 // CreateIssueAttachment creates an attachment and saves the given file
diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go
index a42920d4fd..c40e92c01b 100644
--- a/routers/api/v1/repo/issue_dependency.go
+++ b/routers/api/v1/repo/issue_dependency.go
@@ -153,7 +153,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
 		blockerIssues = append(blockerIssues, &blocker.Issue)
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, blockerIssues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
 }
 
 // CreateIssueDependency create a new issue dependencies
@@ -214,7 +214,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
 }
 
 // RemoveIssueDependency remove an issue dependency
@@ -275,7 +275,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
 }
 
 // GetIssueBlocks list issues that are blocked by this issue
@@ -381,7 +381,7 @@ func GetIssueBlocks(ctx *context.APIContext) {
 		issues = append(issues, &depMeta.Issue)
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 // CreateIssueBlocking block the issue given in the body by the issue in path
@@ -438,7 +438,7 @@ func CreateIssueBlocking(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
 }
 
 // RemoveIssueBlocking unblock the issue given in the body by the issue in path
@@ -495,7 +495,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
 }
 
 func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go
index 8fcf670fd0..af3e06332a 100644
--- a/routers/api/v1/repo/issue_pin.go
+++ b/routers/api/v1/repo/issue_pin.go
@@ -207,7 +207,7 @@ func ListPinnedIssues(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 // ListPinnedPullRequests returns a list of all pinned PRs
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index c640515881..f83855efac 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -138,7 +138,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
 	}
 
 	ctx.SetTotalCountHeader(count)
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
 
 // AddTime add time manual to the given issue
@@ -225,7 +225,7 @@ func AddTime(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 		return
 	}
-	ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, trackedTime))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, user, trackedTime))
 }
 
 // ResetIssueTime reset time manual to the given issue
@@ -455,7 +455,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 		return
 	}
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
 
 // ListTrackedTimesByRepository lists all tracked times of the repository
@@ -567,7 +567,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
 	}
 
 	ctx.SetTotalCountHeader(count)
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
 
 // ListMyTrackedTimes lists all tracked times of the current user
@@ -629,5 +629,5 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
 	}
 
 	ctx.SetTotalCountHeader(count)
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 6c2d4a7390..e4f2e9a2bc 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -2179,7 +2179,7 @@ func GetIssueInfo(ctx *context.Context) {
 		}
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToIssue(ctx, issue))
+	ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue))
 }
 
 // UpdateIssueTitle change issue's title
@@ -2709,7 +2709,7 @@ func SearchIssues(ctx *context.Context) {
 	}
 
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
 }
 
 func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
@@ -2879,7 +2879,7 @@ func ListIssues(ctx *context.Context) {
 	}
 
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
 }
 
 func BatchDeleteIssues(ctx *context.Context) {
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index eec5f814da..6551da39e7 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -49,7 +49,7 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
 	newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).WithPayload(&api.IssuePayload{
 		Action:     api.HookIssueOpened,
 		Index:      issue.Index,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, issue.Poster, issue),
 		Repository: convert.ToRepo(ctx, issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, issue.Poster, nil),
 	}).Notify(withMethod(ctx, "NewIssue"))
@@ -89,7 +89,7 @@ func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_mod
 		WithPayload(&api.IssuePayload{
 			Action:     api.HookIssueEdited,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		}).
@@ -127,7 +127,7 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
 	}
 	apiIssue := &api.IssuePayload{
 		Index:      issue.Index,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, doer, issue),
 		Repository: convert.ToRepo(ctx, issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, doer, nil),
 	}
@@ -229,7 +229,7 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
 		WithPayload(&api.IssuePayload{
 			Action:     action,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		}).
@@ -293,7 +293,7 @@ func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, commen
 
 	payload := &api.IssueCommentPayload{
 		Action:     action,
-		Issue:      convert.ToAPIIssue(ctx, comment.Issue),
+		Issue:      convert.ToAPIIssue(ctx, doer, comment.Issue),
 		Comment:    convert.ToAPIComment(ctx, comment.Issue.Repo, comment),
 		Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, doer, nil),
diff --git a/services/convert/issue.go b/services/convert/issue.go
index c6e06180c8..54b00cd88e 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -18,19 +18,19 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 )
 
-func ToIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
-	return toIssue(ctx, issue, WebAssetDownloadURL)
+func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+	return toIssue(ctx, doer, issue, WebAssetDownloadURL)
 }
 
 // ToAPIIssue converts an Issue to API format
 // it assumes some fields assigned with values:
 // Required - Poster, Labels,
 // Optional - Milestone, Assignee, PullRequest
-func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
-	return toIssue(ctx, issue, APIAssetDownloadURL)
+func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+	return toIssue(ctx, doer, issue, APIAssetDownloadURL)
 }
 
-func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
+func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
 	if err := issue.LoadLabels(ctx); err != nil {
 		return &api.Issue{}
 	}
@@ -44,7 +44,7 @@ func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func
 	apiIssue := &api.Issue{
 		ID:          issue.ID,
 		Index:       issue.Index,
-		Poster:      ToUser(ctx, issue.Poster, nil),
+		Poster:      ToUser(ctx, issue.Poster, doer),
 		Title:       issue.Title,
 		Body:        issue.Content,
 		Attachments: toAttachments(issue.Repo, issue.Attachments, getDownloadURL),
@@ -114,25 +114,25 @@ func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func
 }
 
 // ToIssueList converts an IssueList to API format
-func ToIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+func ToIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
 	result := make([]*api.Issue, len(il))
 	for i := range il {
-		result[i] = ToIssue(ctx, il[i])
+		result[i] = ToIssue(ctx, doer, il[i])
 	}
 	return result
 }
 
 // ToAPIIssueList converts an IssueList to API format
-func ToAPIIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+func ToAPIIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
 	result := make([]*api.Issue, len(il))
 	for i := range il {
-		result[i] = ToAPIIssue(ctx, il[i])
+		result[i] = ToAPIIssue(ctx, doer, il[i])
 	}
 	return result
 }
 
 // ToTrackedTime converts TrackedTime to API format
-func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
+func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
 	apiT = &api.TrackedTime{
 		ID:      t.ID,
 		IssueID: t.IssueID,
@@ -141,7 +141,7 @@ func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.
 		Created: t.Created,
 	}
 	if t.Issue != nil {
-		apiT.Issue = ToAPIIssue(ctx, t.Issue)
+		apiT.Issue = ToAPIIssue(ctx, doer, t.Issue)
 	}
 	if t.User != nil {
 		apiT.UserName = t.User.Name
@@ -192,10 +192,10 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
 }
 
 // ToTrackedTimeList converts TrackedTimeList to API format
-func ToTrackedTimeList(ctx context.Context, tl issues_model.TrackedTimeList) api.TrackedTimeList {
+func ToTrackedTimeList(ctx context.Context, doer *user_model.User, tl issues_model.TrackedTimeList) api.TrackedTimeList {
 	result := make([]*api.TrackedTime, 0, len(tl))
 	for _, t := range tl {
-		result = append(result, ToTrackedTime(ctx, t))
+		result = append(result, ToTrackedTime(ctx, doer, t))
 	}
 	return result
 }
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
index b034a50897..9ffaf1e84c 100644
--- a/services/convert/issue_comment.go
+++ b/services/convert/issue_comment.go
@@ -120,7 +120,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 			return nil
 		}
 
-		comment.TrackedTime = ToTrackedTime(ctx, c.Time)
+		comment.TrackedTime = ToTrackedTime(ctx, doer, c.Time)
 	}
 
 	if c.RefIssueID != 0 {
@@ -129,7 +129,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 			log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
 			return nil
 		}
-		comment.RefIssue = ToAPIIssue(ctx, issue)
+		comment.RefIssue = ToAPIIssue(ctx, doer, issue)
 	}
 
 	if c.RefCommentID != 0 {
@@ -180,7 +180,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 	}
 
 	if c.DependentIssue != nil {
-		comment.DependentIssue = ToAPIIssue(ctx, c.DependentIssue)
+		comment.DependentIssue = ToAPIIssue(ctx, doer, c.DependentIssue)
 	}
 
 	return comment
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 6d98121ed5..775bf3806d 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -33,7 +33,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 		return nil
 	}
 
-	apiIssue := ToAPIIssue(ctx, pr.Issue)
+	apiIssue := ToAPIIssue(ctx, doer, pr.Issue)
 	if err := pr.LoadBaseRepo(ctx); err != nil {
 		log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
 		return nil
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 1ab14fd6a7..587caf62ff 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -67,7 +67,7 @@ func (m *webhookNotifier) IssueClearLabels(ctx context.Context, doer *user_model
 		err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
 			Action:     api.HookIssueLabelCleared,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -168,7 +168,7 @@ func (m *webhookNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo
 		permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
 		apiIssue := &api.IssuePayload{
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		}
@@ -214,7 +214,7 @@ func (m *webhookNotifier) IssueChangeTitle(ctx context.Context, doer *user_model
 					From: oldTitle,
 				},
 			},
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -250,7 +250,7 @@ func (m *webhookNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
 	} else {
 		apiIssue := &api.IssuePayload{
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 			CommitID:   commitID,
@@ -281,7 +281,7 @@ func (m *webhookNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
 	if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{
 		Action:     api.HookIssueOpened,
 		Index:      issue.Index,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, issue.Poster, issue),
 		Repository: convert.ToRepo(ctx, issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, issue.Poster, nil),
 	}); err != nil {
@@ -349,7 +349,7 @@ func (m *webhookNotifier) IssueChangeContent(ctx context.Context, doer *user_mod
 					From: oldContent,
 				},
 			},
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -384,7 +384,7 @@ func (m *webhookNotifier) UpdateComment(ctx context.Context, doer *user_model.Us
 	permission, _ := access_model.GetUserRepoPermission(ctx, c.Issue.Repo, doer)
 	if err := PrepareWebhooks(ctx, EventSource{Repository: c.Issue.Repo}, eventType, &api.IssueCommentPayload{
 		Action:  api.HookIssueCommentEdited,
-		Issue:   convert.ToAPIIssue(ctx, c.Issue),
+		Issue:   convert.ToAPIIssue(ctx, doer, c.Issue),
 		Comment: convert.ToAPIComment(ctx, c.Issue.Repo, c),
 		Changes: &api.ChangesPayload{
 			Body: &api.ChangesFromPayload{
@@ -412,7 +412,7 @@ func (m *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
 	permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer)
 	if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, eventType, &api.IssueCommentPayload{
 		Action:     api.HookIssueCommentCreated,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, doer, issue),
 		Comment:    convert.ToAPIComment(ctx, repo, comment),
 		Repository: convert.ToRepo(ctx, repo, permission),
 		Sender:     convert.ToUser(ctx, doer, nil),
@@ -449,7 +449,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
 	permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer)
 	if err := PrepareWebhooks(ctx, EventSource{Repository: comment.Issue.Repo}, eventType, &api.IssueCommentPayload{
 		Action:     api.HookIssueCommentDeleted,
-		Issue:      convert.ToAPIIssue(ctx, comment.Issue),
+		Issue:      convert.ToAPIIssue(ctx, doer, comment.Issue),
 		Comment:    convert.ToAPIComment(ctx, comment.Issue.Repo, comment),
 		Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, doer, nil),
@@ -533,7 +533,7 @@ func (m *webhookNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
 		err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
 			Action:     api.HookIssueLabelUpdated,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -575,7 +575,7 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
 		err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueMilestone, &api.IssuePayload{
 			Action:     hookAction,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})

From d7013c26c8eadf1679efe14ea338a4f1b2295b07 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 9 Apr 2024 00:24:26 +0000
Subject: [PATCH 649/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_id-ID.ini | 41 +++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index ad7e0f4062..96248cbc1d 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -1331,12 +1331,53 @@ runners.task_list.repository=Repositori
 runners.task_list.commit=Memperbuat
 
 runs.commit=Memperbuat
+runs.no_matching_online_runner_helper=Tidak ada runner online yang cocok dengan label: %s
+runs.actor=Aktor
+runs.status=Status
+runs.actors_no_select=Semua aktor
+runs.status_no_select=Semua status
+runs.no_results=Tidak ada hasil yang cocok.
+runs.no_workflows=Belum ada alur kerja.
+runs.no_workflows.quick_start=Tidak tahu cara memulai dengan Gitea Actions? Lihat <a target="_blank" rel="noopener noreferrer" href="%s">panduan cepat</a>.
+runs.no_workflows.documentation=Untuk informasi lebih lanjut tentang Gitea Actions, lihat <a target="_blank" rel="noopener noreferrer" href="%s">dokumentasi</a>.
+runs.no_runs=Alur kerja belum berjalan.
+runs.empty_commit_message=(pesan commit kosong)
 
+workflow.disable=Nonaktifkan Alur Kerja
+workflow.disable_success=Alur kerja '%s' berhasil dinonaktifkan.
+workflow.enable=Aktifkan Alur Kerja
+workflow.enable_success=Alur kerja '%s' berhasil diaktifkan.
+workflow.disabled=Alur kerja dinonaktifkan.
 
+need_approval_desc=Butuh persetujuan untuk menjalankan alur kerja untuk pull request fork.
 
+variables=Variabel
+variables.management=Managemen Variabel
+variables.creation=Tambah Variabel
+variables.none=Belum ada variabel.
+variables.deletion=Hapus variabel
+variables.deletion.description=Menghapus variabel bersifat permanen dan tidak dapat dibatalkan. Lanjutkan?
+variables.description=Variabel akan diteruskan ke beberapa tindakan dan tidak dapat dibaca sebaliknya.
+variables.id_not_exist=Variabel dengan ID %d tidak ada.
+variables.edit=Edit Variabel
+variables.deletion.failed=Gagal menghapus variabel.
+variables.deletion.success=Variabel telah dihapus.
+variables.creation.failed=Gagal menambahkan variabel.
+variables.creation.success=Variabel "%s" telah ditambahkan.
+variables.update.failed=Gagal mengedit variabel.
+variables.update.success=Variabel telah diedit.
 
 [projects]
+type-1.display_name=Proyek Individu
+type-2.display_name=Proyek Repositori
+type-3.display_name=Proyek Organisasi
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Directory
+normal_file=Normal file
+executable_file=Executable file
+symbolic_link=Symbolic link
+submodule=Submodule
 

From 72dc75e594fb5227abfa1cb74cb652cc33bacc93 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 9 Apr 2024 05:09:43 +0200
Subject: [PATCH 650/679] Reduce checkbox size to 15px (#30346)

16 seems to big, 14 too small. Let's do 15. Alignment:

<img width="181" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/f2988611-dee2-492e-a18f-dc5ab3a1cd6c">
---
 web_src/css/base.css             | 2 +-
 web_src/css/modules/checkbox.css | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 44dc83e6f3..02bc1b7220 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -23,7 +23,7 @@
   --height-loading: 16rem;
   --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
   --tab-size: 4;
-  --checkbox-size: 16px; /* height and width of checkbox and radio inputs */
+  --checkbox-size: 15px; /* height and width of checkbox and radio inputs */
   --page-spacing: 16px; /* space between page elements */
 }
 
diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css
index 9238e0b3f3..d3e45714a4 100644
--- a/web_src/css/modules/checkbox.css
+++ b/web_src/css/modules/checkbox.css
@@ -20,7 +20,7 @@ input[type="radio"] {
 .ui.checkbox input[type="checkbox"],
 .ui.checkbox input[type="radio"] {
   position: absolute;
-  top: 0;
+  top: 1px;
   left: 0;
   width: var(--checkbox-size);
   height: var(--checkbox-size);

From 263a716cb52037f3e7e51f014f6c8cdfad6ae03d Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 9 Apr 2024 11:43:17 +0800
Subject: [PATCH 651/679] Performance optimization for git push (#30104)

Agit returned result should be from `ProcReceive` hook but not
`PostReceive` hook. Then for all non-agit pull requests, it will not
check the pull requests for every pushing `refs/pull/%d/head`.
---
 cmd/hook.go                          | 37 +++++++-----
 modules/private/hook.go              | 25 ++++----
 routers/private/hook_post_receive.go | 85 ++++++++++++++--------------
 services/agit/agit.go                | 26 ++++++---
 tests/integration/git_push_test.go   | 11 ++++
 5 files changed, 110 insertions(+), 74 deletions(-)

diff --git a/cmd/hook.go b/cmd/hook.go
index 6a3358853d..c04591d79e 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -448,23 +448,26 @@ Gitea or set your environment appropriately.`, "")
 
 func hookPrintResults(results []private.HookPostReceiveBranchResult) {
 	for _, res := range results {
-		if !res.Message {
-			continue
-		}
-
-		fmt.Fprintln(os.Stderr, "")
-		if res.Create {
-			fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch)
-			fmt.Fprintf(os.Stderr, "  %s\n", res.URL)
-		} else {
-			fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
-			fmt.Fprintf(os.Stderr, "  %s\n", res.URL)
-		}
-		fmt.Fprintln(os.Stderr, "")
-		os.Stderr.Sync()
+		hookPrintResult(res.Message, res.Create, res.Branch, res.URL)
 	}
 }
 
+func hookPrintResult(output, isCreate bool, branch, url string) {
+	if !output {
+		return
+	}
+	fmt.Fprintln(os.Stderr, "")
+	if isCreate {
+		fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch)
+		fmt.Fprintf(os.Stderr, "  %s\n", url)
+	} else {
+		fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
+		fmt.Fprintf(os.Stderr, "  %s\n", url)
+	}
+	fmt.Fprintln(os.Stderr, "")
+	os.Stderr.Sync()
+}
+
 func pushOptions() map[string]string {
 	opts := make(map[string]string)
 	if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
@@ -691,6 +694,12 @@ Gitea or set your environment appropriately.`, "")
 	}
 	err = writeFlushPktLine(ctx, os.Stdout)
 
+	if err == nil {
+		for _, res := range resp.Results {
+			hookPrintResult(res.ShouldShowMessage, res.IsCreatePR, res.HeadBranch, res.URL)
+		}
+	}
+
 	return err
 }
 
diff --git a/modules/private/hook.go b/modules/private/hook.go
index cab8c81224..79c3d48229 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 )
 
@@ -32,13 +33,13 @@ const (
 )
 
 // Bool checks for a key in the map and parses as a boolean
-func (g GitPushOptions) Bool(key string, def bool) bool {
+func (g GitPushOptions) Bool(key string) optional.Option[bool] {
 	if val, ok := g[key]; ok {
 		if b, err := strconv.ParseBool(val); err == nil {
-			return b
+			return optional.Some(b)
 		}
 	}
-	return def
+	return optional.None[bool]()
 }
 
 // HookOptions represents the options for the Hook calls
@@ -87,13 +88,17 @@ type HookProcReceiveResult struct {
 
 // HookProcReceiveRefResult represents an individual result from ProcReceive
 type HookProcReceiveRefResult struct {
-	OldOID       string
-	NewOID       string
-	Ref          string
-	OriginalRef  git.RefName
-	IsForcePush  bool
-	IsNotMatched bool
-	Err          string
+	OldOID            string
+	NewOID            string
+	Ref               string
+	OriginalRef       git.RefName
+	IsForcePush       bool
+	IsNotMatched      bool
+	Err               string
+	IsCreatePR        bool
+	URL               string
+	ShouldShowMessage bool
+	HeadBranch        string
 }
 
 // HookPreReceive check whether the provided commits are allowed
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 101ae92302..769a68970d 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -6,11 +6,12 @@ package private
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
@@ -159,8 +160,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 		}
 	}
 
+	isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
+	isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
 	// Handle Push Options
-	if len(opts.GitPushOptions) > 0 {
+	if isPrivate.Has() || isTemplate.Has() {
 		// load the repository
 		if repo == nil {
 			repo = loadRepository(ctx, ownerName, repoName)
@@ -171,13 +174,49 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 			wasEmpty = repo.IsEmpty
 		}
 
-		repo.IsPrivate = opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate, repo.IsPrivate)
-		repo.IsTemplate = opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate, repo.IsTemplate)
-		if err := repo_model.UpdateRepositoryCols(ctx, repo, "is_private", "is_template"); err != nil {
+		pusher, err := user_model.GetUserByID(ctx, opts.UserID)
+		if err != nil {
 			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
 			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
 				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
 			})
+			return
+		}
+		perm, err := access_model.GetUserRepoPermission(ctx, repo, pusher)
+		if err != nil {
+			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+			})
+			return
+		}
+		if !perm.IsOwner() && !perm.IsAdmin() {
+			ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{
+				Err: "Permissions denied",
+			})
+			return
+		}
+
+		cols := make([]string, 0, len(opts.GitPushOptions))
+
+		if isPrivate.Has() {
+			repo.IsPrivate = isPrivate.Value()
+			cols = append(cols, "is_private")
+		}
+
+		if isTemplate.Has() {
+			repo.IsTemplate = isTemplate.Value()
+			cols = append(cols, "is_template")
+		}
+
+		if len(cols) > 0 {
+			if err := repo_model.UpdateRepositoryCols(ctx, repo, cols...); err != nil {
+				log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+					Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+				})
+				return
+			}
 		}
 	}
 
@@ -192,42 +231,6 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 		refFullName := opts.RefFullNames[i]
 		newCommitID := opts.NewCommitIDs[i]
 
-		// post update for agit pull request
-		// FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR
-		if git.DefaultFeatures.SupportProcReceive && refFullName.IsPull() {
-			if repo == nil {
-				repo = loadRepository(ctx, ownerName, repoName)
-				if ctx.Written() {
-					return
-				}
-			}
-
-			pullIndex, _ := strconv.ParseInt(refFullName.PullName(), 10, 64)
-			if pullIndex <= 0 {
-				continue
-			}
-
-			pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex)
-			if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
-				log.Error("Failed to get PR by index %v Error: %v", pullIndex, err)
-				ctx.JSON(http.StatusInternalServerError, private.Response{
-					Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err),
-				})
-				return
-			}
-			if pr == nil {
-				continue
-			}
-
-			results = append(results, private.HookPostReceiveBranchResult{
-				Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
-				Create:  false,
-				Branch:  "",
-				URL:     fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
-			})
-			continue
-		}
-
 		// If we've pushed a branch (and not deleted it)
 		if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
 			// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
diff --git a/services/agit/agit.go b/services/agit/agit.go
index eb3bafa906..52a70469e0 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
+	"code.gitea.io/gitea/modules/setting"
 	notify_service "code.gitea.io/gitea/services/notify"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
@@ -145,10 +146,14 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 			log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
 
 			results = append(results, private.HookProcReceiveRefResult{
-				Ref:         pr.GetGitRefName(),
-				OriginalRef: opts.RefFullNames[i],
-				OldOID:      objectFormat.EmptyObjectID().String(),
-				NewOID:      opts.NewCommitIDs[i],
+				Ref:               pr.GetGitRefName(),
+				OriginalRef:       opts.RefFullNames[i],
+				OldOID:            objectFormat.EmptyObjectID().String(),
+				NewOID:            opts.NewCommitIDs[i],
+				IsCreatePR:        true,
+				URL:               fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
+				ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
+				HeadBranch:        headBranch,
 			})
 			continue
 		}
@@ -208,11 +213,14 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		isForcePush := comment != nil && comment.IsForcePush
 
 		results = append(results, private.HookProcReceiveRefResult{
-			OldOID:      oldCommitID,
-			NewOID:      opts.NewCommitIDs[i],
-			Ref:         pr.GetGitRefName(),
-			OriginalRef: opts.RefFullNames[i],
-			IsForcePush: isForcePush,
+			OldOID:            oldCommitID,
+			NewOID:            opts.NewCommitIDs[i],
+			Ref:               pr.GetGitRefName(),
+			OriginalRef:       opts.RefFullNames[i],
+			IsForcePush:       isForcePush,
+			IsCreatePR:        false,
+			URL:               fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
+			ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
 		})
 	}
 
diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go
index 0a35724807..b37fb02444 100644
--- a/tests/integration/git_push_test.go
+++ b/tests/integration/git_push_test.go
@@ -49,6 +49,17 @@ func testGitPush(t *testing.T, u *url.URL) {
 		})
 	})
 
+	t.Run("Push branch with options", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			branchName := "branch-with-options"
+			doGitCreateBranch(gitPath, branchName)(t)
+			doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t)
+			pushed = append(pushed, branchName)
+
+			return pushed, deleted
+		})
+	})
+
 	t.Run("Delete branches", func(t *testing.T) {
 		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
 			doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete

From 8d14266269f1b4fd5e13d701830919c1a1613444 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 9 Apr 2024 08:30:21 +0200
Subject: [PATCH 652/679] Fix label-list rendering in timeline, decrease gap
 (#30342)

Not sure exactly when this regressed, but has been a while I think.

Before:

<img width="895" alt="Screenshot 2024-04-08 at 22 46 50"
src="https://github.com/go-gitea/gitea/assets/115237/9b1788f8-017e-4fe1-8ab9-938e0d76fb41">

After:

<img width="689" alt="Screenshot 2024-04-08 at 23 00 58"
src="https://github.com/go-gitea/gitea/assets/115237/90193df9-5c24-4a1a-96fe-3d4e8392063c">

Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/css/repo.css | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index c50d13a174..8b91b599e7 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2274,9 +2274,9 @@
 }
 
 .labels-list {
-  display: flex;
+  display: inline-flex;
   flex-wrap: wrap;
-  gap: 0.25em;
+  gap: 2.5px;
 }
 
 .labels-list a {

From d547b53cca8a9a7ac96449910bae5d811728c251 Mon Sep 17 00:00:00 2001
From: oliverpool <3864879+oliverpool@users.noreply.github.com>
Date: Tue, 9 Apr 2024 14:27:30 +0200
Subject: [PATCH 653/679] Add container.FilterSlice function (#30339)

Many places have the following logic:
```go
func (jobs ActionJobList) GetRunIDs() []int64 {
	ids := make(container.Set[int64], len(jobs))
	for _, j := range jobs {
		if j.RunID == 0 {
			continue
		}
		ids.Add(j.RunID)
	}
	return ids.Values()
}
```

this introduces a `container.FilterMapUnique` function, which reduces
the code above to:
```go
func (jobs ActionJobList) GetRunIDs() []int64 {
	return container.FilterMapUnique(jobs, func(j *ActionRunJob) (int64, bool) {
		return j.RunID, j.RunID != 0
	})
}
```
---
 models/actions/run_job_list.go       | 11 +---
 models/actions/run_list.go           | 16 ++---
 models/actions/runner_list.go        | 11 +---
 models/actions/schedule_list.go      | 16 ++---
 models/actions/schedule_spec_list.go | 16 ++---
 models/actions/task_list.go          | 11 +---
 models/activities/action_list.go     | 30 ++++-----
 models/git/branch_list.go            | 26 ++++----
 models/issues/comment.go             |  9 ++-
 models/issues/comment_list.go        | 95 ++++++++--------------------
 models/issues/issue_list.go          | 16 ++---
 models/issues/reaction.go            | 10 ++-
 models/issues/review_list.go         |  9 ++-
 models/repo/repo_list.go             |  9 +--
 modules/container/filter.go          | 21 ++++++
 modules/container/filter_test.go     | 28 ++++++++
 16 files changed, 150 insertions(+), 184 deletions(-)
 create mode 100644 modules/container/filter.go
 create mode 100644 modules/container/filter_test.go

diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go
index 6ea6cb9d3b..6c5d3b3252 100644
--- a/models/actions/run_job_list.go
+++ b/models/actions/run_job_list.go
@@ -16,14 +16,9 @@ import (
 type ActionJobList []*ActionRunJob
 
 func (jobs ActionJobList) GetRunIDs() []int64 {
-	ids := make(container.Set[int64], len(jobs))
-	for _, j := range jobs {
-		if j.RunID == 0 {
-			continue
-		}
-		ids.Add(j.RunID)
-	}
-	return ids.Values()
+	return container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) {
+		return j.RunID, j.RunID != 0
+	})
 }
 
 func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error {
diff --git a/models/actions/run_list.go b/models/actions/run_list.go
index 388bfc4f86..4046c7d369 100644
--- a/models/actions/run_list.go
+++ b/models/actions/run_list.go
@@ -19,19 +19,15 @@ type RunList []*ActionRun
 
 // GetUserIDs returns a slice of user's id
 func (runs RunList) GetUserIDs() []int64 {
-	ids := make(container.Set[int64], len(runs))
-	for _, run := range runs {
-		ids.Add(run.TriggerUserID)
-	}
-	return ids.Values()
+	return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
+		return run.TriggerUserID, true
+	})
 }
 
 func (runs RunList) GetRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(runs))
-	for _, run := range runs {
-		ids.Add(run.RepoID)
-	}
-	return ids.Values()
+	return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
+		return run.RepoID, true
+	})
 }
 
 func (runs RunList) LoadTriggerUser(ctx context.Context) error {
diff --git a/models/actions/runner_list.go b/models/actions/runner_list.go
index 87f0886b47..5ce69e07ac 100644
--- a/models/actions/runner_list.go
+++ b/models/actions/runner_list.go
@@ -16,14 +16,9 @@ type RunnerList []*ActionRunner
 
 // GetUserIDs returns a slice of user's id
 func (runners RunnerList) GetUserIDs() []int64 {
-	ids := make(container.Set[int64], len(runners))
-	for _, runner := range runners {
-		if runner.OwnerID == 0 {
-			continue
-		}
-		ids.Add(runner.OwnerID)
-	}
-	return ids.Values()
+	return container.FilterSlice(runners, func(runner *ActionRunner) (int64, bool) {
+		return runner.OwnerID, runner.OwnerID != 0
+	})
 }
 
 func (runners RunnerList) LoadOwners(ctx context.Context) error {
diff --git a/models/actions/schedule_list.go b/models/actions/schedule_list.go
index b806550b87..1d35adc420 100644
--- a/models/actions/schedule_list.go
+++ b/models/actions/schedule_list.go
@@ -18,19 +18,15 @@ type ScheduleList []*ActionSchedule
 
 // GetUserIDs returns a slice of user's id
 func (schedules ScheduleList) GetUserIDs() []int64 {
-	ids := make(container.Set[int64], len(schedules))
-	for _, schedule := range schedules {
-		ids.Add(schedule.TriggerUserID)
-	}
-	return ids.Values()
+	return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
+		return schedule.TriggerUserID, true
+	})
 }
 
 func (schedules ScheduleList) GetRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(schedules))
-	for _, schedule := range schedules {
-		ids.Add(schedule.RepoID)
-	}
-	return ids.Values()
+	return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
+		return schedule.RepoID, true
+	})
 }
 
 func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
diff --git a/models/actions/schedule_spec_list.go b/models/actions/schedule_spec_list.go
index e9ae268a6e..f7dac72f8b 100644
--- a/models/actions/schedule_spec_list.go
+++ b/models/actions/schedule_spec_list.go
@@ -16,11 +16,9 @@ import (
 type SpecList []*ActionScheduleSpec
 
 func (specs SpecList) GetScheduleIDs() []int64 {
-	ids := make(container.Set[int64], len(specs))
-	for _, spec := range specs {
-		ids.Add(spec.ScheduleID)
-	}
-	return ids.Values()
+	return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) {
+		return spec.ScheduleID, true
+	})
 }
 
 func (specs SpecList) LoadSchedules(ctx context.Context) error {
@@ -46,11 +44,9 @@ func (specs SpecList) LoadSchedules(ctx context.Context) error {
 }
 
 func (specs SpecList) GetRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(specs))
-	for _, spec := range specs {
-		ids.Add(spec.RepoID)
-	}
-	return ids.Values()
+	return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) {
+		return spec.RepoID, true
+	})
 }
 
 func (specs SpecList) LoadRepos(ctx context.Context) error {
diff --git a/models/actions/task_list.go b/models/actions/task_list.go
index b07d00b8db..5e17f91441 100644
--- a/models/actions/task_list.go
+++ b/models/actions/task_list.go
@@ -16,14 +16,9 @@ import (
 type TaskList []*ActionTask
 
 func (tasks TaskList) GetJobIDs() []int64 {
-	ids := make(container.Set[int64], len(tasks))
-	for _, t := range tasks {
-		if t.JobID == 0 {
-			continue
-		}
-		ids.Add(t.JobID)
-	}
-	return ids.Values()
+	return container.FilterSlice(tasks, func(t *ActionTask) (int64, bool) {
+		return t.JobID, t.JobID != 0
+	})
 }
 
 func (tasks TaskList) LoadJobs(ctx context.Context) error {
diff --git a/models/activities/action_list.go b/models/activities/action_list.go
index fdf0f35d4f..6e23b173b5 100644
--- a/models/activities/action_list.go
+++ b/models/activities/action_list.go
@@ -22,11 +22,9 @@ import (
 type ActionList []*Action
 
 func (actions ActionList) getUserIDs() []int64 {
-	userIDs := make(container.Set[int64], len(actions))
-	for _, action := range actions {
-		userIDs.Add(action.ActUserID)
-	}
-	return userIDs.Values()
+	return container.FilterSlice(actions, func(action *Action) (int64, bool) {
+		return action.ActUserID, true
+	})
 }
 
 func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) {
@@ -50,11 +48,9 @@ func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_mod
 }
 
 func (actions ActionList) getRepoIDs() []int64 {
-	repoIDs := make(container.Set[int64], len(actions))
-	for _, action := range actions {
-		repoIDs.Add(action.RepoID)
-	}
-	return repoIDs.Values()
+	return container.FilterSlice(actions, func(action *Action) (int64, bool) {
+		return action.RepoID, true
+	})
 }
 
 func (actions ActionList) LoadRepositories(ctx context.Context) error {
@@ -80,18 +76,16 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*
 		userMap = make(map[int64]*user_model.User)
 	}
 
-	userSet := make(container.Set[int64], len(actions))
-	for _, action := range actions {
+	missingUserIDs := container.FilterSlice(actions, func(action *Action) (int64, bool) {
 		if action.Repo == nil {
-			continue
+			return 0, false
 		}
-		if _, ok := userMap[action.Repo.OwnerID]; !ok {
-			userSet.Add(action.Repo.OwnerID)
-		}
-	}
+		_, alreadyLoaded := userMap[action.Repo.OwnerID]
+		return action.Repo.OwnerID, !alreadyLoaded
+	})
 
 	if err := db.GetEngine(ctx).
-		In("id", userSet.Values()).
+		In("id", missingUserIDs).
 		Find(&userMap); err != nil {
 		return fmt.Errorf("find user: %w", err)
 	}
diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 8319e5ecd0..980bd7b4c9 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -17,15 +17,12 @@ import (
 type BranchList []*Branch
 
 func (branches BranchList) LoadDeletedBy(ctx context.Context) error {
-	ids := container.Set[int64]{}
-	for _, branch := range branches {
-		if !branch.IsDeleted {
-			continue
-		}
-		ids.Add(branch.DeletedByID)
-	}
+	ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
+		return branch.DeletedByID, branch.IsDeleted
+	})
+
 	usersMap := make(map[int64]*user_model.User, len(ids))
-	if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil {
+	if err := db.GetEngine(ctx).In("id", ids).Find(&usersMap); err != nil {
 		return err
 	}
 	for _, branch := range branches {
@@ -41,14 +38,13 @@ func (branches BranchList) LoadDeletedBy(ctx context.Context) error {
 }
 
 func (branches BranchList) LoadPusher(ctx context.Context) error {
-	ids := container.Set[int64]{}
-	for _, branch := range branches {
-		if branch.PusherID > 0 { // pusher_id maybe zero because some branches are sync by backend with no pusher
-			ids.Add(branch.PusherID)
-		}
-	}
+	ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
+		// pusher_id maybe zero because some branches are sync by backend with no pusher
+		return branch.PusherID, branch.PusherID > 0
+	})
+
 	usersMap := make(map[int64]*user_model.User, len(ids))
-	if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil {
+	if err := db.GetEngine(ctx).In("id", ids).Find(&usersMap); err != nil {
 		return err
 	}
 	for _, branch := range branches {
diff --git a/models/issues/comment.go b/models/issues/comment.go
index 6f65a5dbbc..353163ebd6 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -1272,10 +1272,9 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
 		return nil
 	}
 
-	issueIDs := make(container.Set[int64])
-	for _, comment := range comments {
-		issueIDs.Add(comment.IssueID)
-	}
+	issueIDs := container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.IssueID, true
+	})
 
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
@@ -1298,7 +1297,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
 		}
 	}
 
-	for issueID := range issueIDs {
+	for _, issueID := range issueIDs {
 		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
 			issueID, CommentTypeComment, issueID); err != nil {
 			return err
diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index 0047b054ba..370b5396e0 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -17,13 +17,9 @@ import (
 type CommentList []*Comment
 
 func (comments CommentList) getPosterIDs() []int64 {
-	posterIDs := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.PosterID > 0 {
-			posterIDs.Add(comment.PosterID)
-		}
-	}
-	return posterIDs.Values()
+	return container.FilterSlice(comments, func(c *Comment) (int64, bool) {
+		return c.PosterID, c.PosterID > 0
+	})
 }
 
 // LoadPosters loads posters
@@ -44,13 +40,9 @@ func (comments CommentList) LoadPosters(ctx context.Context) error {
 }
 
 func (comments CommentList) getLabelIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.LabelID > 0 {
-			ids.Add(comment.LabelID)
-		}
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.LabelID, comment.LabelID > 0
+	})
 }
 
 func (comments CommentList) loadLabels(ctx context.Context) error {
@@ -94,13 +86,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error {
 }
 
 func (comments CommentList) getMilestoneIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.MilestoneID > 0 {
-			ids.Add(comment.MilestoneID)
-		}
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.MilestoneID, comment.MilestoneID > 0
+	})
 }
 
 func (comments CommentList) loadMilestones(ctx context.Context) error {
@@ -137,13 +125,9 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
 }
 
 func (comments CommentList) getOldMilestoneIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.OldMilestoneID > 0 {
-			ids.Add(comment.OldMilestoneID)
-		}
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.OldMilestoneID, comment.OldMilestoneID > 0
+	})
 }
 
 func (comments CommentList) loadOldMilestones(ctx context.Context) error {
@@ -180,13 +164,9 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
 }
 
 func (comments CommentList) getAssigneeIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.AssigneeID > 0 {
-			ids.Add(comment.AssigneeID)
-		}
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.AssigneeID, comment.AssigneeID > 0
+	})
 }
 
 func (comments CommentList) loadAssignees(ctx context.Context) error {
@@ -237,14 +217,9 @@ func (comments CommentList) loadAssignees(ctx context.Context) error {
 
 // getIssueIDs returns all the issue ids on this comment list which issue hasn't been loaded
 func (comments CommentList) getIssueIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.Issue != nil {
-			continue
-		}
-		ids.Add(comment.IssueID)
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.IssueID, comment.Issue == nil
+	})
 }
 
 // Issues returns all the issues of comments
@@ -311,16 +286,12 @@ func (comments CommentList) LoadIssues(ctx context.Context) error {
 }
 
 func (comments CommentList) getDependentIssueIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
 		if comment.DependentIssue != nil {
-			continue
+			return 0, false
 		}
-		if comment.DependentIssueID > 0 {
-			ids.Add(comment.DependentIssueID)
-		}
-	}
-	return ids.Values()
+		return comment.DependentIssueID, comment.DependentIssueID > 0
+	})
 }
 
 func (comments CommentList) loadDependentIssues(ctx context.Context) error {
@@ -375,15 +346,9 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error {
 
 // getAttachmentCommentIDs only return the comment ids which possibly has attachments
 func (comments CommentList) getAttachmentCommentIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.Type == CommentTypeComment ||
-			comment.Type == CommentTypeReview ||
-			comment.Type == CommentTypeCode {
-			ids.Add(comment.ID)
-		}
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.ID, comment.Type.HasAttachmentSupport()
+	})
 }
 
 // LoadAttachmentsByIssue loads attachments by issue id
@@ -451,13 +416,9 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
 }
 
 func (comments CommentList) getReviewIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.ReviewID > 0 {
-			ids.Add(comment.ReviewID)
-		}
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.ReviewID, comment.ReviewID > 0
+	})
 }
 
 func (comments CommentList) loadReviews(ctx context.Context) error {
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index 218891ad35..1b05f0aa35 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -74,11 +74,9 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
 }
 
 func (issues IssueList) getPosterIDs() []int64 {
-	posterIDs := make(container.Set[int64], len(issues))
-	for _, issue := range issues {
-		posterIDs.Add(issue.PosterID)
-	}
-	return posterIDs.Values()
+	return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
+		return issue.PosterID, true
+	})
 }
 
 func (issues IssueList) loadPosters(ctx context.Context) error {
@@ -193,11 +191,9 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
 }
 
 func (issues IssueList) getMilestoneIDs() []int64 {
-	ids := make(container.Set[int64], len(issues))
-	for _, issue := range issues {
-		ids.Add(issue.MilestoneID)
-	}
-	return ids.Values()
+	return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
+		return issue.MilestoneID, true
+	})
 }
 
 func (issues IssueList) loadMilestones(ctx context.Context) error {
diff --git a/models/issues/reaction.go b/models/issues/reaction.go
index d5448636fe..eb7faefc79 100644
--- a/models/issues/reaction.go
+++ b/models/issues/reaction.go
@@ -305,14 +305,12 @@ func (list ReactionList) GroupByType() map[string]ReactionList {
 }
 
 func (list ReactionList) getUserIDs() []int64 {
-	userIDs := make(container.Set[int64], len(list))
-	for _, reaction := range list {
+	return container.FilterSlice(list, func(reaction *Reaction) (int64, bool) {
 		if reaction.OriginalAuthor != "" {
-			continue
+			return 0, false
 		}
-		userIDs.Add(reaction.UserID)
-	}
-	return userIDs.Values()
+		return reaction.UserID, true
+	})
 }
 
 func valuesUser(m map[int64]*user_model.User) []*user_model.User {
diff --git a/models/issues/review_list.go b/models/issues/review_list.go
index ec6cb07988..7b8c3d319c 100644
--- a/models/issues/review_list.go
+++ b/models/issues/review_list.go
@@ -38,12 +38,11 @@ func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
 }
 
 func (reviews ReviewList) LoadIssues(ctx context.Context) error {
-	issueIDs := container.Set[int64]{}
-	for i := 0; i < len(reviews); i++ {
-		issueIDs.Add(reviews[i].IssueID)
-	}
+	issueIDs := container.FilterSlice(reviews, func(review *Review) (int64, bool) {
+		return review.IssueID, true
+	})
 
-	issues, err := GetIssuesByIDs(ctx, issueIDs.Values())
+	issues, err := GetIssuesByIDs(ctx, issueIDs)
 	if err != nil {
 		return err
 	}
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index cb7cd47a8d..987c7df9b0 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -104,18 +104,19 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
 		return nil
 	}
 
-	set := make(container.Set[int64])
+	userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
+		return repo.OwnerID, true
+	})
 	repoIDs := make([]int64, len(repos))
 	for i := range repos {
-		set.Add(repos[i].OwnerID)
 		repoIDs[i] = repos[i].ID
 	}
 
 	// Load owners.
-	users := make(map[int64]*user_model.User, len(set))
+	users := make(map[int64]*user_model.User, len(userIDs))
 	if err := db.GetEngine(ctx).
 		Where("id > 0").
-		In("id", set.Values()).
+		In("id", userIDs).
 		Find(&users); err != nil {
 		return fmt.Errorf("find users: %w", err)
 	}
diff --git a/modules/container/filter.go b/modules/container/filter.go
new file mode 100644
index 0000000000..37ec7c3d56
--- /dev/null
+++ b/modules/container/filter.go
@@ -0,0 +1,21 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package container
+
+import "slices"
+
+// FilterSlice ranges over the slice and calls include() for each element.
+// If the second returned value is true, the first returned value will be included in the resulting
+// slice (after deduplication).
+func FilterSlice[E any, T comparable](s []E, include func(E) (T, bool)) []T {
+	filtered := make([]T, 0, len(s)) // slice will be clipped before returning
+	seen := make(map[T]bool, len(s))
+	for i := range s {
+		if v, ok := include(s[i]); ok && !seen[v] {
+			filtered = append(filtered, v)
+			seen[v] = true
+		}
+	}
+	return slices.Clip(filtered)
+}
diff --git a/modules/container/filter_test.go b/modules/container/filter_test.go
new file mode 100644
index 0000000000..ad304e5abb
--- /dev/null
+++ b/modules/container/filter_test.go
@@ -0,0 +1,28 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package container
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFilterMapUnique(t *testing.T) {
+	result := FilterSlice([]int{
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+	}, func(i int) (int, bool) {
+		switch i {
+		case 0:
+			return 0, true // included later
+		case 1:
+			return 0, true // duplicate of previous (should be ignored)
+		case 2:
+			return 2, false // not included
+		default:
+			return i, true
+		}
+	})
+	assert.Equal(t, []int{0, 3, 4, 5, 6, 7, 8, 9}, result)
+}

From 63c80aeb29efa2bc0ffa0ed46ea7cd86e634ef8a Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 10 Apr 2024 00:39:38 +0800
Subject: [PATCH 654/679] Fix actions design about default actions download url
 (#30360)

Fix #30359
---
 docs/content/usage/actions/design.en-us.md | 2 +-
 docs/content/usage/actions/design.zh-cn.md | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/content/usage/actions/design.en-us.md b/docs/content/usage/actions/design.en-us.md
index 29fa433e59..0d72c19dce 100644
--- a/docs/content/usage/actions/design.en-us.md
+++ b/docs/content/usage/actions/design.en-us.md
@@ -104,7 +104,7 @@ However, if a job container tries to fetch code from localhost, it will fail bec
 ### Connection 3, act runner to internet
 
 When you use some actions like `actions/checkout@v4`, the act runner downloads the scripts, not the job containers.
-By default, it downloads from [gitea.com](http://gitea.com/), so it requires access to the internet.
+By default, it downloads from [github.com](http://github.com/), so it requires access to the internet. If you configure the `DEFAULT_ACTIONS_URL` to `self`, then it will download from your Gitea instance by default. Then it will not connect to internet when downloading the action itself.
 It also downloads some docker images from Docker Hub by default, which also requires internet access.
 
 However, internet access is not strictly necessary.
diff --git a/docs/content/usage/actions/design.zh-cn.md b/docs/content/usage/actions/design.zh-cn.md
index 8add1cf7c5..f48576477f 100644
--- a/docs/content/usage/actions/design.zh-cn.md
+++ b/docs/content/usage/actions/design.zh-cn.md
@@ -105,7 +105,8 @@ act runner 必须能够连接到Gitea以接收任务并发送执行结果回来
 ### 连接 3,act runner到互联网
 
 当您使用诸如 `actions/checkout@v4` 的一些Actions时,act runner下载的是脚本,而不是Job容器。
-默认情况下,它从[gitea.com](http://gitea.com/)下载,因此需要访问互联网。
+默认情况下,它从[github.com](http://github.com/)下载,因此需要访问互联网。如果您设置的是 self,
+那么默认将从您的当前Gitea实例下载,那么此步骤不需要连接到互联网。
 它还默认从Docker Hub下载一些Docker镜像,这也需要互联网访问。
 
 然而,互联网访问并不是绝对必需的。

From fec754258cce7f82ce9263f2dd0fad3f0b078d8a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 10 Apr 2024 04:16:55 +0200
Subject: [PATCH 655/679] Fix floated list items (#30377)

Fixes https://github.com/go-gitea/gitea/issues/30365, regression from
https://github.com/go-gitea/gitea/pull/30281
---
 web_src/css/modules/list.css | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css
index 73760390de..32c71e802b 100644
--- a/web_src/css/modules/list.css
+++ b/web_src/css/modules/list.css
@@ -126,6 +126,12 @@
   cursor: pointer;
 }
 
+.ui.list .list > .item [class*="right floated"],
+.ui.list > .item [class*="right floated"] {
+  float: right;
+  margin: 0 0 0 1em;
+}
+
 .ui.menu .ui.list > .item,
 .ui.menu .ui.list .list > .item {
   display: list-item;

From 310e2517e5d55a037f612a8561fb1850b517b37f Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Wed, 10 Apr 2024 10:57:43 +0800
Subject: [PATCH 656/679] Fix ambiguous id when fetch Actions tasks (#30382)

Fix regression of #30331.

```txt
time="2024-04-10T02:23:49Z" level=error msg="failed to fetch task" func="[fetchTask]" file="[poller.go:91]" error="unknown: rpc error: code = Internal desc = pick task: CreateTaskForRunner: Error 1052 (23000): Column 'id' in field list is ambiguous"
```
---
 models/actions/task.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/models/actions/task.go b/models/actions/task.go
index 1e279659c7..9946cf5233 100644
--- a/models/actions/task.go
+++ b/models/actions/task.go
@@ -228,7 +228,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
 	if runner.RepoID != 0 {
 		jobCond = builder.Eq{"repo_id": runner.RepoID}
 	} else if runner.OwnerID != 0 {
-		jobCond = builder.In("repo_id", builder.Select("id").From("repository").
+		jobCond = builder.In("repo_id", builder.Select("`repository`.id").From("repository").
 			Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id").
 			Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions}))
 	}

From b09687f1d18e1489d82dd481a1cce50203f2da94 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 10 Apr 2024 12:18:41 +0800
Subject: [PATCH 657/679] Refactor more filterslice (#30370)

---
 models/actions/runner_list.go          | 13 +++----------
 models/activities/notification_list.go | 12 +++++-------
 models/issues/issue_list.go            | 11 +++++------
 3 files changed, 13 insertions(+), 23 deletions(-)

diff --git a/models/actions/runner_list.go b/models/actions/runner_list.go
index 5ce69e07ac..3ef8ebb254 100644
--- a/models/actions/runner_list.go
+++ b/models/actions/runner_list.go
@@ -36,16 +36,9 @@ func (runners RunnerList) LoadOwners(ctx context.Context) error {
 }
 
 func (runners RunnerList) getRepoIDs() []int64 {
-	repoIDs := make(container.Set[int64], len(runners))
-	for _, runner := range runners {
-		if runner.RepoID == 0 {
-			continue
-		}
-		if _, ok := repoIDs[runner.RepoID]; !ok {
-			repoIDs[runner.RepoID] = struct{}{}
-		}
-	}
-	return repoIDs.Values()
+	return container.FilterSlice(runners, func(runner *ActionRunner) (int64, bool) {
+		return runner.RepoID, runner.RepoID > 0
+	})
 }
 
 func (runners RunnerList) LoadRepos(ctx context.Context) error {
diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go
index 5858933391..0cbb91df3c 100644
--- a/models/activities/notification_list.go
+++ b/models/activities/notification_list.go
@@ -190,14 +190,12 @@ func (nl NotificationList) LoadAttributes(ctx context.Context) error {
 }
 
 func (nl NotificationList) getPendingRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.Repository != nil {
-			continue
+	return container.FilterSlice(nl, func(n *Notification) (int64, bool) {
+		if n.Repository != nil {
+			return 0, false
 		}
-		ids.Add(notification.RepoID)
-	}
-	return ids.Values()
+		return n.RepoID, true
+	})
 }
 
 // LoadRepos loads repositories from database
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index 1b05f0aa35..f8ee271a6b 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -21,16 +21,15 @@ type IssueList []*Issue
 
 // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo
 func (issues IssueList) getRepoIDs() []int64 {
-	repoIDs := make(container.Set[int64], len(issues))
-	for _, issue := range issues {
+	return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
 		if issue.Repo == nil {
-			repoIDs.Add(issue.RepoID)
+			return issue.RepoID, true
 		}
 		if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil {
-			repoIDs.Add(issue.PullRequest.HeadRepoID)
+			return issue.PullRequest.HeadRepoID, true
 		}
-	}
-	return repoIDs.Values()
+		return 0, false
+	})
 }
 
 // LoadRepositories loads issues' all repositories

From 6cac11cb1bc4b42bc7851a59b1f3a94700c5eb84 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 10 Apr 2024 07:44:48 +0200
Subject: [PATCH 658/679] Fix line height on inline code preview (#30372)

Fixes https://github.com/go-gitea/gitea/issues/30353.

I don't know what causes `code-inner` to not inherit `line-height` from
its direct parent `.lines-code` but instead from grandparent `.markup`
even thought MDN tells me it's
[inherited](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#formal_definition).
This causes no negative impact on other code views, so I think it's the
best solution.
---
 web_src/css/base.css | 1 +
 1 file changed, 1 insertion(+)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 02bc1b7220..d188bf6f3e 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1242,6 +1242,7 @@ overflow-menu .ui.label {
   white-space: pre-wrap;
   word-break: break-all;
   overflow-wrap: anywhere;
+  line-height: inherit; /* needed for inline code preview in markup */
 }
 
 .blame .code-inner {

From 50099d7af436785daf66a3a9f27bd5c009f90684 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 10 Apr 2024 08:13:22 +0200
Subject: [PATCH 659/679] Various improvements for long file and commit names
 (#30374)

Fixes: https://github.com/go-gitea/gitea/issues/29438

This contains numerous enhancements for how large commit messages and
large filenames render. Another notable change is that the file path is
no longer cut off by backend at 30 chars, but rendered in full with
wrapping.

<img width="1329" alt="Screenshot 2024-04-09 at 21 53 57"
src="https://github.com/go-gitea/gitea/assets/115237/5ccbb3d6-643a-4f60-ba79-3572b36d5182">
<hr>
<img width="711" alt="Screenshot 2024-04-09 at 21 44 24"
src="https://github.com/go-gitea/gitea/assets/115237/6ffe8fbb-407c-4aa7-b591-3d80daea7d57">
<hr>
<img width="439" alt="Screenshot 2024-04-09 at 21 19 03"
src="https://github.com/go-gitea/gitea/assets/115237/1ec7f6e9-2fd8-4841-87eb-6ca02ab9cd61">
<hr>
<img width="444" alt="Screenshot 2024-04-09 at 21 18 52"
src="https://github.com/go-gitea/gitea/assets/115237/70931b9e-5841-477e-b3bc-98f8d2662964">

---------

Co-authored-by: Giteabot <teabot@gitea.io>
---
 templates/repo/commit_page.tmpl   |  4 +-
 templates/repo/diff/box.tmpl      | 34 +++++++-------
 templates/repo/home.tmpl          | 21 +++++----
 templates/repo/view_file.tmpl     |  6 +--
 templates/repo/view_list.tmpl     |  8 +++-
 web_src/css/base.css              | 13 ++++++
 web_src/css/modules/container.css | 23 +---------
 web_src/css/repo.css              | 76 +++++++++++++++++++++----------
 8 files changed, 107 insertions(+), 78 deletions(-)

diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 49a0b445b1..7fec88cb79 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -18,10 +18,10 @@
 			{{end}}
 		{{end}}
 		<div class="ui top attached header clearing segment tw-relative commit-header {{$class}}">
-			<div class="tw-flex tw-mb-4 tw-flex-wrap">
+			<div class="tw-flex tw-mb-4 tw-gap-1">
 				<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
 				{{if not $.PageIsWiki}}
-					<div>
+					<div class="commit-header-buttons">
 						<a class="ui primary tiny button" href="{{.SourcePath}}">
 							{{ctx.Locale.Tr "repo.diff.browse_source"}}
 						</a>
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 5327b7f02c..92a3163642 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -111,7 +111,7 @@
 					{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
 					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
 						<h4 class="diff-file-header sticky-2nd-row ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between tw-flex-wrap">
-							<div class="diff-file-name tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
+							<div class="diff-file-name tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-flex-wrap">
 								<button class="fold-file btn interact-bg tw-p-1{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
 										{{svg "octicon-chevron-right" 18}}
@@ -128,21 +128,23 @@
 										{{template "repo/diff/stats" dict "file" . "root" $}}
 									{{end}}
 								</div>
-								<span class="file tw-font-mono"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}</span>
-								<button class="btn interact-fg tw-p-2" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
-								{{if $file.IsGenerated}}
-									<span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span>
-								{{end}}
-								{{if $file.IsVendored}}
-									<span class="ui label">{{ctx.Locale.Tr "repo.diff.vendored"}}</span>
-								{{end}}
-								{{if and $file.Mode $file.OldMode}}
-									{{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}}
-									{{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}
-									<span class="tw-ml-4 tw-font-mono">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
-								{{else if $file.Mode}}
-									<span class="tw-ml-4 tw-font-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
-								{{end}}
+								<span class="file tw-flex tw-items-center tw-font-mono tw-flex-1"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>
+									{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
+									<button class="btn interact-fg tw-p-2" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
+									{{if $file.IsGenerated}}
+										<span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span>
+									{{end}}
+									{{if $file.IsVendored}}
+										<span class="ui label">{{ctx.Locale.Tr "repo.diff.vendored"}}</span>
+									{{end}}
+									{{if and $file.Mode $file.OldMode}}
+										{{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}}
+										{{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}
+										<span class="tw-mx-2 tw-font-mono tw-whitespace-nowrap">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
+									{{else if $file.Mode}}
+										<span class="tw-mx-2 tw-font-mono tw-whitespace-nowrap">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
+									{{end}}
+								</span>
 							</div>
 							<div class="diff-file-header-actions tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
 								{{if $showFileViewToggle}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index ab37f7e318..e18a0aec17 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -50,8 +50,11 @@
 			</div>
 		{{end}}
 		{{template "repo/sub_menu" .}}
+		{{$n := len .TreeNames}}
+		{{$l := Eval $n "-" 1}}
+		{{$isHomepage := (eq $n 0)}}
 		<div class="repo-button-row">
-			<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-y-2">
+			<div class="tw-flex tw-items-center tw-gap-y-2">
 				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
 				{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 					{{$cmpBranch := ""}}
@@ -66,9 +69,7 @@
 					</a>
 				{{end}}
 				<!-- Show go to file and breadcrumbs if not on home page -->
-				{{$n := len .TreeNames}}
-				{{$l := Eval $n "-" 1}}
-				{{if eq $n 0}}
+				{{if $isHomepage}}
 					<a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
 				{{end}}
 
@@ -92,20 +93,20 @@
 					</button>
 				{{end}}
 
-				{{if and (eq $n 0) (.Repository.IsTemplate)}}
+				{{if and $isHomepage (.Repository.IsTemplate)}}
 					<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
 						{{ctx.Locale.Tr "repo.use_template"}}
 					</a>
 				{{end}}
-				{{if ne $n 0}}
+				{{if (not $isHomepage)}}
 					<span class="breadcrumb repo-path tw-ml-1">
 						<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
 						{{- range $i, $v := .TreeNames -}}
 							<span class="breadcrumb-divider">/</span>
 							{{- if eq $i $l -}}
-								<span class="active section" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</span>
+								<span class="active section" title="{{$v}}">{{$v}}</span>
 							{{- else -}}
-								{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</a></span>
+								{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{$v}}</a></span>
 							{{- end -}}
 						{{- end -}}
 					</span>
@@ -113,7 +114,7 @@
 			</div>
 			<div class="tw-flex tw-items-center">
 				<!-- Only show clone panel in repository home page -->
-				{{if eq $n 0}}
+				{{if $isHomepage}}
 					<div class="clone-panel ui action tiny input">
 						{{template "repo/clone_buttons" .}}
 						<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
@@ -136,7 +137,7 @@
 					</div>
 					{{template "repo/cite/cite_modal" .}}
 				{{end}}
-				{{if and (ne $n 0) (not .IsViewFile) (not .IsBlame)}}
+				{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}
 					<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
 						{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
 					</a>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 9c5bd9094d..0683004718 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -11,13 +11,13 @@
 	{{end}}
 
 	{{if not .ReadmeInList}}
-		<div id="repo-file-commit-box" class="ui top attached header list-header tw-mb-4">
-			<div>
+		<div id="repo-file-commit-box" class="ui top attached header list-header tw-mb-4 tw-flex tw-justify-between">
+			<div class="latest-commit">
 				{{template "repo/latest_commit" .}}
 			</div>
 			{{if .LatestCommit}}
 				{{if .LatestCommit.Committer}}
-					<div class="ui text grey right age">
+					<div class="text grey age">
 						{{TimeSince .LatestCommit.Committer.When ctx.Locale}}
 					</div>
 				{{end}}
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index 7c463c50a6..fb257bd474 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -1,8 +1,12 @@
 <table id="repo-files-table" class="ui single line table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
 	<thead>
 		<tr class="commit-list">
-			<th colspan="2">
-				{{template "repo/latest_commit" .}}
+			<th class="tw-overflow-hidden" colspan="2">
+				<div class="tw-flex">
+					<div class="latest-commit">
+						{{template "repo/latest_commit" .}}
+					</div>
+				</div>
 			</th>
 			<th class="text grey right age">{{if .LatestCommit}}{{if .LatestCommit.Committer}}{{TimeSince .LatestCommit.Committer.When ctx.Locale}}{{end}}{{end}}</th>
 		</tr>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index d188bf6f3e..c6a22a5dc4 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -25,6 +25,19 @@
   --tab-size: 4;
   --checkbox-size: 15px; /* height and width of checkbox and radio inputs */
   --page-spacing: 16px; /* space between page elements */
+  --page-margin-x: 32px; /* minimum space on left and right side of page */
+}
+
+@media (min-width: 768px) and (max-width: 1200px) {
+  :root {
+    --page-margin-x: 16px;
+  }
+}
+
+@media (max-width: 767.98px) {
+  :root {
+    --page-margin-x: 8px;
+  }
 }
 
 :root * {
diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css
index dc854f89d0..f394d6c06d 100644
--- a/web_src/css/modules/container.css
+++ b/web_src/css/modules/container.css
@@ -49,30 +49,11 @@
 /* overwrite width of containers inside the main page content div (div with class "page-content") */
 .page-content .ui.ui.ui.container:not(.fluid) {
   width: 1280px;
-  max-width: calc(100% - 64px);
+  max-width: calc(100% - calc(2 * var(--page-margin-x)));
   margin-left: auto;
   margin-right: auto;
 }
 
 .ui.container.fluid.padded {
-  padding: 0 32px;
-}
-
-/* enable fluid page widths for medium size viewports */
-@media (min-width: 768px) and (max-width: 1200px) {
-  .page-content .ui.ui.ui.container:not(.fluid) {
-    max-width: calc(100% - 32px);
-  }
-  .ui.container.fluid.padded {
-    padding: 0 16px;
-  }
-}
-
-@media (max-width: 767.98px) {
-  .page-content .ui.ui.ui.container:not(.fluid) {
-    max-width: calc(100% - 16px);
-  }
-  .ui.container.fluid.padded {
-    padding: 0 8px;
-  }
+  padding: 0 var(--page-margin-x);
 }
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 8b91b599e7..c579745238 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -177,12 +177,44 @@
   }
 }
 
-.repository.file.list .repo-path {
-  word-break: break-word;
+.commit-summary {
+  flex: 1;
+  overflow-wrap: anywhere;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
 }
 
-.repository.file.list #repo-files-table {
-  table-layout: fixed;
+.commit-header .commit-summary,
+td .commit-summary {
+  white-space: normal;
+}
+
+.latest-commit {
+  display: flex;
+  flex: 1;
+  align-items: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+@media (max-width: 767.98px) {
+  .latest-commit .sha {
+    display: none;
+  }
+  .latest-commit .commit-summary {
+    margin-left: 8px;
+  }
+}
+
+.repo-path {
+  display: flex;
+  overflow-wrap: anywhere;
+}
+
+/* this is what limits the commit table width to a value that works on all viewport sizes */
+#repo-files-table th:first-of-type {
+  max-width: calc(calc(min(100vw, 1280px)) - 145px - calc(2 * var(--page-margin-x)));
 }
 
 .repository.file.list #repo-files-table thead th {
@@ -262,7 +294,6 @@
 }
 
 .repository.file.list #repo-files-table td.age {
-  width: 120px;
   color: var(--color-text-light-1);
 }
 
@@ -1219,10 +1250,6 @@
   margin: 0;
 }
 
-.repository #commits-table td.message {
-  text-overflow: unset;
-}
-
 .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
   background-color: var(--color-light) !important;
 }
@@ -2114,6 +2141,20 @@
   padding-bottom: 0 !important;
 }
 
+.commit-header-buttons {
+  display: flex;
+  gap: 4px;
+  align-items: flex-start;
+  white-space: nowrap;
+}
+
+@media (max-width: 767.98px) {
+  .commit-header-buttons {
+    flex-direction: column;
+    align-items: stretch;
+  }
+}
+
 .settings.webhooks .list > .item:not(:first-child),
 .settings.githooks .list > .item:not(:first-child),
 .settings.actions .list > .item:not(:first-child) {
@@ -2346,7 +2387,7 @@ tbody.commit-list {
 .author-wrapper {
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: calc(100% - 50px);
+  max-width: 100%;
   display: inline-block;
   vertical-align: middle;
 }
@@ -2371,10 +2412,6 @@ tbody.commit-list {
   tr.commit-list {
     width: 100%;
   }
-  th .message-wrapper {
-    display: block;
-    max-width: calc(100vw - 70px);
-  }
   .author-wrapper {
     max-width: 80px;
   }
@@ -2384,27 +2421,18 @@ tbody.commit-list {
   tr.commit-list {
     width: 723px;
   }
-  th .message-wrapper {
-    max-width: 120px;
-  }
 }
 
 @media (min-width: 992px) and (max-width: 1200px) {
   tr.commit-list {
     width: 933px;
   }
-  th .message-wrapper {
-    max-width: 350px;
-  }
 }
 
 @media (min-width: 1201px) {
   tr.commit-list {
     width: 1127px;
   }
-  th .message-wrapper {
-    max-width: 525px;
-  }
 }
 
 .commit-list .commit-status-link {
@@ -2732,7 +2760,7 @@ tbody.commit-list {
   .repository.file.list #repo-files-table .entry td.message,
   .repository.file.list #repo-files-table .commit-list td.message,
   .repository.file.list #repo-files-table .entry span.commit-summary,
-  .repository.file.list #repo-files-table .commit-list span.commit-summary {
+  .repository.file.list #repo-files-table .commit-list tr span.commit-summary {
     display: none !important;
   }
   .repository.view.issue .comment-list .timeline,

From c1f76aea45f11e1d5ae22c047cf3bda9c681de8d Mon Sep 17 00:00:00 2001
From: Rafael <git@rafael.ovh>
Date: Wed, 10 Apr 2024 18:49:57 +0100
Subject: [PATCH 660/679] Use raw Wiki links for non-renderable Wiki files
 (#30273)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

In Wiki pages, short-links created to local Wiki files were always
expanded as regular Wiki Links. In particular, if a link wanted to point
to a file that Gitea doesn't know how to render (e.g, a .zip file), a
user following the link would be silently redirected to the Wiki's home
page.

This change makes short-links* in Wiki pages be expanded to raw wiki
links, so these local wiki files may be accessed without manually
accessing their URL.

* only short-links ending in a file extension that isn't renderable are
affected.

Closes #27121.

Signed-off-by: Rafael Girão <rafael.s.girao@tecnico.ulisboa.pt>
Co-authored-by: silverwind <me@silverwind.io>
---
 modules/markup/html.go                        |  22 +++++++++++++---
 modules/markup/html_test.go                   |  12 +++++++++
 modules/markup/markdown/markdown_test.go      |  24 +++++++++---------
 modules/markup/markdown/transform_link.go     |  13 +++++++++-
 routers/web/repo/wiki_test.go                 |  13 +++++-----
 .../81/a1c039774e337621609336c0e44ed9f92278f7 | Bin 0 -> 68 bytes
 .../91/dc55f9de16a558e859123f2b99668469b1a1dc | Bin 0 -> 234 bytes
 .../a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02 |   1 +
 .../cf/19952a40b92eb2f86689146a65ac2d87c0818a | Bin 0 -> 255 bytes
 .../e1/6da91326b845f1ba86a7df0a67db352f96dcb0 | Bin 0 -> 149 bytes
 .../user2/repo1.wiki.git/refs/heads/master    |   2 +-
 tests/integration/git_clone_wiki_test.go      |   1 +
 12 files changed, 65 insertions(+), 23 deletions(-)
 create mode 100644 tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7
 create mode 100644 tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc
 create mode 100644 tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02
 create mode 100644 tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a
 create mode 100644 tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0

diff --git a/modules/markup/html.go b/modules/markup/html.go
index 56aa1cb49c..cef643bf18 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -709,7 +709,8 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 
 		name += tail
 		image := false
-		switch ext := filepath.Ext(link); ext {
+		ext := filepath.Ext(link)
+		switch ext {
 		// fast path: empty string, ignore
 		case "":
 			// leave image as false
@@ -767,11 +768,26 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			}
 		} else {
 			if !absoluteLink {
+				var base string
 				if ctx.IsWiki {
-					link = util.URLJoin(ctx.Links.WikiLink(), link)
+					switch ext {
+					case "":
+						// no file extension, create a regular wiki link
+						base = ctx.Links.WikiLink()
+					default:
+						// we have a file extension:
+						// return a regular wiki link if it's a renderable file (extension),
+						// raw link otherwise
+						if Type(link) != "" {
+							base = ctx.Links.WikiLink()
+						} else {
+							base = ctx.Links.WikiRawLink()
+						}
+					}
 				} else {
-					link = util.URLJoin(ctx.Links.SrcLink(), link)
+					base = ctx.Links.SrcLink()
 				}
+				link = util.URLJoin(base, link)
 			}
 			childNode.Type = html.TextNode
 			childNode.Data = name
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 55de65d196..916e74fb62 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -427,6 +427,10 @@ func TestRender_ShortLinks(t *testing.T) {
 	otherImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+Other.jpg")
 	encodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+%23.jpg")
 	notencodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "some", "path", "Link+#.jpg")
+	renderableFileURL := util.URLJoin(tree, "markdown_file.md")
+	renderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "markdown_file.md")
+	unrenderableFileURL := util.URLJoin(tree, "file.zip")
+	unrenderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "file.zip")
 	favicon := "http://google.com/favicon.ico"
 
 	test(
@@ -481,6 +485,14 @@ func TestRender_ShortLinks(t *testing.T) {
 		"[[Link]] [[Other Link]] [[Link?]]",
 		`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a> <a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
 		`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a> <a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`)
+	test(
+		"[[markdown_file.md]]",
+		`<p><a href="`+renderableFileURL+`" rel="nofollow">markdown_file.md</a></p>`,
+		`<p><a href="`+renderableFileURLWiki+`" rel="nofollow">markdown_file.md</a></p>`)
+	test(
+		"[[file.zip]]",
+		`<p><a href="`+unrenderableFileURL+`" rel="nofollow">file.zip</a></p>`,
+		`<p><a href="`+unrenderableFileURLWiki+`" rel="nofollow">file.zip</a></p>`)
 	test(
 		"[[Link #.jpg]]",
 		`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`,
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index a9c9024982..d9b67e43af 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -653,9 +653,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -711,9 +711,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -769,9 +769,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -829,9 +829,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -889,9 +889,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -951,9 +951,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
diff --git a/modules/markup/markdown/transform_link.go b/modules/markup/markdown/transform_link.go
index 8bf19ea4ce..7e305b74bc 100644
--- a/modules/markup/markdown/transform_link.go
+++ b/modules/markup/markdown/transform_link.go
@@ -4,6 +4,8 @@
 package markdown
 
 import (
+	"path/filepath"
+
 	"code.gitea.io/gitea/modules/markup"
 	giteautil "code.gitea.io/gitea/modules/util"
 
@@ -18,7 +20,16 @@ func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, r
 	if !isAnchorFragment && !markup.IsFullURLBytes(link) {
 		base := ctx.Links.Base
 		if ctx.IsWiki {
-			base = ctx.Links.WikiLink()
+			if filepath.Ext(string(link)) == "" {
+				// This link doesn't have a file extension - assume a regular wiki link
+				base = ctx.Links.WikiLink()
+			} else if markup.Type(string(link)) != "" {
+				// If it's a file type we can render, use a regular wiki link
+				base = ctx.Links.WikiLink()
+			} else {
+				// Otherwise, use a raw link instead
+				base = ctx.Links.WikiRawLink()
+			}
 		} else if ctx.Links.HasBranchInfo() {
 			base = ctx.Links.SrcLink()
 		}
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 2894c06fbd..4602dcfeb4 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -200,12 +200,13 @@ func TestDeleteWikiPagePost(t *testing.T) {
 
 func TestWikiRaw(t *testing.T) {
 	for filepath, filetype := range map[string]string{
-		"jpeg.jpg":                 "image/jpeg",
-		"images/jpeg.jpg":          "image/jpeg",
-		"Page With Spaced Name":    "text/plain; charset=utf-8",
-		"Page-With-Spaced-Name":    "text/plain; charset=utf-8",
-		"Page With Spaced Name.md": "", // there is no "Page With Spaced Name.md" in repo
-		"Page-With-Spaced-Name.md": "text/plain; charset=utf-8",
+		"jpeg.jpg":                      "image/jpeg",
+		"images/jpeg.jpg":               "image/jpeg",
+		"files/Non-Renderable-File.zip": "application/octet-stream",
+		"Page With Spaced Name":         "text/plain; charset=utf-8",
+		"Page-With-Spaced-Name":         "text/plain; charset=utf-8",
+		"Page With Spaced Name.md":      "", // there is no "Page With Spaced Name.md" in repo
+		"Page-With-Spaced-Name.md":      "text/plain; charset=utf-8",
 	} {
 		unittest.PrepareTestEnv(t)
 
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7
new file mode 100644
index 0000000000000000000000000000000000000000..17a5547da8286e051f08cb79be1169efacb59654
GIT binary patch
literal 68
zcmV-K0K5Nq0V^p=O;s>8WH2-^Ff%bx@XOEB4NA>RNi9lD%1PCA%gjmDtI8~3c$m9V
aSZ#;v$6am9?{lT!Hr1bYX9EC2Q5Lqqq8?@d

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc
new file mode 100644
index 0000000000000000000000000000000000000000..8390a40c0805716854172c1d6c26de3e3845cbf6
GIT binary patch
literal 234
zcmV<G02Tju0V^p=O;s>5w`4FhFfcPQQSivmP1VayVR+T_r@efUH~XP%EAKClN_3cu
z?N&pT1SF=X>V{{QWaxV40+}GyMzKeJyfpDk%lIT}wrp;^tlw3e#TcrC3lfu4Q*`|j
zAvP5KNeVoZ(l^mZfMKqov;3mR-Ln*+dP4J3i<1)zQd1P%GIMY`$HV{#6w-hyiWwRg
z9<VI;GcHq1m~3|7iQmniN)_KB@|hqv7BiGwU2xrY=g#+4E2sBuUodk+f5??MsBKvV
ksp)!I1?dch6Q#ejryZ9Nl{m=GDabf~H^at102VQB$tL$~r2qf`

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02
new file mode 100644
index 0000000000..94312d3db6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02
@@ -0,0 +1 @@
+x��Kn� D��죱�v�EQ��~n�@3F���r�\,d��^�T��S�ϏGj|��K+D����
$2`�4��Y��Y�u{�Xho\���u4E;k-	P4�Q^H�84��lk.��i�_��|g�V�v��=����|�-U�q8�;�ZJ��,��Nn�_����0�a�3���ҿ	T�mķ�q�1m�؍b�򵵣^���z��/����_�5�zR'-'�~�tl�
\ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a
new file mode 100644
index 0000000000000000000000000000000000000000..b384e5c72edc4f93a1c15a2ab088e39f7d12176b
GIT binary patch
literal 255
zcmV<b0094Z0cDOsZp0uAMZ4w{UUgGV5|RL6R8`Ye_j3aSCPB)C2*_NhN9n~%r`^>H
z%ir?<em352p}^4Vr;><3b7zI{+7Xv1#6*+OydQjTw3c!jr8XSv4cjr%R-khhg>l*l
zr^xF;Dc(^hq!&uiByGp1(cmN)9%YFMuIQ0g_z3CiGs0_n$R;;)NEk1L>=tZnjx}Tx
zvDwQTaK*VC)hIN)bhVg$AQ$=<HivTg3yk;W*Qr&d?yHkmCwq4ewz!=tx}$_<C0L`y
z6?~}UGbGA8*%IyqJI=(|m3%(KQp%psZCf8KhiNK6JI7O1gg?95L(T`~wpYC8>>Ccm
FYv@Vae2V}8

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0
new file mode 100644
index 0000000000000000000000000000000000000000..da281ff7916dcb6dac5154f4d87bab9af4428b29
GIT binary patch
literal 149
zcmV;G0BZku0ZYosPf{?nG+_wvW@Zs#U|`^2_|;Mq!FtVSryP){0>tbLG7KL1xv6@&
zDWM^p49pR8S<^tcw1S&~k>v$50|QG6P+4%;hZE<0Lp3~n{GUGJI(b4TjA7BFo{(0a
z85@+ufyOg3$uZ-yKmw?rfq@Z-mo$P{aO+qh)}dJy;LXYgQpN~`zCb!2#9;scqk${9
Dp4URN

literal 0
HcmV?d00001

diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
index 38984b12b7..b352f15003 100644
--- a/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
@@ -1 +1 @@
-0dca5bd9b5d7ef937710e056f575e86c0184ba85
+a5bbc0fd39a696feabed2d4cccaf05abbcaf3b02
diff --git a/tests/integration/git_clone_wiki_test.go b/tests/integration/git_clone_wiki_test.go
index d7949dfe25..ef662300f3 100644
--- a/tests/integration/git_clone_wiki_test.go
+++ b/tests/integration/git_clone_wiki_test.go
@@ -45,6 +45,7 @@ func TestRepoCloneWiki(t *testing.T) {
 			assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md"))
 			assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md"))
 			assertFileExist(t, filepath.Join(dstPath, "images"))
+			assertFileExist(t, filepath.Join(dstPath, "files/Non-Renderable-File.zip"))
 			assertFileExist(t, filepath.Join(dstPath, "jpeg.jpg"))
 		})
 	})

From e7ecdba4933f4ff5e6e2b24cdcea8b76f486966a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 10 Apr 2024 22:29:05 +0200
Subject: [PATCH 661/679] Minor color tweaks (#30397)

New approach to color shades: Stem all colors off the body color
`#1b1f23` using [this](https://pinetools.com/darken-color) and
[this](https://pinetools.com/lighten-color) tool. The differences are
very subtle, but it will give a more consistent color scheme until
https://github.com/go-gitea/gitea/issues/30160.

<img width="1342" alt="Screenshot 2024-04-10 at 20 44 16"
src="https://github.com/go-gitea/gitea/assets/115237/75b65797-2521-46ea-91d8-d76f77b591b1">
---
 web_src/css/themes/theme-gitea-dark.css  | 128 +++++++++++------------
 web_src/css/themes/theme-gitea-light.css |  18 ++--
 2 files changed, 73 insertions(+), 73 deletions(-)

diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index c74f334c2d..7bf2c982c6 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -30,45 +30,45 @@
   --color-primary-alpha-90: #4183c4e1;
   --color-primary-hover: var(--color-primary-light-1);
   --color-primary-active: var(--color-primary-light-2);
-  --color-secondary: #3b444a;
-  --color-secondary-dark-1: #424b51;
-  --color-secondary-dark-2: #4a545b;
-  --color-secondary-dark-3: #59646c;
-  --color-secondary-dark-4: #6b7681;
-  --color-secondary-dark-5: #78858f;
-  --color-secondary-dark-6: #87929d;
-  --color-secondary-dark-7: #939ea9;
-  --color-secondary-dark-8: #a1acb4;
-  --color-secondary-dark-9: #aab3bc;
-  --color-secondary-dark-10: #b6bfc8;
-  --color-secondary-dark-11: #c2cbd3;
-  --color-secondary-dark-12: #ccd4dc;
-  --color-secondary-dark-13: #cfd7df;
-  --color-secondary-light-1: #2e353b;
-  --color-secondary-light-2: #2b353e;
-  --color-secondary-light-3: #1c2227;
-  --color-secondary-light-4: #161b1f;
-  --color-secondary-alpha-10: #3b444a19;
-  --color-secondary-alpha-20: #3b444a33;
-  --color-secondary-alpha-30: #3b444a4b;
-  --color-secondary-alpha-40: #3b444a66;
-  --color-secondary-alpha-50: #3b444a80;
-  --color-secondary-alpha-60: #3b444a99;
-  --color-secondary-alpha-70: #3b444ab3;
-  --color-secondary-alpha-80: #3b444acc;
-  --color-secondary-alpha-90: #3b444ae1;
+  --color-secondary: #3b444c;
+  --color-secondary-dark-1: #414b54;
+  --color-secondary-dark-2: #49545f;
+  --color-secondary-dark-3: #576471;
+  --color-secondary-dark-4: #677685;
+  --color-secondary-dark-5: #758594;
+  --color-secondary-dark-6: #8392a0;
+  --color-secondary-dark-7: #929eab;
+  --color-secondary-dark-8: #a2acb7;
+  --color-secondary-dark-9: #a9b3bd;
+  --color-secondary-dark-10: #b7bfc7;
+  --color-secondary-dark-11: #c5cbd2;
+  --color-secondary-dark-12: #cfd4da;
+  --color-secondary-dark-13: #d2d7dc;
+  --color-secondary-light-1: #313940;
+  --color-secondary-light-2: #292f35;
+  --color-secondary-light-3: #1d2226;
+  --color-secondary-light-4: #171b1e;
+  --color-secondary-alpha-10: #3b444c19;
+  --color-secondary-alpha-20: #3b444c33;
+  --color-secondary-alpha-30: #3b444c4b;
+  --color-secondary-alpha-40: #3b444c66;
+  --color-secondary-alpha-50: #3b444c80;
+  --color-secondary-alpha-60: #3b444c99;
+  --color-secondary-alpha-70: #3b444cb3;
+  --color-secondary-alpha-80: #3b444ccc;
+  --color-secondary-alpha-90: #3b444ce1;
   --color-secondary-button: var(--color-secondary-dark-4);
   --color-secondary-hover: var(--color-secondary-dark-3);
   --color-secondary-active: var(--color-secondary-dark-2);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #f8f8f9;
-  --color-console-fg-subtle: #bec4c8;
+  --color-console-fg: #f7f8f9;
+  --color-console-fg-subtle: #bdc4cc;
   --color-console-bg: #171b1e;
   --color-console-border: #2e353b;
-  --color-console-hover-bg: #292d31;
+  --color-console-hover-bg: #272d33;
   --color-console-active-bg: #2e353b;
-  --color-console-menu-bg: #252b30;
-  --color-console-menu-border: #424b51;
+  --color-console-menu-bg: #262b31;
+  --color-console-menu-border: #414b55;
   /* named colors */
   --color-red: #cc4848;
   --color-orange: #cc580c;
@@ -122,7 +122,7 @@
   --color-brown-dark-2: #835b42;
   --color-black-dark-2: #272930;
   /* ansi colors used for actions console and console files */
-  --color-ansi-black: #1d2328;
+  --color-ansi-black: #1e2327;
   --color-ansi-red: #cc4848;
   --color-ansi-green: #87ab63;
   --color-ansi-yellow: #cc9903;
@@ -139,8 +139,8 @@
   --color-ansi-bright-cyan: #00b6ad;
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
-  --color-grey: #384147;
-  --color-grey-light: #828f99;
+  --color-grey: #384149;
+  --color-grey-light: #818f9e;
   --color-gold: #b1983b;
   --color-white: #ffffff;
   --color-diff-removed-word-bg: #6f3333;
@@ -180,55 +180,55 @@
   --color-orange-badge-hover-bg: #f2711c4d;
   --color-git: #f05133;
   /* target-based colors */
-  --color-body: #1c1f25;
+  --color-body: #1b1f23;
   --color-box-header: #1a1d1f;
   --color-box-body: #14171a;
-  --color-box-body-highlight: #1c2227;
-  --color-text-dark: #f8f8f9;
-  --color-text: #d1d5d8;
-  --color-text-light: #bdc3c7;
-  --color-text-light-1: #a8afb5;
-  --color-text-light-2: #929ba2;
-  --color-text-light-3: #7c8790;
+  --color-box-body-highlight: #1e2226;
+  --color-text-dark: #f7f8f9;
+  --color-text: #d0d5da;
+  --color-text-light: #bcc3cb;
+  --color-text-light-1: #a5afb9;
+  --color-text-light-2: #8f9ba8;
+  --color-text-light-3: #788797;
   --color-footer: var(--color-nav-bg);
-  --color-timeline: #353c42;
+  --color-timeline: #343c44;
   --color-input-text: var(--color-text-dark);
-  --color-input-background: #151a1e;
-  --color-input-toggle-background: #2e353b;
+  --color-input-background: #171a1e;
+  --color-input-toggle-background: #2e353c;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
   --color-light: #00001728;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
-  --color-light-border: #e8e8ff28;
-  --color-hover: #e8e8ff19;
-  --color-active: #e8e8ff24;
-  --color-menu: #151a1e;
-  --color-card: #151a1e;
-  --color-markup-table-row: #e8e8ff0f;
-  --color-markup-code-block: #e8e8ff12;
-  --color-markup-code-inline: #e8e8ff28;
-  --color-button: #151a1e;
+  --color-light-border: #e8f3ff28;
+  --color-hover: #e8f3ff19;
+  --color-active: #e8f3ff24;
+  --color-menu: #171a1e;
+  --color-card: #171a1e;
+  --color-markup-table-row: #e8f3ff0f;
+  --color-markup-code-block: #e8f3ff12;
+  --color-markup-code-inline: #e8f3ff28;
+  --color-button: #171a1e;
   --color-code-bg: #14171a;
   --color-shadow: #00001758;
-  --color-secondary-bg: #2f3138;
-  --color-expand-button: #2b353e;
+  --color-secondary-bg: #2a3137;
+  --color-expand-button: #2f363d;
   --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-5);
   --color-project-board-bg: var(--color-secondary-light-2);
   --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
-  --color-reaction-bg: #e8e8ff12;
+  --color-reaction-bg: #e8f3ff12;
   --color-reaction-hover-bg: var(--color-primary-light-4);
   --color-reaction-active-bg: var(--color-primary-light-5);
-  --color-tooltip-text: #fafafb;
-  --color-tooltip-bg: #000017f0;
-  --color-nav-bg: #16191c;
+  --color-tooltip-text: #f9fafb;
+  --color-tooltip-bg: #000b17f0;
+  --color-nav-bg: #16191d;
   --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
   --color-secondary-nav-bg: #181c20;
   --color-label-text: var(--color-text);
-  --color-label-bg: #73828e4b;
-  --color-label-hover-bg: #73828ea0;
-  --color-label-active-bg: #73828eff;
+  --color-label-bg: #7282924b;
+  --color-label-hover-bg: #728292a0;
+  --color-label-active-bg: #728292ff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-5);
   --color-highlight-fg: #87651e;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 01dd8ba4f7..dfccd37647 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -61,14 +61,14 @@
   --color-secondary-hover: var(--color-secondary-dark-5);
   --color-secondary-active: var(--color-secondary-dark-6);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #f8f8f9;
-  --color-console-fg-subtle: #bec4c8;
+  --color-console-fg: #f7f8f9;
+  --color-console-fg-subtle: #bdc4cc;
   --color-console-bg: #171b1e;
   --color-console-border: #2e353b;
-  --color-console-hover-bg: #292d31;
+  --color-console-hover-bg: #272d33;
   --color-console-active-bg: #2e353b;
-  --color-console-menu-bg: #252b30;
-  --color-console-menu-border: #424b51;
+  --color-console-menu-bg: #262b31;
+  --color-console-menu-border: #414b55;
   /* named colors */
   --color-red: #db2828;
   --color-orange: #f2711c;
@@ -81,7 +81,7 @@
   --color-purple: #a333c8;
   --color-pink: #e03997;
   --color-brown: #a5673f;
-  --color-black: #191c1d;
+  --color-black: #1d2328;
   /* light variants - produced via Sass scale-color(color, $lightness: +25%) */
   --color-red-light: #e45e5e;
   --color-orange-light: #f59555;
@@ -94,7 +94,7 @@
   --color-purple-light: #bb64d8;
   --color-pink-light: #e86bb1;
   --color-brown-light: #c58b66;
-  --color-black-light: #525558;
+  --color-black-light: #4b5b68;
   /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
   --color-red-dark-1: #c82121;
   --color-orange-dark-1: #e6630d;
@@ -107,7 +107,7 @@
   --color-purple-dark-1: #932eb4;
   --color-pink-dark-1: #db228a;
   --color-brown-dark-1: #955d39;
-  --color-black-dark-1: #16191c;
+  --color-black-dark-1: #2c3339;
   /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
   --color-red-dark-2: #b11e1e;
   --color-orange-dark-2: #cc580c;
@@ -122,7 +122,7 @@
   --color-brown-dark-2: #845232;
   --color-black-dark-2: #131619;
   /* ansi colors used for actions console and console files */
-  --color-ansi-black: #1f2326;
+  --color-ansi-black: #1e2327;
   --color-ansi-red: #cc4848;
   --color-ansi-green: #87ab63;
   --color-ansi-yellow: #cc9903;

From 17c7ebb327faf6f8b6d659a0adb451b553405116 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Thu, 11 Apr 2024 00:24:56 +0000
Subject: [PATCH 662/679] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_ja-JP.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 57b2aff254..0edd6c5dd7 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -2109,7 +2109,7 @@ settings.pulls.default_delete_branch_after_merge=デフォルトでプルリク
 settings.pulls.default_allow_edits_from_maintainers=デフォルトでメンテナからの編集を許可する
 settings.releases_desc=リリースを有効にする
 settings.packages_desc=リポジトリパッケージレジストリを有効にする
-settings.projects_desc=リポジトリプロジェクトを有効にする
+settings.projects_desc=プロジェクトを有効にする
 settings.projects_mode_all=すべてのプロジェクト
 settings.actions_desc=Actionsを有効にする
 settings.admin_settings=管理者用設定

From f0bfad29ea00eea7fd421d51352825aaa931aba8 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 11 Apr 2024 09:12:40 +0800
Subject: [PATCH 663/679] Replace MSSQL driver with a better maintained version
 (#30390)

As the latest tag of `github.com/denisenkom/go-mssqldb` is in 2022, but
as a fork of it, `github.com/microsoft/go-mssqldb` has more activities
than the original repository. We can convert the driver to the fork.

Since the interface of Go database driver are the same, it should have
no any affect for the end users.
---
 assets/go-licenses.json | 15 ++++++++++-----
 go.mod                  |  2 +-
 go.sum                  | 28 ++++++++++++++++++----------
 models/db/engine.go     |  6 +++---
 4 files changed, 32 insertions(+), 19 deletions(-)

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index be9022b694..ea73182a83 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -304,11 +304,6 @@
     "path": "github.com/davecgh/go-spew/spew/LICENSE",
     "licenseText": "ISC License\n\nCopyright (c) 2012-2016 Dave Collins \u003cdave@davec.name\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
   },
-  {
-    "name": "github.com/denisenkom/go-mssqldb",
-    "path": "github.com/denisenkom/go-mssqldb/LICENSE.txt",
-    "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
-  },
   {
     "name": "github.com/dgryski/go-rendezvous",
     "path": "github.com/dgryski/go-rendezvous/LICENSE",
@@ -759,6 +754,16 @@
     "path": "github.com/microcosm-cc/bluemonday/LICENSE.md",
     "licenseText": "SPDX short identifier: BSD-3-Clause\nhttps://opensource.org/licenses/BSD-3-Clause\n\nCopyright (c) 2014, David Kitchen \u003cdavid@buro9.com\u003e\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the organisation (Microcosm) nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
   },
+  {
+    "name": "github.com/microsoft/go-mssqldb",
+    "path": "github.com/microsoft/go-mssqldb/LICENSE.txt",
+    "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) Microsoft Corporation.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
+  {
+    "name": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg",
+    "path": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg/LICENSE.txt",
+    "licenseText": "Copyright (c) 2021 Swisscom (Switzerland) Ltd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\n"
+  },
   {
     "name": "github.com/miekg/dns",
     "path": "github.com/miekg/dns/LICENSE",
diff --git a/go.mod b/go.mod
index 27e1924806..1e0f1ea8f8 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,6 @@ require (
 	github.com/buildkite/terminal-to-html/v3 v3.11.0
 	github.com/caddyserver/certmagic v0.20.0
 	github.com/chi-middleware/proxy v1.1.1
-	github.com/denisenkom/go-mssqldb v0.12.3
 	github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
 	github.com/djherbis/buffer v1.2.0
 	github.com/djherbis/nio/v3 v3.0.1
@@ -77,6 +76,7 @@ require (
 	github.com/meilisearch/meilisearch-go v0.26.2
 	github.com/mholt/archiver/v3 v3.5.1
 	github.com/microcosm-cc/bluemonday v1.0.26
+	github.com/microsoft/go-mssqldb v1.7.0
 	github.com/minio/minio-go/v7 v7.0.69
 	github.com/msteinert/pam v1.2.0
 	github.com/nektos/act v0.2.52
diff --git a/go.sum b/go.sum
index 55f24bf2e7..864bed6677 100644
--- a/go.sum
+++ b/go.sum
@@ -38,11 +38,20 @@ github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 h1:r3qt8PCHnfjOv9PN3H
 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121/go.mod h1:Ock8XgA7pvULhIaHGAk/cDnRfNrF9Jey81nPcc403iU=
 github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U=
 github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
-github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
 github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
@@ -220,7 +229,6 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -355,7 +363,6 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -513,6 +520,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -551,6 +560,8 @@ github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Cl
 github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
 github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
 github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
+github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs=
+github.com/microsoft/go-mssqldb v1.7.0/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
 github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
 github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
@@ -574,7 +585,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
@@ -627,7 +637,8 @@ github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ
 github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
 github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
-github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -836,7 +847,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -871,7 +881,6 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -1022,7 +1031,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
diff --git a/models/db/engine.go b/models/db/engine.go
index 2a2743e927..8684c4e2f1 100755
--- a/models/db/engine.go
+++ b/models/db/engine.go
@@ -21,9 +21,9 @@ import (
 	"xorm.io/xorm/names"
 	"xorm.io/xorm/schemas"
 
-	_ "github.com/denisenkom/go-mssqldb" // Needed for the MSSQL driver
-	_ "github.com/go-sql-driver/mysql"   // Needed for the MySQL driver
-	_ "github.com/lib/pq"                // Needed for the Postgresql driver
+	_ "github.com/go-sql-driver/mysql"  // Needed for the MySQL driver
+	_ "github.com/lib/pq"               // Needed for the Postgresql driver
+	_ "github.com/microsoft/go-mssqldb" // Needed for the MSSQL driver
 )
 
 var (

From e6d3f9fc07d193ce95cf0964f0d12da87156fac9 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 11 Apr 2024 04:40:03 +0200
Subject: [PATCH 664/679] Upgrade golangci-lint to v1.57.2 (#30401)

Update and adapt to one setting
[deprecation](https://github.com/golangci/golangci-lint/pull/4509).
---
 .golangci.yml | 5 +----
 Makefile      | 2 +-
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/.golangci.yml b/.golangci.yml
index d6ce37f49a..5be2cefe44 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -30,10 +30,6 @@ linters:
 
 run:
   timeout: 10m
-  skip-dirs:
-    - node_modules
-    - public
-    - web_src
 
 linters-settings:
   stylecheck:
@@ -94,6 +90,7 @@ linters-settings:
 issues:
   max-issues-per-linter: 0
   max-same-issues: 0
+  exclude-dirs: [node_modules, public, web_src]
   exclude-rules:
     # Exclude some linters from running on tests files.
     - path: _test\.go
diff --git a/Makefile b/Makefile
index 8489520920..ee9c90e8d9 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ XGO_VERSION := go-1.22.x
 AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0
 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
-GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2
 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1
 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285

From 50dbed652738182eb42af51967ec7bd10e84ede9 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 11 Apr 2024 05:16:44 +0200
Subject: [PATCH 665/679] Fix author name alignment in commits table (#30396)

Fixes https://github.com/go-gitea/gitea/issues/30129 by introducing a
wrapper div with flexbox that collapses any inter-tag whitespace within.
View diff with whitespace hidden.

Author names aligned:
<img width="172" alt="Screenshot 2024-04-10 at 19 41 27"
src="https://github.com/go-gitea/gitea/assets/115237/d761e8f2-0e67-4f84-8d37-9ed73850470a">

Vertically centered on expand:
<img width="466" alt="Screenshot 2024-04-10 at 19 43 02"
src="https://github.com/go-gitea/gitea/assets/115237/decd68b3-19b5-4cfa-a505-b358e4a0715b">

Ellipsis works:

<img width="344" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/6f8624a2-f8b6-4f3e-ac98-c44dd0cdfca5">
---
 templates/repo/commits_list.tmpl | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index be73c4ca18..bb5d2a0394 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -13,17 +13,19 @@
 			{{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}}
 			{{range .Commits}}
 				<tr>
-					<td class="author tw-flex">
-						{{$userName := .Author.Name}}
-						{{if .User}}
-							{{if and .User.FullName DefaultShowFullName}}
-								{{$userName = .User.FullName}}
+					<td class="author">
+						<div class="tw-flex">
+							{{$userName := .Author.Name}}
+							{{if .User}}
+								{{if and .User.FullName DefaultShowFullName}}
+									{{$userName = .User.FullName}}
+								{{end}}
+								{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
+							{{else}}
+								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}}
+								<span class="author-wrapper">{{$userName}}</span>
 							{{end}}
-							{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
-						{{else}}
-							{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}}
-							<span class="author-wrapper">{{$userName}}</span>
-						{{end}}
+						</div>
 					</td>
 					<td class="sha">
 						{{$class := "ui sha label"}}

From f3cc00626b5a170e193961b885d4e60088ef7d9b Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 11 Apr 2024 11:57:03 +0800
Subject: [PATCH 666/679] Update actions variables documents (#30394)

Fix #30393

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Zettat123 <zettat123@gmail.com>
---
 .../content/usage/actions/act-runner.en-us.md | 31 --------------
 .../content/usage/actions/act-runner.zh-cn.md | 29 -------------
 docs/content/usage/actions/variables.en-us.md | 41 +++++++++++++++++++
 docs/content/usage/actions/variables.zh-cn.md | 39 ++++++++++++++++++
 4 files changed, 80 insertions(+), 60 deletions(-)
 create mode 100644 docs/content/usage/actions/variables.en-us.md
 create mode 100644 docs/content/usage/actions/variables.zh-cn.md

diff --git a/docs/content/usage/actions/act-runner.en-us.md b/docs/content/usage/actions/act-runner.en-us.md
index b2806bf5dd..942d126919 100644
--- a/docs/content/usage/actions/act-runner.en-us.md
+++ b/docs/content/usage/actions/act-runner.en-us.md
@@ -303,34 +303,3 @@ sudo systemctl enable act_runner --now
 ```
 
 If using Docker, the `act_runner` user should also be added to the `docker` group before starting the service. Keep in mind that this effectively gives `act_runner` root access to the system [[1]](https://docs.docker.com/engine/security/#docker-daemon-attack-surface).
-
-## Configuration variable
-
-You can create configuration variables on the user, organization and repository level.
-The level of the variable depends on where you created it.
-
-### Naming conventions
-
-The following rules apply to variable names:
-
-- Variable names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
-
-- Variable names must not start with the `GITHUB_` and `GITEA_` prefix.
-
-- Variable names must not start with a number.
-
-- Variable names are case-insensitive.
-
-- Variable names must be unique at the level they are created at.
-
-- Variable names must not be `CI`.
-
-### Using variable
-
-After creating configuration variables, they will be automatically filled in the `vars` context.
-They can be accessed through expressions like `{{ vars.VARIABLE_NAME }}` in the workflow.
-
-### Precedence
-
-If a variable with the same name exists at multiple levels, the variable at the lowest level takes precedence:
-A repository variable will always be chosen over an organization/user variable.
diff --git a/docs/content/usage/actions/act-runner.zh-cn.md b/docs/content/usage/actions/act-runner.zh-cn.md
index 274b0f0692..e5ebff976d 100644
--- a/docs/content/usage/actions/act-runner.zh-cn.md
+++ b/docs/content/usage/actions/act-runner.zh-cn.md
@@ -258,32 +258,3 @@ Runner的标签用于确定Runner可以运行哪些Job以及如何运行它们
 Runner将从Gitea实例获取Job并自动运行它们。
 
 由于Act Runner仍处于开发中,建议定期检查最新版本并进行升级。
-
-## 变量
-
-您可以创建用户、组织和仓库级别的变量。变量的级别取决于创建它的位置。
-
-### 命名规则
-
-以下规则适用于变量名:
-
-- 变量名称只能包含字母数字字符 (`[a-z]`, `[A-Z]`, `[0-9]`) 或下划线 (`_`)。不允许使用空格。
-
-- 变量名称不能以 `GITHUB_` 和 `GITEA_` 前缀开头。
-
-- 变量名称不能以数字开头。
-
-- 变量名称不区分大小写。
-
-- 变量名称在创建它们的级别上必须是唯一的。
-
-- 变量名称不能为 “CI”。
-
-### 使用
-
-创建配置变量后,它们将自动填充到 `vars` 上下文中。您可以在工作流中使用类似 `{{ vars.VARIABLE_NAME }}` 这样的表达式来使用它们。
-
-### 优先级
-
-如果同名变量存在于多个级别,则级别最低的变量优先。
-仓库级别的变量总是比组织或者用户级别的变量优先被选中。
diff --git a/docs/content/usage/actions/variables.en-us.md b/docs/content/usage/actions/variables.en-us.md
new file mode 100644
index 0000000000..dee2e74234
--- /dev/null
+++ b/docs/content/usage/actions/variables.en-us.md
@@ -0,0 +1,41 @@
+---
+date: "2024-04-10T22:21:00+08:00"
+title: "Variables"
+slug: "actions-variables"
+sidebar_position: 25
+draft: false
+toc: false
+menu:
+  sidebar:
+    parent: "actions"
+    name: "Variables"
+    sidebar_position: 25
+    identifier: "actions-variables"
+---
+
+## Variables
+
+You can create configuration variables on the user, organization and repository level.
+The level of the variable depends on where you created it. When creating a variable, the
+key will be converted to uppercase. You need use uppercase on the yaml file.
+
+### Naming conventions
+
+The following rules apply to variable names:
+
+- Variable names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
+- Variable names must not start with the `GITHUB_` and `GITEA_` prefix.
+- Variable names must not start with a number.
+- Variable names are case-insensitive.
+- Variable names must be unique at the level they are created at.
+- Variable names must not be `CI`.
+
+### Using variable
+
+After creating configuration variables, they will be automatically filled in the `vars` context.
+They can be accessed through expressions like `${{ vars.VARIABLE_NAME }}` in the workflow.
+
+### Precedence
+
+If a variable with the same name exists at multiple levels, the variable at the lowest level takes precedence:
+A repository variable will always be chosen over an organization/user variable.
diff --git a/docs/content/usage/actions/variables.zh-cn.md b/docs/content/usage/actions/variables.zh-cn.md
new file mode 100644
index 0000000000..77643408a1
--- /dev/null
+++ b/docs/content/usage/actions/variables.zh-cn.md
@@ -0,0 +1,39 @@
+---
+date: "2024-04-10T22:21:00+08:00"
+title: "变量"
+slug: "actions-variables"
+sidebar_position: 25
+draft: false
+toc: false
+menu:
+  sidebar:
+    parent: "actions"
+    name: "变量"
+    sidebar_position: 25
+    identifier: "actions-variables"
+---
+
+## 变量
+
+您可以创建用户、组织和仓库级别的变量。变量的级别取决于创建它的位置。当创建变量时,变量的名称会被
+转换为大写,在yaml文件中引用时需要使用大写。
+
+### 命名规则
+
+以下规则适用于变量名:
+
+- 变量名称只能包含字母数字字符 (`[a-z]`, `[A-Z]`, `[0-9]`) 或下划线 (`_`)。不允许使用空格。
+- 变量名称不能以 `GITHUB_` 和 `GITEA_` 前缀开头。
+- 变量名称不能以数字开头。
+- 变量名称不区分大小写。
+- 变量名称在创建它们的级别上必须是唯一的。
+- 变量名称不能为 `CI`。
+
+### 使用
+
+创建配置变量后,它们将自动填充到 `vars` 上下文中。您可以在工作流中使用类似 `${{ vars.VARIABLE_NAME }}` 这样的表达式来使用它们。
+
+### 优先级
+
+如果同名变量存在于多个级别,则级别最低的变量优先。
+仓库级别的变量总是比组织或者用户级别的变量优先被选中。

From 96d31fe0a8b88c09488989cd5459d4124dcb7983 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Thu, 11 Apr 2024 16:11:32 +0900
Subject: [PATCH 667/679] Avoid user does not exist error when detecting
 schedule actions when the commit author is an external user (#30357)

![image](https://github.com/go-gitea/gitea/assets/18380374/ddf6ee84-2242-49b9-b066-bd8429ba4d76)

When repo is a mirror, and commit author is an external user, then
`GetUserByEmail` will return error.

reproduce/test:
- mirror Gitea to your instance
- disable action and enable it again, this will trigger
`DetectAndHandleSchedules`

ps: also follow #24706, it only fixed normal runs, not scheduled runs.
---
 models/actions/schedule_list.go     | 3 +++
 services/actions/notifier_helper.go | 9 +++------
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/models/actions/schedule_list.go b/models/actions/schedule_list.go
index 1d35adc420..5361b94801 100644
--- a/models/actions/schedule_list.go
+++ b/models/actions/schedule_list.go
@@ -40,6 +40,9 @@ func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
 			schedule.TriggerUser = user_model.NewActionsUser()
 		} else {
 			schedule.TriggerUser = users[schedule.TriggerUserID]
+			if schedule.TriggerUser == nil {
+				schedule.TriggerUser = user_model.NewGhostUser()
+			}
 		}
 	}
 	return nil
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 8c98f56af5..c48886a824 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -525,12 +525,9 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository)
 	}
 
 	// We need a notifyInput to call handleSchedules
-	// Here we use the commit author as the Doer of the notifyInput
-	commitUser, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
-	if err != nil {
-		return fmt.Errorf("get user by email: %w", err)
-	}
-	notifyInput := newNotifyInput(repo, commitUser, webhook_module.HookEventSchedule)
+	// if repo is a mirror, commit author maybe an external user,
+	// so we use action user as the Doer of the notifyInput
+	notifyInput := newNotifyInput(repo, user_model.NewActionsUser(), webhook_module.HookEventSchedule)
 
 	return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
 }

From 0fe9f93eb4c94d55e43b18b9c3cc6d513a34c0b5 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Thu, 11 Apr 2024 16:01:44 +0800
Subject: [PATCH 668/679] Check the token's owner and repository when
 registering a runner (#30406)

Fix #30378
---
 models/organization/org.go           |  3 +++
 routers/api/actions/runner/runner.go | 14 ++++++++++++++
 services/repository/delete.go        |  1 +
 services/user/delete.go              |  1 +
 4 files changed, 19 insertions(+)

diff --git a/models/organization/org.go b/models/organization/org.go
index ba0fd756e3..b33d15d29c 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"strings"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -402,6 +403,8 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
 		&TeamInvite{OrgID: org.ID},
 		&secret_model.Secret{OwnerID: org.ID},
 		&user_model.Blocking{BlockerID: org.ID},
+		&actions_model.ActionRunner{OwnerID: org.ID},
+		&actions_model.ActionRunnerToken{OwnerID: org.ID},
 	); err != nil {
 		return fmt.Errorf("DeleteBeans: %w", err)
 	}
diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index 1d07be3aec..b2f3e7af78 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -9,6 +9,8 @@ import (
 	"net/http"
 
 	actions_model "code.gitea.io/gitea/models/actions"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
@@ -52,6 +54,18 @@ func (s *Service) Register(
 		return nil, errors.New("runner registration token has been invalidated, please use the latest one")
 	}
 
+	if runnerToken.OwnerID > 0 {
+		if _, err := user_model.GetUserByID(ctx, runnerToken.OwnerID); err != nil {
+			return nil, errors.New("owner of the token not found")
+		}
+	}
+
+	if runnerToken.RepoID > 0 {
+		if _, err := repo_model.GetRepositoryByID(ctx, runnerToken.RepoID); err != nil {
+			return nil, errors.New("repository of the token not found")
+		}
+	}
+
 	labels := req.Msg.Labels
 	// TODO: agent_labels should be removed from pb after Gitea 1.20 released.
 	// Old version runner's agent_labels slice is not empty and labels slice is empty.
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 8d6729f31b..7c7dfe2ddd 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -163,6 +163,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 		&actions_model.ActionScheduleSpec{RepoID: repoID},
 		&actions_model.ActionSchedule{RepoID: repoID},
 		&actions_model.ActionArtifact{RepoID: repoID},
+		&actions_model.ActionRunnerToken{RepoID: repoID},
 	); err != nil {
 		return fmt.Errorf("deleteBeans: %w", err)
 	}
diff --git a/services/user/delete.go b/services/user/delete.go
index 212cb83e03..889da3eb67 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -94,6 +94,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 		&actions_model.ActionRunner{OwnerID: u.ID},
 		&user_model.Blocking{BlockerID: u.ID},
 		&user_model.Blocking{BlockeeID: u.ID},
+		&actions_model.ActionRunnerToken{OwnerID: u.ID},
 	); err != nil {
 		return fmt.Errorf("deleteBeans: %w", err)
 	}

From 26ee66327fecf2f1755a47f9193bc6305132def1 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 12 Apr 2024 02:22:59 +0800
Subject: [PATCH 669/679] Split `issue edit` code from `repo-legacy.js` into
 its own file (#30419)

Follow Split `index.js` to separate files (#17315)

It's time to move some code away from the messy "legacy" file.
---
 web_src/js/features/repo-issue-edit.js | 206 ++++++++++++++++++++++++
 web_src/js/features/repo-legacy.js     | 207 +------------------------
 2 files changed, 209 insertions(+), 204 deletions(-)
 create mode 100644 web_src/js/features/repo-issue-edit.js

diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js
new file mode 100644
index 0000000000..4c03325c7a
--- /dev/null
+++ b/web_src/js/features/repo-issue-edit.js
@@ -0,0 +1,206 @@
+import $ from 'jquery';
+import {handleReply} from './repo-issue.js';
+import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
+import {createDropzone} from './dropzone.js';
+import {GET, POST} from '../modules/fetch.js';
+import {hideElem, showElem} from '../utils/dom.js';
+import {attachRefIssueContextPopup} from './contextpopup.js';
+import {initCommentContent, initMarkupContent} from '../markup/content.js';
+
+const {csrfToken} = window.config;
+
+async function onEditContent(event) {
+  event.preventDefault();
+
+  const segment = this.closest('.header').nextElementSibling;
+  const editContentZone = segment.querySelector('.edit-content-zone');
+  const renderContent = segment.querySelector('.render-content');
+  const rawContent = segment.querySelector('.raw-content');
+
+  let comboMarkdownEditor;
+
+  /**
+   * @param {HTMLElement} dropzone
+   */
+  const setupDropzone = async (dropzone) => {
+    if (!dropzone) return null;
+
+    let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
+    let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
+    const dz = await createDropzone(dropzone, {
+      url: dropzone.getAttribute('data-upload-url'),
+      headers: {'X-Csrf-Token': csrfToken},
+      maxFiles: dropzone.getAttribute('data-max-file'),
+      maxFilesize: dropzone.getAttribute('data-max-size'),
+      acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
+      addRemoveLinks: true,
+      dictDefaultMessage: dropzone.getAttribute('data-default-message'),
+      dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
+      dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
+      dictRemoveFile: dropzone.getAttribute('data-remove-file'),
+      timeout: 0,
+      thumbnailMethod: 'contain',
+      thumbnailWidth: 480,
+      thumbnailHeight: 480,
+      init() {
+        this.on('success', (file, data) => {
+          file.uuid = data.uuid;
+          fileUuidDict[file.uuid] = {submitted: false};
+          const input = document.createElement('input');
+          input.id = data.uuid;
+          input.name = 'files';
+          input.type = 'hidden';
+          input.value = data.uuid;
+          dropzone.querySelector('.files').append(input);
+        });
+        this.on('removedfile', async (file) => {
+          document.getElementById(file.uuid)?.remove();
+          if (disableRemovedfileEvent) return;
+          if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
+            try {
+              await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
+            } catch (error) {
+              console.error(error);
+            }
+          }
+        });
+        this.on('submit', () => {
+          for (const fileUuid of Object.keys(fileUuidDict)) {
+            fileUuidDict[fileUuid].submitted = true;
+          }
+        });
+        this.on('reload', async () => {
+          try {
+            const response = await GET(editContentZone.getAttribute('data-attachment-url'));
+            const data = await response.json();
+            // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
+            disableRemovedfileEvent = true;
+            dz.removeAllFiles(true);
+            dropzone.querySelector('.files').innerHTML = '';
+            for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
+            fileUuidDict = {};
+            disableRemovedfileEvent = false;
+
+            for (const attachment of data) {
+              const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
+              dz.emit('addedfile', attachment);
+              dz.emit('thumbnail', attachment, imgSrc);
+              dz.emit('complete', attachment);
+              fileUuidDict[attachment.uuid] = {submitted: true};
+              dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
+              const input = document.createElement('input');
+              input.id = attachment.uuid;
+              input.name = 'files';
+              input.type = 'hidden';
+              input.value = attachment.uuid;
+              dropzone.querySelector('.files').append(input);
+            }
+            if (!dropzone.querySelector('.dz-preview')) {
+              dropzone.classList.remove('dz-started');
+            }
+          } catch (error) {
+            console.error(error);
+          }
+        });
+      },
+    });
+    dz.emit('reload');
+    return dz;
+  };
+
+  const cancelAndReset = (e) => {
+    e.preventDefault();
+    showElem(renderContent);
+    hideElem(editContentZone);
+    comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
+  };
+
+  const saveAndRefresh = async (e) => {
+    e.preventDefault();
+    showElem(renderContent);
+    hideElem(editContentZone);
+    const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
+    try {
+      const params = new URLSearchParams({
+        content: comboMarkdownEditor.value(),
+        context: editContentZone.getAttribute('data-context'),
+      });
+      for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
+
+      const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
+      const data = await response.json();
+      if (!data.content) {
+        renderContent.innerHTML = document.getElementById('no-content').innerHTML;
+        rawContent.textContent = '';
+      } else {
+        renderContent.innerHTML = data.content;
+        rawContent.textContent = comboMarkdownEditor.value();
+        const refIssues = renderContent.querySelectorAll('p .ref-issue');
+        attachRefIssueContextPopup(refIssues);
+      }
+      const content = segment;
+      if (!content.querySelector('.dropzone-attachments')) {
+        if (data.attachments !== '') {
+          content.insertAdjacentHTML('beforeend', data.attachments);
+        }
+      } else if (data.attachments === '') {
+        content.querySelector('.dropzone-attachments').remove();
+      } else {
+        content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
+      }
+      dropzoneInst?.emit('submit');
+      dropzoneInst?.emit('reload');
+      initMarkupContent();
+      initCommentContent();
+    } catch (error) {
+      console.error(error);
+    }
+  };
+
+  comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+  if (!comboMarkdownEditor) {
+    editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
+    comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+    comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
+    editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
+    editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
+  }
+
+  // Show write/preview tab and copy raw content as needed
+  showElem(editContentZone);
+  hideElem(renderContent);
+  if (!comboMarkdownEditor.value()) {
+    comboMarkdownEditor.value(rawContent.textContent);
+  }
+  comboMarkdownEditor.focus();
+}
+
+export function initRepoIssueCommentEdit() {
+  // Edit issue or comment content
+  $(document).on('click', '.edit-content', onEditContent);
+
+  // Quote reply
+  $(document).on('click', '.quote-reply', async function (event) {
+    event.preventDefault();
+    const target = $(this).data('target');
+    const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
+    const content = `> ${quote}\n\n`;
+    let editor;
+    if ($(this).hasClass('quote-reply-diff')) {
+      const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
+      editor = await handleReply($replyBtn);
+    } else {
+      // for normal issue/comment page
+      editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
+    }
+    if (editor) {
+      if (editor.value()) {
+        editor.value(`${editor.value()}\n\n${content}`);
+      } else {
+        editor.value(content);
+      }
+      editor.focus();
+      editor.moveCursorToEnd();
+    }
+  });
+}
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 4c7dd36920..e83de27e4c 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -3,7 +3,7 @@ import {
   initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
   initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
   initRepoIssueTitleEdit, initRepoIssueWipToggle,
-  initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
+  initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor,
 } from './repo-issue.js';
 import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
 import {svg} from '../svg.js';
@@ -15,18 +15,13 @@ import {
 import {initCitationFileCopyContent} from './citation.js';
 import {initCompLabelEdit} from './comp/LabelEdit.js';
 import {initRepoDiffConversationNav} from './repo-diff.js';
-import {createDropzone} from './dropzone.js';
-import {initCommentContent, initMarkupContent} from '../markup/content.js';
 import {initCompReactionSelector} from './comp/ReactionSelector.js';
 import {initRepoSettingBranches} from './repo-settings.js';
 import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
 import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
 import {hideElem, showElem} from '../utils/dom.js';
-import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
-import {attachRefIssueContextPopup} from './contextpopup.js';
-import {POST, GET} from '../modules/fetch.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
+import {initRepoIssueCommentEdit} from './repo-issue-edit.js';
 
 // if there are draft comments, confirm before reloading, to avoid losing comments
 function reloadConfirmDraftComment() {
@@ -316,172 +311,6 @@ export function initRepoCommentForm() {
   selectItem('.select-assignee', '#assignee_id');
 }
 
-async function onEditContent(event) {
-  event.preventDefault();
-
-  const segment = this.closest('.header').nextElementSibling;
-  const editContentZone = segment.querySelector('.edit-content-zone');
-  const renderContent = segment.querySelector('.render-content');
-  const rawContent = segment.querySelector('.raw-content');
-
-  let comboMarkdownEditor;
-
-  /**
-   * @param {HTMLElement} dropzone
-   */
-  const setupDropzone = async (dropzone) => {
-    if (!dropzone) return null;
-
-    let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
-    let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
-    const dz = await createDropzone(dropzone, {
-      url: dropzone.getAttribute('data-upload-url'),
-      headers: {'X-Csrf-Token': csrfToken},
-      maxFiles: dropzone.getAttribute('data-max-file'),
-      maxFilesize: dropzone.getAttribute('data-max-size'),
-      acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
-      addRemoveLinks: true,
-      dictDefaultMessage: dropzone.getAttribute('data-default-message'),
-      dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
-      dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
-      dictRemoveFile: dropzone.getAttribute('data-remove-file'),
-      timeout: 0,
-      thumbnailMethod: 'contain',
-      thumbnailWidth: 480,
-      thumbnailHeight: 480,
-      init() {
-        this.on('success', (file, data) => {
-          file.uuid = data.uuid;
-          fileUuidDict[file.uuid] = {submitted: false};
-          const input = document.createElement('input');
-          input.id = data.uuid;
-          input.name = 'files';
-          input.type = 'hidden';
-          input.value = data.uuid;
-          dropzone.querySelector('.files').append(input);
-        });
-        this.on('removedfile', async (file) => {
-          document.getElementById(file.uuid)?.remove();
-          if (disableRemovedfileEvent) return;
-          if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
-            try {
-              await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
-            } catch (error) {
-              console.error(error);
-            }
-          }
-        });
-        this.on('submit', () => {
-          for (const fileUuid of Object.keys(fileUuidDict)) {
-            fileUuidDict[fileUuid].submitted = true;
-          }
-        });
-        this.on('reload', async () => {
-          try {
-            const response = await GET(editContentZone.getAttribute('data-attachment-url'));
-            const data = await response.json();
-            // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
-            disableRemovedfileEvent = true;
-            dz.removeAllFiles(true);
-            dropzone.querySelector('.files').innerHTML = '';
-            for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
-            fileUuidDict = {};
-            disableRemovedfileEvent = false;
-
-            for (const attachment of data) {
-              const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
-              dz.emit('addedfile', attachment);
-              dz.emit('thumbnail', attachment, imgSrc);
-              dz.emit('complete', attachment);
-              fileUuidDict[attachment.uuid] = {submitted: true};
-              dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
-              const input = document.createElement('input');
-              input.id = attachment.uuid;
-              input.name = 'files';
-              input.type = 'hidden';
-              input.value = attachment.uuid;
-              dropzone.querySelector('.files').append(input);
-            }
-            if (!dropzone.querySelector('.dz-preview')) {
-              dropzone.classList.remove('dz-started');
-            }
-          } catch (error) {
-            console.error(error);
-          }
-        });
-      },
-    });
-    dz.emit('reload');
-    return dz;
-  };
-
-  const cancelAndReset = (e) => {
-    e.preventDefault();
-    showElem(renderContent);
-    hideElem(editContentZone);
-    comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
-  };
-
-  const saveAndRefresh = async (e) => {
-    e.preventDefault();
-    showElem(renderContent);
-    hideElem(editContentZone);
-    const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
-    try {
-      const params = new URLSearchParams({
-        content: comboMarkdownEditor.value(),
-        context: editContentZone.getAttribute('data-context'),
-      });
-      for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
-
-      const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
-      const data = await response.json();
-      if (!data.content) {
-        renderContent.innerHTML = document.getElementById('no-content').innerHTML;
-        rawContent.textContent = '';
-      } else {
-        renderContent.innerHTML = data.content;
-        rawContent.textContent = comboMarkdownEditor.value();
-        const refIssues = renderContent.querySelectorAll('p .ref-issue');
-        attachRefIssueContextPopup(refIssues);
-      }
-      const content = segment;
-      if (!content.querySelector('.dropzone-attachments')) {
-        if (data.attachments !== '') {
-          content.insertAdjacentHTML('beforeend', data.attachments);
-        }
-      } else if (data.attachments === '') {
-        content.querySelector('.dropzone-attachments').remove();
-      } else {
-        content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
-      }
-      dropzoneInst?.emit('submit');
-      dropzoneInst?.emit('reload');
-      initMarkupContent();
-      initCommentContent();
-    } catch (error) {
-      console.error(error);
-    }
-  };
-
-  comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
-  if (!comboMarkdownEditor) {
-    editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
-    comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
-    comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
-    editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
-    editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
-  }
-
-  // Show write/preview tab and copy raw content as needed
-  showElem(editContentZone);
-  hideElem(renderContent);
-  if (!comboMarkdownEditor.value()) {
-    comboMarkdownEditor.value(rawContent.textContent);
-  }
-  comboMarkdownEditor.focus();
-}
-
 export function initRepository() {
   if (!$('.page-content.repository').length) return;
 
@@ -585,33 +414,3 @@ export function initRepository() {
 
   initUnicodeEscapeButton();
 }
-
-function initRepoIssueCommentEdit() {
-  // Edit issue or comment content
-  $(document).on('click', '.edit-content', onEditContent);
-
-  // Quote reply
-  $(document).on('click', '.quote-reply', async function (event) {
-    event.preventDefault();
-    const target = $(this).data('target');
-    const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
-    const content = `> ${quote}\n\n`;
-    let editor;
-    if ($(this).hasClass('quote-reply-diff')) {
-      const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
-      editor = await handleReply($replyBtn);
-    } else {
-      // for normal issue/comment page
-      editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
-    }
-    if (editor) {
-      if (editor.value()) {
-        editor.value(`${editor.value()}\n\n${content}`);
-      } else {
-        editor.value(content);
-      }
-      editor.focus();
-      editor.moveCursorToEnd();
-    }
-  });
-}

From fc34481d054a9324ea4654dc721e54e2f608ac17 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 12 Apr 2024 09:41:50 +0800
Subject: [PATCH 670/679] Add commit status summary table to reduce query from
 commit status table (#30223)

This PR adds a new table named commit status summary to reduce queries
from the commit status table. After this change, commit status summary
table will be used for the final result, commit status table will be for
details.

---------

Co-authored-by: Jason Song <i@wolfogre.com>
---
 models/git/commit_status.go                   | 21 ++---
 models/git/commit_status_summary.go           | 84 +++++++++++++++++++
 models/migrations/migrations.go               |  3 +
 models/migrations/v1_23/v295.go               | 18 ++++
 services/actions/commit_status.go             | 20 ++---
 .../repository/commitstatus/commitstatus.go   | 48 +++++++++--
 tests/integration/pull_status_test.go         |  7 ++
 7 files changed, 170 insertions(+), 31 deletions(-)
 create mode 100644 models/git/commit_status_summary.go
 create mode 100644 models/migrations/v1_23/v295.go

diff --git a/models/git/commit_status.go b/models/git/commit_status.go
index bb75dcca26..c3cda7b73d 100644
--- a/models/git/commit_status.go
+++ b/models/git/commit_status.go
@@ -292,30 +292,27 @@ func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOp
 }
 
 // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
-func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) {
+func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map[int64][]*CommitStatus, error) {
 	type result struct {
 		Index  int64
 		RepoID int64
+		SHA    string
 	}
 
-	results := make([]result, 0, len(repoIDsToLatestCommitSHAs))
+	results := make([]result, 0, len(repoSHAs))
 
 	getBase := func() *xorm.Session {
 		return db.GetEngine(ctx).Table(&CommitStatus{})
 	}
 
 	// Create a disjunction of conditions for each repoID and SHA pair
-	conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs))
-	for repoID, sha := range repoIDsToLatestCommitSHAs {
-		conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha})
+	conds := make([]builder.Cond, 0, len(repoSHAs))
+	for _, repoSHA := range repoSHAs {
+		conds = append(conds, builder.Eq{"repo_id": repoSHA.RepoID, "sha": repoSHA.SHA})
 	}
 	sess := getBase().Where(builder.Or(conds...)).
-		Select("max( `index` ) as `index`, repo_id").
-		GroupBy("context_hash, repo_id").OrderBy("max( `index` ) desc")
-
-	if !listOptions.IsListAll() {
-		sess = db.SetSessionPagination(sess, &listOptions)
-	}
+		Select("max( `index` ) as `index`, repo_id, sha").
+		GroupBy("context_hash, repo_id, sha").OrderBy("max( `index` ) desc")
 
 	err := sess.Find(&results)
 	if err != nil {
@@ -332,7 +329,7 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
 			cond := builder.Eq{
 				"`index`": result.Index,
 				"repo_id": result.RepoID,
-				"sha":     repoIDsToLatestCommitSHAs[result.RepoID],
+				"sha":     result.SHA,
 			}
 			conds = append(conds, cond)
 		}
diff --git a/models/git/commit_status_summary.go b/models/git/commit_status_summary.go
new file mode 100644
index 0000000000..01674e943d
--- /dev/null
+++ b/models/git/commit_status_summary.go
@@ -0,0 +1,84 @@
+// Copyright 2024 Gitea. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"xorm.io/builder"
+)
+
+// CommitStatusSummary holds the latest commit Status of a single Commit
+type CommitStatusSummary struct {
+	ID     int64                 `xorm:"pk autoincr"`
+	RepoID int64                 `xorm:"INDEX UNIQUE(repo_id_sha)"`
+	SHA    string                `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
+	State  api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
+}
+
+func init() {
+	db.RegisterModel(new(CommitStatusSummary))
+}
+
+type RepoSHA struct {
+	RepoID int64
+	SHA    string
+}
+
+func GetLatestCommitStatusForRepoAndSHAs(ctx context.Context, repoSHAs []RepoSHA) ([]*CommitStatus, error) {
+	cond := builder.NewCond()
+	for _, rs := range repoSHAs {
+		cond = cond.Or(builder.Eq{"repo_id": rs.RepoID, "sha": rs.SHA})
+	}
+
+	var summaries []CommitStatusSummary
+	if err := db.GetEngine(ctx).Where(cond).Find(&summaries); err != nil {
+		return nil, err
+	}
+
+	commitStatuses := make([]*CommitStatus, 0, len(repoSHAs))
+	for _, summary := range summaries {
+		commitStatuses = append(commitStatuses, &CommitStatus{
+			RepoID: summary.RepoID,
+			SHA:    summary.SHA,
+			State:  summary.State,
+		})
+	}
+	return commitStatuses, nil
+}
+
+func UpdateCommitStatusSummary(ctx context.Context, repoID int64, sha string) error {
+	commitStatuses, _, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll)
+	if err != nil {
+		return err
+	}
+	state := CalcCommitStatus(commitStatuses)
+	// mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database,
+	// so we need to use insert in on duplicate
+	if setting.Database.Type.IsMySQL() {
+		_, err := db.GetEngine(ctx).Exec("INSERT INTO commit_status_summary (repo_id,sha,state) VALUES (?,?,?) ON DUPLICATE KEY UPDATE state=?",
+			repoID, sha, state.State, state.State)
+		return err
+	}
+
+	if cnt, err := db.GetEngine(ctx).Where("repo_id=? AND sha=?", repoID, sha).
+		Cols("state").
+		Update(&CommitStatusSummary{
+			State: state.State,
+		}); err != nil {
+		return err
+	} else if cnt == 0 {
+		_, err = db.GetEngine(ctx).Insert(&CommitStatusSummary{
+			RepoID: repoID,
+			SHA:    sha,
+			State:  state.State,
+		})
+		return err
+	}
+	return nil
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 387cd96a53..3ea8f2acbf 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -576,7 +576,10 @@ var migrations = []Migration{
 
 	// Gitea 1.22.0 ends at 294
 
+	// v294 -> v295
 	NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
+	// v295 -> v296
+	NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_23/v295.go b/models/migrations/v1_23/v295.go
new file mode 100644
index 0000000000..9a2003cfc1
--- /dev/null
+++ b/models/migrations/v1_23/v295.go
@@ -0,0 +1,18 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import "xorm.io/xorm"
+
+func AddCommitStatusSummary(x *xorm.Engine) error {
+	type CommitStatusSummary struct {
+		ID     int64  `xorm:"pk autoincr"`
+		RepoID int64  `xorm:"INDEX UNIQUE(repo_id_sha)"`
+		SHA    string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
+		State  string `xorm:"VARCHAR(7) NOT NULL"`
+	}
+	// there is no migrations because if there is no data on this table, it will fall back to get data
+	// from commit status
+	return x.Sync2(new(CommitStatusSummary))
+}
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index 4236553927..eb031511f6 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
 
 	"github.com/nektos/act/pkg/jobparser"
 )
@@ -122,18 +123,13 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
 	if err != nil {
 		return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err)
 	}
-	if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
-		Repo:    repo,
-		SHA:     commitID,
-		Creator: creator,
-		CommitStatus: &git_model.CommitStatus{
-			SHA:         sha,
-			TargetURL:   fmt.Sprintf("%s/jobs/%d", run.Link(), index),
-			Description: description,
-			Context:     ctxname,
-			CreatorID:   creator.ID,
-			State:       state,
-		},
+	if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &git_model.CommitStatus{
+		SHA:         sha,
+		TargetURL:   fmt.Sprintf("%s/jobs/%d", run.Link(), index),
+		Description: description,
+		Context:     ctxname,
+		CreatorID:   creator.ID,
+		State:       state,
 	}); err != nil {
 		return fmt.Errorf("NewCommitStatus: %w", err)
 	}
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
index 145fc7d53c..167a5330dd 100644
--- a/services/repository/commitstatus/commitstatus.go
+++ b/services/repository/commitstatus/commitstatus.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"crypto/sha256"
 	"fmt"
+	"slices"
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
@@ -59,13 +60,19 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
 		sha = commit.ID.String()
 	}
 
-	if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
-		Repo:         repo,
-		Creator:      creator,
-		SHA:          commit.ID,
-		CommitStatus: status,
+	if err := db.WithTx(ctx, func(ctx context.Context) error {
+		if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
+			Repo:         repo,
+			Creator:      creator,
+			SHA:          commit.ID,
+			CommitStatus: status,
+		}); err != nil {
+			return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+		}
+
+		return git_model.UpdateCommitStatusSummary(ctx, repo.ID, commit.ID.String())
 	}); err != nil {
-		return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+		return err
 	}
 
 	defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
@@ -114,8 +121,35 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
 		return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err)
 	}
 
+	var repoSHAs []git_model.RepoSHA
+	for id, sha := range repoIDsToLatestCommitSHAs {
+		repoSHAs = append(repoSHAs, git_model.RepoSHA{RepoID: id, SHA: sha})
+	}
+
+	summaryResults, err := git_model.GetLatestCommitStatusForRepoAndSHAs(ctx, repoSHAs)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestCommitStatusForRepoAndSHAs: %v", err)
+	}
+
+	for _, summary := range summaryResults {
+		for i, repo := range repos {
+			if repo.ID == summary.RepoID {
+				results[i] = summary
+				_ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
+					return repoSHA.RepoID == repo.ID
+				})
+				if results[i].State != "" {
+					if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil {
+						log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+					}
+				}
+				break
+			}
+		}
+	}
+
 	// call the database O(1) times to get the commit statuses for all repos
-	repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll)
+	repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
 	if err != nil {
 		return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err)
 	}
diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go
index 26c99e6445..bb7098e424 100644
--- a/tests/integration/pull_status_test.go
+++ b/tests/integration/pull_status_test.go
@@ -12,6 +12,9 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	git_model "code.gitea.io/gitea/models/git"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
 	api "code.gitea.io/gitea/modules/structs"
 
 	"github.com/stretchr/testify/assert"
@@ -90,6 +93,10 @@ func TestPullCreate_CommitStatus(t *testing.T) {
 			assert.True(t, ok)
 			assert.Contains(t, cls, statesIcons[status])
 		}
+
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
+		css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID})
+		assert.EqualValues(t, api.CommitStatusWarning, css.State)
 	})
 }
 

From e8a99c8f92c5b36382abb38a1471c94245457560 Mon Sep 17 00:00:00 2001
From: HEREYUA <37935145+HEREYUA@users.noreply.github.com>
Date: Fri, 12 Apr 2024 10:08:58 +0800
Subject: [PATCH 671/679] Fix the spacing issue in the Project view (#30415)

**fix**:  [#30388](https://github.com/go-gitea/gitea/issues/30388)

**before**

![image](https://github.com/go-gitea/gitea/assets/37935145/52ca7311-dca4-4430-9a37-3c45b08fe3dd)

**after**

![image](https://github.com/go-gitea/gitea/assets/37935145/6b75ce69-4423-4ea4-99a1-d7234287c5c0)
---
 templates/org/projects/list.tmpl              | 6 ++----
 templates/org/projects/view.tmpl              | 2 +-
 templates/user/overview/package_versions.tmpl | 6 ++----
 templates/user/overview/packages.tmpl         | 6 ++----
 templates/user/profile.tmpl                   | 7 ++-----
 5 files changed, 9 insertions(+), 18 deletions(-)

diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl
index ec9cfece9a..80dde1c4d2 100644
--- a/templates/org/projects/list.tmpl
+++ b/templates/org/projects/list.tmpl
@@ -13,11 +13,9 @@
 				<div class="ui four wide column">
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
-				<div class="ui twelve wide column">
-				<div class="tw-mb-4">
+				<div class="ui twelve wide column tw-mb-4">
 					{{template "user/overview/header" .}}
-				</div>
-				{{template "projects/list" .}}
+					{{template "projects/list" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl
index 495204b06d..e1ab81c4cd 100644
--- a/templates/org/projects/view.tmpl
+++ b/templates/org/projects/view.tmpl
@@ -1,7 +1,7 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 	{{template "shared/user/org_profile_avatar" .}}
-	<div class="ui container">
+	<div class="ui container tw-mb-4">
 		{{template "user/overview/header" .}}
 	</div>
 	<div class="ui container fluid padded">
diff --git a/templates/user/overview/package_versions.tmpl b/templates/user/overview/package_versions.tmpl
index b2cc814e13..0ac2db0d86 100644
--- a/templates/user/overview/package_versions.tmpl
+++ b/templates/user/overview/package_versions.tmpl
@@ -13,11 +13,9 @@
 				<div class="ui four wide column">
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
-				<div class="ui twelve wide column">
-					<div class="tw-mb-4">
+				<div class="ui twelve wide column tw-mb-4">
 						{{template "user/overview/header" .}}
-					</div>
-					{{template "package/shared/versionlist" .}}
+						{{template "package/shared/versionlist" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/overview/packages.tmpl b/templates/user/overview/packages.tmpl
index 95cb506e57..bb2238b919 100644
--- a/templates/user/overview/packages.tmpl
+++ b/templates/user/overview/packages.tmpl
@@ -13,11 +13,9 @@
 				<div class="ui four wide column">
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
-				<div class="ui twelve wide column">
-					<div class="tw-mb-4">
+				<div class="ui twelve wide column tw-mb-4">
 						{{template "user/overview/header" .}}
-					</div>
-					{{template "package/shared/list" .}}
+						{{template "package/shared/list" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index e68f79fae6..cf61bb906a 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -5,11 +5,8 @@
 			<div class="ui four wide column">
 				{{template "shared/user/profile_big_avatar" .}}
 			</div>
-			<div class="ui twelve wide column">
-				<div class="tw-mb-4">
-					{{template "user/overview/header" .}}
-				</div>
-
+			<div class="ui twelve wide column tw-mb-4">
+				{{template "user/overview/header" .}}
 				{{if eq .TabName "activity"}}
 					{{if .ContextUser.KeepActivityPrivate}}
 						<div class="ui info message">

From 7af074dbeebc3c863618992b43f84ec9e5ab9657 Mon Sep 17 00:00:00 2001
From: "Kazushi (Jam) Marukawa" <jam@pobox.com>
Date: Fri, 12 Apr 2024 11:51:40 +0900
Subject: [PATCH 672/679] Change the default maxPerPage for gitbucket (#30392)

This patch improves the migration from gitbucket to gitea.

The gitbucket uses it's own internal perPage value (= 25) for paging and
ignore per_page arguments in the requested URL. This cause gitea to
migrate only 25 issues and 25 PRs from gitbucket repository. This may
not happens on old gitbucket. But recent gitbucket 4.40 or 4.38.4 has
this problem.

This patch change to use this internally hardcoded perPage of gitbucket
as gitea's maxPerPage numer when migrating from gitbucket. There are
several perPage values in gitbucket like 25 for Isseus/PRs and 10 for
Releases. Some of those API doesn't support paging yet. It sounds
difficult to implement, but using the minimum number among them worked
out very well. So, I use 10 in this patch.

Brief descriptions of problems and this patch are also available in
https://github.com/go-gitea/gitea/issues/30316.

In addition, I'm not sure what kind of test cases are possible to write
here. It's a test for migration, so it requires testing gitbucket server
and gitea server, I guess. Please let me know if it is possible to write
such test cases here. Thanks!
---
 services/migrations/gitbucket.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go
index 5f11555839..4fe9e30a39 100644
--- a/services/migrations/gitbucket.go
+++ b/services/migrations/gitbucket.go
@@ -72,6 +72,11 @@ func (g *GitBucketDownloader) LogString() string {
 // NewGitBucketDownloader creates a GitBucket downloader
 func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader {
 	githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName)
+	// Gitbucket 4.40 uses different internal hard-coded perPage values.
+	// Issues, PRs, and other major parts use 25.  Release page uses 10.
+	// Some API doesn't support paging yet.  Sounds difficult, but using
+	// minimum number among them worked out very well.
+	githubDownloader.maxPerPage = 10
 	githubDownloader.SkipReactions = true
 	githubDownloader.SkipReviews = true
 	return &GitBucketDownloader{

From f9fdac9809335729b2ac3227b2a5f71a62fc64ad Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 12 Apr 2024 11:36:34 +0800
Subject: [PATCH 673/679] Limit the max line length when parsing git grep
 output (#30418)

---
 modules/git/grep.go      | 20 ++++++++++++++++----
 modules/git/grep_test.go | 10 ++++++++++
 2 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/modules/git/grep.go b/modules/git/grep.go
index a6c486112a..e7d238e586 100644
--- a/modules/git/grep.go
+++ b/modules/git/grep.go
@@ -10,6 +10,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -27,6 +28,7 @@ type GrepOptions struct {
 	MaxResultLimit    int
 	ContextLineNumber int
 	IsFuzzy           bool
+	MaxLineLength     int // the maximum length of a line to parse, exceeding chars will be truncated
 }
 
 func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
@@ -71,10 +73,20 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
 			defer stdoutReader.Close()
 
 			isInBlock := false
-			scanner := bufio.NewScanner(stdoutReader)
+			rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
 			var res *GrepResult
-			for scanner.Scan() {
-				line := scanner.Text()
+			for {
+				lineBytes, isPrefix, err := rd.ReadLine()
+				if isPrefix {
+					lineBytes = slices.Clone(lineBytes)
+					for isPrefix && err == nil {
+						_, isPrefix, err = rd.ReadLine()
+					}
+				}
+				if len(lineBytes) == 0 && err != nil {
+					break
+				}
+				line := string(lineBytes) // the memory of lineBytes is mutable
 				if !isInBlock {
 					if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok {
 						isInBlock = true
@@ -100,7 +112,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
 					res.LineCodes = append(res.LineCodes, lineCode)
 				}
 			}
-			return scanner.Err()
+			return nil
 		},
 	})
 	// git grep exits by cancel (killed), usually it is caused by the limit of results
diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go
index b5fa437c53..7f4ded478f 100644
--- a/modules/git/grep_test.go
+++ b/modules/git/grep_test.go
@@ -41,6 +41,16 @@ func TestGrepSearch(t *testing.T) {
 		},
 	}, res)
 
+	res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1, MaxLineLength: 39})
+	assert.NoError(t, err)
+	assert.Equal(t, []*GrepResult{
+		{
+			Filename:    "java-hello/main.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] arg"},
+		},
+	}, res)
+
 	res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, res, 0)

From 9466fec879f4f2c88c7c1e7a5cffba319282ab66 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 12 Apr 2024 18:11:16 +0800
Subject: [PATCH 674/679] Fix rename branch 500 when the target branch is
 deleted but exist in database (#30430)

Fix #30428
---
 models/git/branch.go                         | 31 ++++++--
 routers/web/repo/setting/protected_branch.go |  8 +-
 tests/integration/rename_branch_test.go      | 80 ++++++++++++++++++--
 3 files changed, 107 insertions(+), 12 deletions(-)

diff --git a/models/git/branch.go b/models/git/branch.go
index fa0781fed1..2979dff3d2 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -297,6 +297,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 
 	sess := db.GetEngine(ctx)
 
+	// check whether from branch exist
 	var branch Branch
 	exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch)
 	if err != nil {
@@ -308,6 +309,24 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 		}
 	}
 
+	// check whether to branch exist or is_deleted
+	var dstBranch Branch
+	exist, err = db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, to).Get(&dstBranch)
+	if err != nil {
+		return err
+	}
+	if exist {
+		if !dstBranch.IsDeleted {
+			return ErrBranchAlreadyExists{
+				BranchName: to,
+			}
+		}
+
+		if _, err := db.GetEngine(ctx).ID(dstBranch.ID).NoAutoCondition().Delete(&dstBranch); err != nil {
+			return err
+		}
+	}
+
 	// 1. update branch in database
 	if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
 		Name: to,
@@ -362,12 +381,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 		return err
 	}
 
-	// 5. do git action
-	if err = gitAction(ctx, isDefault); err != nil {
-		return err
-	}
-
-	// 6. insert renamed branch record
+	// 5. insert renamed branch record
 	renamedBranch := &RenamedBranch{
 		RepoID: repo.ID,
 		From:   from,
@@ -378,6 +392,11 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 		return err
 	}
 
+	// 6. do git action
+	if err = gitAction(ctx, isDefault); err != nil {
+		return err
+	}
+
 	return committer.Commit()
 }
 
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index b30dc3b061..4bab3f897a 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -313,7 +313,13 @@ func RenameBranchPost(ctx *context.Context) {
 
 	msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
 	if err != nil {
-		ctx.ServerError("RenameBranch", err)
+		switch {
+		case git_model.IsErrBranchAlreadyExists(err):
+			ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
+			ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+		default:
+			ctx.ServerError("RenameBranch", err)
+		}
 		return
 	}
 
diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go
index 703fc243a4..13f6cf204b 100644
--- a/tests/integration/rename_branch_test.go
+++ b/tests/integration/rename_branch_test.go
@@ -5,17 +5,23 @@ package integration
 
 import (
 	"net/http"
+	"net/url"
 	"testing"
 
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	gitea_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestRenameBranch(t *testing.T) {
+	onGiteaRun(t, testRenameBranch)
+}
+
+func testRenameBranch(t *testing.T, u *url.URL) {
 	defer tests.PrepareTestEnv(t)()
 
 	unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: "master"})
@@ -26,20 +32,19 @@ func TestRenameBranch(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	htmlDoc := NewHTMLParser(t, resp.Body)
 
-	postData := map[string]string{
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
 		"_csrf": htmlDoc.GetCSRF(),
 		"from":  "master",
 		"to":    "main",
-	}
-	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", postData)
+	})
 	session.MakeRequest(t, req, http.StatusSeeOther)
 
 	// check new branch link
-	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", postData)
+	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", nil)
 	session.MakeRequest(t, req, http.StatusOK)
 
 	// check old branch link
-	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", postData)
+	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", nil)
 	resp = session.MakeRequest(t, req, http.StatusSeeOther)
 	location := resp.Header().Get("Location")
 	assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location)
@@ -47,4 +52,69 @@ func TestRenameBranch(t *testing.T) {
 	// check db
 	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	assert.Equal(t, "main", repo1.DefaultBranch)
+
+	// create branch1
+	csrf := GetCSRF(t, session, "/user2/repo1/src/branch/main")
+
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{
+		"_csrf":           csrf,
+		"new_branch_name": "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	branch1 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.Equal(t, "branch1", branch1.Name)
+
+	// create branch2
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{
+		"_csrf":           csrf,
+		"new_branch_name": "branch2",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	branch2 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	assert.Equal(t, "branch2", branch2.Name)
+
+	// rename branch2 to branch1
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+		"from":  "branch2",
+		"to":    "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+	flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
+	assert.NotNil(t, flashCookie)
+	assert.Contains(t, flashCookie.Value, "error")
+
+	branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	assert.Equal(t, "branch2", branch2.Name)
+	branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.Equal(t, "branch1", branch1.Name)
+
+	// delete branch1
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete", map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+		"name":  "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusOK)
+	branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	assert.Equal(t, "branch2", branch2.Name)
+	branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.True(t, branch1.IsDeleted) // virtual deletion
+
+	// rename branch2 to branch1 again
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+		"from":  "branch2",
+		"to":    "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	flashCookie = session.GetCookie(gitea_context.CookieNameFlash)
+	assert.NotNil(t, flashCookie)
+	assert.Contains(t, flashCookie.Value, "success")
+
+	unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.Equal(t, "branch1", branch1.Name)
 }

From 25427e0aee435cdedb9f8aae58767174d877767f Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Fri, 12 Apr 2024 13:34:12 +0300
Subject: [PATCH 675/679] Remove jQuery from the commit graph (except Fomantic)
 (#30395)

- Switched to plain JavaScript
- Tested the commit graph and it works as before

# Demo using JavaScript without jQuery

![demo](https://github.com/go-gitea/gitea/assets/20454870/d0755ed6-bb5c-4601-a2b7-ebccaf4abce4)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 web_src/js/features/repo-graph.js | 138 +++++++++++++++++-------------
 1 file changed, 77 insertions(+), 61 deletions(-)

diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
index a5b61bff54..0086b92021 100644
--- a/web_src/js/features/repo-graph.js
+++ b/web_src/js/features/repo-graph.js
@@ -1,14 +1,16 @@
 import $ from 'jquery';
+import {hideElem, showElem} from '../utils/dom.js';
 import {GET} from '../modules/fetch.js';
 
 export function initRepoGraphGit() {
   const graphContainer = document.getElementById('git-graph-container');
   if (!graphContainer) return;
 
-  $('#flow-color-monochrome').on('click', () => {
-    $('#flow-color-monochrome').addClass('active');
-    $('#flow-color-colored').removeClass('active');
-    $('#git-graph-container').removeClass('colored').addClass('monochrome');
+  document.getElementById('flow-color-monochrome')?.addEventListener('click', () => {
+    document.getElementById('flow-color-monochrome').classList.add('active');
+    document.getElementById('flow-color-colored')?.classList.remove('active');
+    graphContainer.classList.remove('colored');
+    graphContainer.classList.add('monochrome');
     const params = new URLSearchParams(window.location.search);
     params.set('mode', 'monochrome');
     const queryString = params.toString();
@@ -17,29 +19,31 @@ export function initRepoGraphGit() {
     } else {
       window.history.replaceState({}, '', window.location.pathname);
     }
-    $('.pagination a').each((_, that) => {
-      const href = that.getAttribute('href');
-      if (!href) return;
+    for (const link of document.querySelectorAll('.pagination a')) {
+      const href = link.getAttribute('href');
+      if (!href) continue;
       const url = new URL(href, window.location);
       const params = url.searchParams;
       params.set('mode', 'monochrome');
       url.search = `?${params.toString()}`;
-      that.setAttribute('href', url.href);
-    });
+      link.setAttribute('href', url.href);
+    }
   });
-  $('#flow-color-colored').on('click', () => {
-    $('#flow-color-colored').addClass('active');
-    $('#flow-color-monochrome').removeClass('active');
-    $('#git-graph-container').addClass('colored').removeClass('monochrome');
-    $('.pagination a').each((_, that) => {
-      const href = that.getAttribute('href');
-      if (!href) return;
+
+  document.getElementById('flow-color-colored')?.addEventListener('click', () => {
+    document.getElementById('flow-color-colored').classList.add('active');
+    document.getElementById('flow-color-monochrome')?.classList.remove('active');
+    graphContainer.classList.add('colored');
+    graphContainer.classList.remove('monochrome');
+    for (const link of document.querySelectorAll('.pagination a')) {
+      const href = link.getAttribute('href');
+      if (!href) continue;
       const url = new URL(href, window.location);
       const params = url.searchParams;
       params.delete('mode');
       url.search = `?${params.toString()}`;
-      that.setAttribute('href', url.href);
-    });
+      link.setAttribute('href', url.href);
+    }
     const params = new URLSearchParams(window.location.search);
     params.delete('mode');
     const queryString = params.toString();
@@ -56,20 +60,21 @@ export function initRepoGraphGit() {
     const ajaxUrl = new URL(url);
     ajaxUrl.searchParams.set('div-only', 'true');
     window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname);
-    $('#pagination').empty();
-    $('#rel-container').addClass('tw-hidden');
-    $('#rev-container').addClass('tw-hidden');
-    $('#loading-indicator').removeClass('tw-hidden');
+    document.getElementById('pagination').innerHTML = '';
+    hideElem('#rel-container');
+    hideElem('#rev-container');
+    showElem('#loading-indicator');
     (async () => {
       const response = await GET(String(ajaxUrl));
       const html = await response.text();
-      const $div = $(html);
-      $('#pagination').html($div.find('#pagination').html());
-      $('#rel-container').html($div.find('#rel-container').html());
-      $('#rev-container').html($div.find('#rev-container').html());
-      $('#loading-indicator').addClass('tw-hidden');
-      $('#rel-container').removeClass('tw-hidden');
-      $('#rev-container').removeClass('tw-hidden');
+      const div = document.createElement('div');
+      div.innerHTML = html;
+      document.getElementById('pagination').innerHTML = div.getElementById('pagination').innerHTML;
+      document.getElementById('rel-container').innerHTML = div.getElementById('rel-container').innerHTML;
+      document.getElementById('rev-container').innerHTML = div.getElementById('rev-container').innerHTML;
+      hideElem('#loading-indicator');
+      showElem('#rel-container');
+      showElem('#rev-container');
     })();
   };
   const dropdownSelected = params.getAll('branch');
@@ -77,8 +82,9 @@ export function initRepoGraphGit() {
     dropdownSelected.splice(0, 0, '...flow-hide-pr-refs');
   }
 
-  $('#flow-select-refs-dropdown').dropdown('set selected', dropdownSelected);
-  $('#flow-select-refs-dropdown').dropdown({
+  const flowSelectRefsDropdown = document.getElementById('flow-select-refs-dropdown');
+  $(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
+  $(flowSelectRefsDropdown).dropdown({
     clearable: true,
     fullTextSeach: 'exact',
     onRemove(toRemove) {
@@ -104,36 +110,46 @@ export function initRepoGraphGit() {
       updateGraph();
     },
   });
-  $('#git-graph-container').on('mouseenter', '#rev-list li', (e) => {
-    const flow = $(e.currentTarget).data('flow');
-    if (flow === 0) return;
-    $(`#flow-${flow}`).addClass('highlight');
-    $(e.currentTarget).addClass('hover');
-    $(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
+
+  graphContainer.addEventListener('mouseenter', (e) => {
+    if (e.target.matches('#rev-list li')) {
+      const flow = e.target.getAttribute('data-flow');
+      if (flow === '0') return;
+      document.getElementById(`flow-${flow}`)?.classList.add('highlight');
+      e.target.classList.add('hover');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.add('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-group')) {
+      e.target.classList.add('highlight');
+      const flow = e.target.getAttribute('data-flow');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.add('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-commit')) {
+      const rev = e.target.getAttribute('data-rev');
+      document.querySelector(`#rev-list li#commit-${rev}`)?.classList.add('hover');
+    }
   });
-  $('#git-graph-container').on('mouseleave', '#rev-list li', (e) => {
-    const flow = $(e.currentTarget).data('flow');
-    if (flow === 0) return;
-    $(`#flow-${flow}`).removeClass('highlight');
-    $(e.currentTarget).removeClass('hover');
-    $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
-  });
-  $('#git-graph-container').on('mouseenter', '#rel-container .flow-group', (e) => {
-    $(e.currentTarget).addClass('highlight');
-    const flow = $(e.currentTarget).data('flow');
-    $(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
-  });
-  $('#git-graph-container').on('mouseleave', '#rel-container .flow-group', (e) => {
-    $(e.currentTarget).removeClass('highlight');
-    const flow = $(e.currentTarget).data('flow');
-    $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
-  });
-  $('#git-graph-container').on('mouseenter', '#rel-container .flow-commit', (e) => {
-    const rev = $(e.currentTarget).data('rev');
-    $(`#rev-list li#commit-${rev}`).addClass('hover');
-  });
-  $('#git-graph-container').on('mouseleave', '#rel-container .flow-commit', (e) => {
-    const rev = $(e.currentTarget).data('rev');
-    $(`#rev-list li#commit-${rev}`).removeClass('hover');
+
+  graphContainer.addEventListener('mouseleave', (e) => {
+    if (e.target.matches('#rev-list li')) {
+      const flow = e.target.getAttribute('data-flow');
+      if (flow === '0') return;
+      document.getElementById(`flow-${flow}`)?.classList.remove('highlight');
+      e.target.classList.remove('hover');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.remove('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-group')) {
+      e.target.classList.remove('highlight');
+      const flow = e.target.getAttribute('data-flow');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.remove('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-commit')) {
+      const rev = e.target.getAttribute('data-rev');
+      document.querySelector(`#rev-list li#commit-${rev}`)?.classList.remove('hover');
+    }
   });
 }

From f9c3e79abac9dc417cdbbddf24a9fb8dc49363c4 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 12 Apr 2024 19:02:42 +0800
Subject: [PATCH 676/679] Fix commit status cache which missed target_url
 (#30426)

Fix #30421

---------

Co-authored-by: Jason Song <i@wolfogre.com>
---
 .../repository/commitstatus/commitstatus.go   | 54 ++++++++++++++-----
 1 file changed, 42 insertions(+), 12 deletions(-)

diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
index 167a5330dd..7c1c6c2609 100644
--- a/services/repository/commitstatus/commitstatus.go
+++ b/services/repository/commitstatus/commitstatus.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/services/automerge"
@@ -26,12 +27,41 @@ func getCacheKey(repoID int64, brancheName string) string {
 	return fmt.Sprintf("commit_status:%x", hashBytes)
 }
 
-func updateCommitStatusCache(ctx context.Context, repoID int64, branchName string, status api.CommitStatusState) error {
-	c := cache.GetCache()
-	return c.Put(getCacheKey(repoID, branchName), string(status), 3*24*60)
+type commitStatusCacheValue struct {
+	State     string `json:"state"`
+	TargetURL string `json:"target_url"`
 }
 
-func deleteCommitStatusCache(ctx context.Context, repoID int64, branchName string) error {
+func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue {
+	c := cache.GetCache()
+	statusStr, ok := c.Get(getCacheKey(repoID, branchName)).(string)
+	if ok && statusStr != "" {
+		var cv commitStatusCacheValue
+		err := json.Unmarshal([]byte(statusStr), &cv)
+		if err == nil && cv.State != "" {
+			return &cv
+		}
+		if err != nil {
+			log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
+		}
+	}
+	return nil
+}
+
+func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error {
+	c := cache.GetCache()
+	bs, err := json.Marshal(commitStatusCacheValue{
+		State:     state.String(),
+		TargetURL: targetURL,
+	})
+	if err != nil {
+		log.Warn("updateCommitStatusCache: json.Marshal failed: %v", err)
+		return nil
+	}
+	return c.Put(getCacheKey(repoID, branchName), string(bs), 3*24*60)
+}
+
+func deleteCommitStatusCache(repoID int64, branchName string) error {
 	c := cache.GetCache()
 	return c.Delete(getCacheKey(repoID, branchName))
 }
@@ -81,7 +111,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
 	}
 
 	if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
-		if err := deleteCommitStatusCache(ctx, repo.ID, repo.DefaultBranch); err != nil {
+		if err := deleteCommitStatusCache(repo.ID, repo.DefaultBranch); err != nil {
 			log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
 		}
 	}
@@ -98,12 +128,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
 // FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
 func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
 	results := make([]*git_model.CommitStatus, len(repos))
-	c := cache.GetCache()
-
 	for i, repo := range repos {
-		status, ok := c.Get(getCacheKey(repo.ID, repo.DefaultBranch)).(string)
-		if ok && status != "" {
-			results[i] = &git_model.CommitStatus{State: api.CommitStatusState(status)}
+		if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
+			results[i] = &git_model.CommitStatus{
+				State:     api.CommitStatusState(cv.State),
+				TargetURL: cv.TargetURL,
+			}
 		}
 	}
 
@@ -139,7 +169,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
 					return repoSHA.RepoID == repo.ID
 				})
 				if results[i].State != "" {
-					if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil {
+					if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
 						log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
 					}
 				}
@@ -158,7 +188,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
 		if results[i] == nil {
 			results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
 			if results[i].State != "" {
-				if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil {
+				if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
 					log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
 				}
 			}

From 487b12783fb2dba7459a8aa739162cfe6bab3904 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 12 Apr 2024 14:52:39 +0200
Subject: [PATCH 677/679] Lock a few tool dependencies to major versions
 (#30439)

It's better having to update these less often, so unlock a few
dependencies that I trust enough to not break to their latest major
versions. This excludes any tool still at major version 0 and
golangci-lint can't really be unlocked either because new versions
almost always break there.

For the v0 packages, I've opened
https://github.com/golangci/misspell/issues/14 and
https://github.com/mvdan/gofumpt/issues/303.
---
 Makefile | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Makefile b/Makefile
index ee9c90e8d9..f1acfbc81e 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ COMMA := ,
 
 XGO_VERSION := go-1.22.x
 
-AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0
+AIR_PACKAGE ?= github.com/cosmtrek/air@v1
 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2
@@ -33,9 +33,9 @@ GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1
 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285
 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
-GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0
-GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3
-ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.6.26
+GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
+GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
+ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
 
 DOCKER_IMAGE ?= gitea/gitea
 DOCKER_TAG ?= latest

From 68271834d6ae6d397b5a2048f9e515ff53735994 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 13 Apr 2024 04:28:20 +0200
Subject: [PATCH 678/679] Add `/public/assets/img/webpack` to ignore files
 again (#30451)

Fixes https://github.com/go-gitea/gitea/issues/30442

It's inconvenient to have new untracked files show up in git when
switching to older branches that had generated them.

Introduce a list of such files and folders to gitignore and
dockerignore.
---
 .dockerignore | 3 +++
 .gitignore    | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/.dockerignore b/.dockerignore
index b299c7313d..b696e1603c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -95,6 +95,9 @@ cpu.out
 /.air
 /.go-licenses
 
+# Files and folders that were previously generated
+/public/assets/img/webpack
+
 # Snapcraft
 snap/.snapcraft/
 parts/
diff --git a/.gitignore b/.gitignore
index 501fef7dcf..46c8b9b49c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,6 +94,9 @@ cpu.out
 /.air
 /.go-licenses
 
+# Files and folders that were previously generated
+/public/assets/img/webpack
+
 # Snapcraft
 /gitea_a*.txt
 snap/.snapcraft/

From b4d86912ef18c58e973ee10c4a3bc621e9bd6c52 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Sat, 13 Apr 2024 12:01:02 +0900
Subject: [PATCH 679/679] Fix mirror error when mirror repo is empty (#30432)

Fix #30424

Co-authored-by: Giteabot <teabot@gitea.io>
---
 services/mirror/mirror_pull.go | 40 +++++++++++++++++++---------------
 1 file changed, 23 insertions(+), 17 deletions(-)

diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index 2a38d4ba55..21d5f08205 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -449,19 +449,17 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
 		return false
 	}
 
-	var gitRepo *git.Repository
-	if len(results) == 0 {
-		log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo)
-	} else {
-		log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
-		gitRepo, err = gitrepo.OpenRepository(ctx, m.Repo)
-		if err != nil {
-			log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err)
-			return false
-		}
-		defer gitRepo.Close()
+	gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo)
+	if err != nil {
+		log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err)
+		return false
+	}
+	defer gitRepo.Close()
 
+	log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
+	if len(results) > 0 {
 		if ok := checkAndUpdateEmptyRepository(ctx, m, gitRepo, results); !ok {
+			log.Error("SyncMirrors [repo: %-v]: checkAndUpdateEmptyRepository: %v", m.Repo, err)
 			return false
 		}
 	}
@@ -534,16 +532,24 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
 	}
 	log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo)
 
-	// Get latest commit date and update to current repository updated time
-	commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath())
+	isEmpty, err := gitRepo.IsEmpty()
 	if err != nil {
-		log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
+		log.Error("SyncMirrors [repo: %-v]: unable to check empty git repo: %v", m.Repo, err)
 		return false
 	}
+	if !isEmpty {
+		// Get latest commit date and update to current repository updated time
+		commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath())
+		if err != nil {
+			log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
+			return false
+		}
+
+		if err = repo_model.UpdateRepositoryUpdatedTime(ctx, m.RepoID, commitDate); err != nil {
+			log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err)
+			return false
+		}
 
-	if err = repo_model.UpdateRepositoryUpdatedTime(ctx, m.RepoID, commitDate); err != nil {
-		log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err)
-		return false
 	}
 
 	log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)